LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
sum_closed.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_SUM_SUM_CLOSED_H
6#define LINE_API_SUM_SUM_CLOSED_H
7
8/**
9 * @file
10 * @ingroup api_sum
11 * Summation method (SUM) and its extension (ESUM) for closed queueing
12 * networks, including non-product-form stations with generally distributed
13 * service times.
14 *
15 * Templated port of matlab/src/api/sum/sum_closed.m, cross-checked against
16 * jar/src/main/java/jline/api/sum/Sum_closed.java (the two agree step for
17 * step, including the Gauss-Seidel multiclass variant and the incremental
18 * Erlang-C evaluation).
19 *
20 * The method writes the mean queue length of a station as a function of its
21 * throughput, K_i = f_i(lambda_i), and closes the model with the population
22 * constraint sum_i K_i + lambda Z = K. A single class is solved by bisection
23 * on the system throughput (Bolch et al., Sec. 9.2.1); several classes by
24 * Gauss-Seidel sweeps of per-class bisections on the per-class constraints,
25 * which is more robust than the successive substitution of Sec. 9.2.2 because
26 * it cannot overshoot the saturation polytope.
27 *
28 * Node functions:
29 * - product-form stations (scv = 1, or an insensitive discipline for which
30 * the caller passes scv = 1): Eq. (9.15)/(9.19)
31 * - FCFS with general service (scv != 1): the ESUM corrections, Eq. (10.88)
32 * for -/G/1 and Eq. (10.89) for -/G/m, with a_i = (1+scv_i)/2 and the
33 * Erlang-C waiting probability
34 * - infinite-server stations and think time: K_i = lambda_i L_i
35 *
36 * Reference: G. Bolch, S. Greiner, H. de Meer, K.S. Trivedi, Queueing Networks
37 * and Markov Chains, 2nd ed., Wiley, 2006, Secs. 9.2 and 10.1.4.4.
38 *
39 * ARITHMETIC: a bisection stopped on a tolerance, so the answer is the root of
40 * the population constraint only to within tol whatever the arithmetic. Gated
41 * on has_transcendental for that reason; the Erlang-C evaluation is itself a
42 * finite field computation (the a^k/k! terms are built incrementally) and
43 * needs no transcendental function.
44 *
45 * Infinity is carried by the explicit Servers::infinite flag rather than by a
46 * floating infinity, so the same code compiles for a number type that has no
47 * infinity at all. An infinite population is not accepted here: MATLAB's
48 * N(r) = Inf is only ever produced by sum_closing, which substitutes the
49 * closing population before calling in.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <vector>
55
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/matrix.h"
59
60namespace line {
61namespace sum {
62
63/** Number of servers of a station; MATLAB's mi(i) = Inf becomes infinite. */
64struct Servers {
65 long m = 1;
66 bool infinite = false;
67
68 static Servers of(long m) {
69 Servers s;
70 s.m = m;
71 s.infinite = false;
72 return s;
73 }
74 static Servers inf() {
75 Servers s;
76 s.m = 0;
77 s.infinite = true;
78 return s;
79 }
80};
81
82/** Mirrors the [XN, QN, UN, RN, it] return list of the MATLAB function. */
83template <class T>
85 std::vector<T> XN; ///< (R) class throughputs
86 Matrix<T> QN; ///< (M x R) mean queue lengths
87 Matrix<T> UN; ///< (M x R) utilizations, per server at queueing stations
88 Matrix<T> RN; ///< (M x R) residence times, QN/XN
89 std::size_t it = 0; ///< iterations of the outer loop
90};
91
92/** Convergence controls, mirroring the trailing (tol, maxiter) arguments. */
93struct SumOptions {
94 double tol = 1e-6;
95 std::size_t maxiter = 10000;
96};
97
98namespace detail {
99
100/**
101 * Erlang-C waiting probability of an M/M/m queue, Eq. (6.28), built from the
102 * incremental terms a^k/k! so that no factorial and no real power is formed.
103 */
104template <class T>
105T sum_erlangc(long m, const T& rho) {
106 const T one = num_traits<T>::from_int(1);
107 if (!(rho < one)) return one;
108 const T a = num_traits<T>::from_int(m) * rho;
110 T term = one; // a^k / k!
111 for (long k = 0; k < m; ++k) {
112 if (k > 0) term *= a / num_traits<T>::from_int(k);
113 s += term;
114 }
115 const T last = term * a / num_traits<T>::from_int(m) / (one - rho);
116 return last / (s + last);
117}
118
119/** Per-station per-class mean queue lengths K_ir = f_ir(lambda_r). */
120template <class T>
121Matrix<T> sum_node_qlen(const Matrix<T>& L, const std::vector<T>& XN,
122 const std::vector<Servers>& mi, const Matrix<T>& scv, long K) {
123 const std::size_t M = L.rows(), R = L.cols();
124 const T one = num_traits<T>::from_int(1);
125 const T zero = num_traits<T>::from_int(0);
126 Matrix<T> Qir(M, R, zero);
127 for (std::size_t i = 0; i < M; ++i) {
128 if (mi[i].infinite) {
129 for (std::size_t r = 0; r < R; ++r) Qir(i, r) = XN[r] * L(i, r); // Type 3, Eq. (9.15)
130 continue;
131 }
132 const long m = mi[i].m;
133 std::vector<T> Uir(R);
134 T Ui = zero, ci2num = zero;
135 for (std::size_t r = 0; r < R; ++r) {
136 Uir[r] = XN[r] * L(i, r);
137 Ui += Uir[r];
138 ci2num += Uir[r] * scv(i, r);
139 }
140 if (Ui == zero) continue;
141 // per-server utilization; the correction factors below stay finite at
142 // rho = 1 because the node function is capped by the population
143 T rho = Ui / num_traits<T>::from_int(m);
144 if (rho > one) rho = one;
145 const T ci2 = ci2num / Ui; // demand-weighted node service SCV
146 const T ai = (one + ci2) / num_traits<T>::from_int(2);
147 if (K <= m) {
148 // never more than m jobs at an m-server node: no queueing at all
149 for (std::size_t r = 0; r < R; ++r) Qir(i, r) = Uir[r];
150 continue;
151 }
152 const T Kt = num_traits<T>::from_int(K);
153 const T mt = num_traits<T>::from_int(m);
154 if (m == 1) {
155 if (ci2 == one || K <= 1) {
156 // Type 1, 2, 4 with m = 1, Eq. (9.15)/(9.19)
157 const T den = one - (Kt - one) / Kt * rho;
158 for (std::size_t r = 0; r < R; ++r) Qir(i, r) = Uir[r] / den;
159 } else {
160 // -/G/1 FCFS, Eq. (10.88)
161 const T den = one - (Kt - one - ai) / (Kt - one) * rho;
162 for (std::size_t r = 0; r < R; ++r) Qir(i, r) = Uir[r] * (one + rho * ai / den);
163 }
164 } else {
165 const T Pm = sum_erlangc(m, rho);
166 if (ci2 == one) {
167 // Type 1 with m > 1, Eq. (9.15)/(9.19)
168 const T den = one - (Kt - mt - one) / (Kt - mt) * rho;
169 for (std::size_t r = 0; r < R; ++r) Qir(i, r) = Uir[r] + (Uir[r] / mt) * Pm / den;
170 } else {
171 // -/G/m FCFS, Eq. (10.89)
172 const T den = one - (Kt - mt - ai) / (Kt - mt) * rho;
173 for (std::size_t r = 0; r < R; ++r)
174 Qir(i, r) = Uir[r] + (Uir[r] / mt) * ai * Pm / den;
175 }
176 }
177 }
178 return Qir;
179}
180
181} // namespace detail
182
183/**
184 * @brief Summation method (SUM) and its extension (ESUM) for closed queueing
185 * networks, including non-product-form stations with generally
186 * distributed service times.
187 *
188 * @param L (M x R) service demands, L(i,r) = e(i,r)/mu(i,r)
189 * @param N (R) population per class, all finite
190 * @param Z (R) think times
191 * @param mi (M) servers per station
192 * @param scv (M x R) squared coefficients of variation of the service times
193 * @param options tolerances, iteration caps and the closing method
194 */
195template <class T>
196SumClosedResult<T> sum_closed(const Matrix<T>& L, const std::vector<long>& N,
197 const std::vector<T>& Z, const std::vector<Servers>& mi,
198 const Matrix<T>& scv, const SumOptions& options = SumOptions()) {
200 "sum_closed requires transcendental arithmetic: it locates the root of the "
201 "population constraint by bisection to within tol, so its answer is inexact "
202 "whatever the arithmetic");
203 const std::size_t M = L.rows(), R = L.cols();
204 if (N.size() != R) throw InputError("sum_closed: L and N disagree on the class count");
205 if (Z.size() != R) throw InputError("sum_closed: L and Z disagree on the class count");
206 if (mi.size() != M) throw InputError("sum_closed: L and mi disagree on the station count");
207 if (scv.rows() != M || scv.cols() != R) throw InputError("sum_closed: scv has the wrong shape");
208 for (long v : N)
209 if (v < 0) throw InputError("sum_closed: negative population");
210
211 const T zero = num_traits<T>::from_int(0);
212 const T two = num_traits<T>::from_int(2);
214 out.XN.assign(R, zero);
215 out.QN = Matrix<T>(M, R, zero);
216 out.UN = Matrix<T>(M, R, zero);
217 out.RN = Matrix<T>(M, R, zero);
218
219 long K = 0;
220 for (long v : N) K += v;
221 if (K == 0) return out;
222
223 if (R == 1) {
224 // single class: bisection on the system throughput (Sec. 9.2.1)
225 T lambda_l = zero, lambda_u = zero;
226 bool ub_set = false;
227 for (std::size_t i = 0; i < M; ++i) {
228 if (L(i, 0) > zero) {
229 const T cand = mi[i].infinite ? num_traits<T>::from_int(K) / L(i, 0)
230 : num_traits<T>::from_int(mi[i].m) / L(i, 0);
231 if (!ub_set || cand < lambda_u) {
232 lambda_u = cand;
233 ub_set = true;
234 }
235 }
236 }
237 if (Z[0] > zero) {
238 const T cand = num_traits<T>::from_int(K) / Z[0];
239 if (!ub_set || cand < lambda_u) {
240 lambda_u = cand;
241 ub_set = true;
242 }
243 }
244 if (!ub_set) throw InputError("sum_closed: all service demands are zero");
245
246 T lambda = lambda_u;
247 for (out.it = 1; out.it <= options.maxiter; ++out.it) {
248 lambda = (lambda_l + lambda_u) / two;
249 std::vector<T> X(1, lambda);
250 const Matrix<T> Qir = detail::sum_node_qlen(L, X, mi, scv, K);
251 T g = lambda * Z[0];
252 for (std::size_t i = 0; i < M; ++i) g += Qir(i, 0);
253 const double gap = num_traits<T>::to_double(num_abs(T(g - num_traits<T>::from_int(K))));
254 const double width = num_traits<T>::to_double(T(lambda_u - lambda_l));
255 if (gap <= options.tol ||
256 width <= options.tol * num_traits<T>::to_double(lambda_u))
257 break;
258 if (g > num_traits<T>::from_int(K))
259 lambda_u = lambda;
260 else
261 lambda_l = lambda;
262 }
263 if (out.it > options.maxiter) out.it = options.maxiter;
264 out.XN[0] = lambda;
265 } else {
266 // multiclass: Gauss-Seidel sweeps of per-class bisections
267 for (out.it = 1; out.it <= options.maxiter; ++out.it) {
268 double delta = 0.0;
269 for (std::size_t r = 0; r < R; ++r) {
270 if (N[r] == 0) continue;
271 T ub = zero;
272 bool ub_set = false;
273 for (std::size_t i = 0; i < M; ++i) {
274 if (!(L(i, r) > zero)) continue;
275 T cand;
276 if (mi[i].infinite) {
277 cand = num_traits<T>::from_int(K) / L(i, r);
278 } else {
279 T rowload = zero;
280 for (std::size_t q = 0; q < R; ++q) rowload += out.XN[q] * L(i, q);
281 T rem = num_traits<T>::from_int(mi[i].m) - (rowload - out.XN[r] * L(i, r));
282 if (rem < zero) rem = zero;
283 cand = rem / L(i, r);
284 }
285 if (!ub_set || cand < ub) {
286 ub = cand;
287 ub_set = true;
288 }
289 }
290 if (Z[r] > zero) {
291 const T cand = num_traits<T>::from_int(N[r]) / Z[r];
292 if (!ub_set || cand < ub) {
293 ub = cand;
294 ub_set = true;
295 }
296 }
297 if (!ub_set) throw InputError("sum_closed: all service demands are zero");
298
299 const T lambda_old = out.XN[r];
300 T lambda_l = zero, lambda_u = ub;
301 const double ubd = num_traits<T>::to_double(ub);
302 const double stop = options.tol * (ubd > 1.0 ? ubd : 1.0) / 1e3;
303 while (num_traits<T>::to_double(T(lambda_u - lambda_l)) > stop) {
304 const T lambda = (lambda_l + lambda_u) / two;
305 out.XN[r] = lambda;
306 const Matrix<T> Qir = detail::sum_node_qlen(L, out.XN, mi, scv, K);
307 T g = lambda * Z[r];
308 for (std::size_t i = 0; i < M; ++i) g += Qir(i, r);
309 if (g > num_traits<T>::from_int(N[r]))
310 lambda_u = lambda;
311 else
312 lambda_l = lambda;
313 }
314 out.XN[r] = (lambda_l + lambda_u) / two;
315 const double d = num_traits<T>::to_double(num_abs(T(out.XN[r] - lambda_old)));
316 if (d > delta) delta = d;
317 }
318 if (delta <= options.tol) break;
319 }
320 if (out.it > options.maxiter) out.it = options.maxiter;
321 }
322
323 out.QN = detail::sum_node_qlen(L, out.XN, mi, scv, K);
324 for (std::size_t i = 0; i < M; ++i)
325 for (std::size_t r = 0; r < R; ++r) {
326 out.UN(i, r) = mi[i].infinite
327 ? T(out.XN[r] * L(i, r))
328 : T(out.XN[r] * L(i, r) / num_traits<T>::from_int(mi[i].m));
329 if (out.XN[r] > zero) out.RN(i, r) = out.QN(i, r) / out.XN[r];
330 }
331 return out;
332}
333
334} // namespace sum
335} // namespace line
336
337#endif // LINE_API_SUM_SUM_CLOSED_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.
SumClosedResult< T > sum_closed(const Matrix< T > &L, const std::vector< long > &N, const std::vector< T > &Z, const std::vector< Servers > &mi, const Matrix< T > &scv, const SumOptions &options=SumOptions())
Summation method (SUM) and its extension (ESUM) for closed queueing networks, including non-product-f...
Definition sum_closed.h:196
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Number of servers of a station; MATLAB's mi(i) = Inf becomes infinite.
Definition sum_closed.h:64
static Servers of(long m)
Definition sum_closed.h:68
static Servers inf()
Definition sum_closed.h:74
Mirrors the [XN, QN, UN, RN, it] return list of the MATLAB function.
Definition sum_closed.h:84
Matrix< T > QN
(M x R) mean queue lengths
Definition sum_closed.h:86
Matrix< T > UN
(M x R) utilizations, per server at queueing stations
Definition sum_closed.h:87
std::size_t it
iterations of the outer loop
Definition sum_closed.h:89
Matrix< T > RN
(M x R) residence times, QN/XN
Definition sum_closed.h:88
std::vector< T > XN
(R) class throughputs
Definition sum_closed.h:85
Convergence controls, mirroring the trailing (tol, maxiter) arguments.
Definition sum_closed.h:93
std::size_t maxiter
Definition sum_closed.h:95