LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_conwayms.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_CONWAYMS_H
6#define LINE_API_PFQN_CONWAYMS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Conway's multiserver Linearizer for chain-dependent FCFS queues (Conway
12 * 1989, "Fast Approximate Solution of Queueing Networks with Multi-Server
13 * Chain-Dependent FCFS Queues").
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_conwayms.m, cross-checked against
16 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_conwayms.java.
17 *
18 * The distinguishing feature over pfqn_linearizerms is the pair of conditional
19 * service rates at a c-server station, obtained by averaging over the
20 * compositions n of the c busy servers among the R chains,
21 *
22 * A_i(n) = multinomial(n) prod_c F_r(i,c)^{n_c}, F_r(i,c) = T_1(c|r) L(i,c) / sum_c' ...
23 * XR(i,r) = sum_{n in B_r} A_i(n) / (sum_c n_c / L(i,c)) / sum_{n in B_r} A_i(n)
24 * XE(i,r,c) = the same restricted to n_c >= 1
25 *
26 * with B_r = { n : sum(n) = c, n <= N - e_r }, which enter the residence time
27 * as W = L + PB XR + sum_c XE (Q_1 - L T_1).
28 *
29 * Arithmetic: TRANSCENDENTAL-GATED, on the fixed-point tolerance alone. The
30 * inner Core loop stops on norm(Q_{k+1} - Q_k) < tol, so the returned value
31 * depends on the stopping rule. The reference forms A_i(n) as
32 * exp(multinomialln(n) + n log F), but that is a convenience: the value is a
33 * finite rational in F, and this port computes it as multinomial(n) times an
34 * integer power product, which is both exact and free of the 0 * (-Inf) = NaN
35 * the reference produces whenever some F_r(i,c) vanishes at n_c == 0.
36 *
37 * Convergence norm and the FCFS selection follow pfqn_linearizerms: Frobenius
38 * rather than spectral (dominating, same fixed point), and the FCFS arm of the
39 * single-server residence time is taken only when every station is FCFS, which
40 * is what MATLAB's `if type == SchedStrategy.FCFS` on a vector means. The JAR
41 * inverts this test; MATLAB is the reference.
42 */
43
44#include <cstddef>
45#include <vector>
46
49#include "line/api/pfqn/pfqn_linearizerms.h" // shares the marginal-probability initialization
50#include "line/num/number.h"
51#include "line/util/error.h"
52#include "line/util/matrix.h"
53
54namespace line {
55namespace pfqn {
56
57namespace detail {
58
59/** Estimate step: frozen-Delta queue lengths and the auxiliary throughputs. */
60template <class T>
61void conway_estimate(std::size_t M, std::size_t R, const std::vector<int>& N_1,
62 const Matrix<T>& Q, const std::vector<Matrix<T>>& Delta, const Matrix<T>& W,
63 std::vector<Matrix<T>>& Q1, Matrix<T>& T_1) {
64 const T zero = num_traits<T>::from_int(0);
65 for (std::size_t i = 0; i < M; ++i)
66 for (std::size_t r = 0; r < R; ++r)
67 for (std::size_t s = 1; s <= R; ++s) {
68 const std::vector<int> Ns = oner(N_1, s);
69 if (N_1[r] <= 0 || Ns[r] <= 0) {
70 Q1[s](i, r) = zero;
71 } else {
72 Q1[s](i, r) = num_traits<T>::from_int(Ns[r]) *
73 (Q(i, r) / num_traits<T>::from_int(N_1[r]) + Delta[r](i, s - 1));
74 }
75 }
76 // T_1 is Little's law over the queueing part of the cycle, sum_i Q1 / sum_i W,
77 // and not the ratio at the FIRST station with a positive residence time: the
78 // per-station estimates disagree, so picking one made the answer depend on the
79 // station order. The demand matrix carries no order, so a model symmetric under
80 // permuting classes and stations together must return equal class throughputs,
81 // and with the single-station pick it did not.
82 for (std::size_t s = 0; s < R; ++s)
83 for (std::size_t r = 0; r < R; ++r) T_1(s, r) = zero;
84 for (std::size_t r = 1; r <= R; ++r) {
85 const std::vector<int> Nr = oner(N_1, r);
86 for (std::size_t s = 0; s < R; ++s) {
87 if (N_1[s] <= 0 || Nr[s] <= 0) continue;
88 T num = zero;
89 T den = zero;
90 for (std::size_t i = 0; i < M; ++i) {
91 if (W(i, s) > zero) {
92 // Delta is indexed [queued class](station, removed class), as the
93 // Q1 loop above uses it: here class s queues and class r-1 is removed
94 num += num_traits<T>::from_int(Nr[s]) *
95 (Q(i, s) / num_traits<T>::from_int(N_1[s]) + Delta[s](i, r - 1));
96 den += W(i, s);
97 }
98 }
99 // a reduced-population throughput cannot be negative; a negative one
100 // makes log(F) complex in the XR/XE sums of the forward step
101 if (den > zero) {
102 const T t1 = num / den;
103 T_1(s, r - 1) = (t1 < zero) ? zero : t1;
104 }
105 }
106 }
107}
108
109/**
110 * The conditional service rates XR(i,r) and XE(i,r,c) at the multiserver
111 * stations, by enumeration of the compositions of the server count.
112 */
113template <class T>
114void conway_conditional_rates(const Matrix<T>& L, std::size_t M, std::size_t R,
115 const std::vector<int>& N_1, const std::vector<int>& nservers,
116 const Matrix<T>& T_1, std::vector<T>& XRflat,
117 std::vector<T>& XEflat) {
118 const T zero = num_traits<T>::from_int(0);
119 XRflat.assign(M * R, zero);
120 XEflat.assign(M * R * R, zero);
121 for (std::size_t r = 0; r < R; ++r) {
122 // F_r(i,c), the chain-composition weights at station i.
123 Matrix<T> F(M, R, zero);
124 for (std::size_t i = 0; i < M; ++i) {
125 T den = zero;
126 for (std::size_t c = 0; c < R; ++c) den += L(i, c) * T_1(c, r);
127 if (den == zero) continue; // station unreachable at this population
128 for (std::size_t c = 0; c < R; ++c) F(i, c) = T_1(c, r) * L(i, c) / den;
129 }
130 const std::vector<int> Nr = oner(N_1, r + 1);
131 for (std::size_t i = 0; i < M; ++i) {
132 if (nservers[i] <= 1) continue;
133 std::vector<int> n(R, 0);
134 first_composition(n, nservers[i]);
135 T Csum = zero, XRacc = zero;
136 std::vector<T> Cx(R, zero), XEacc(R, zero);
137 bool more = true;
138 while (more) {
139 bool inB = true;
140 for (std::size_t c = 0; c < R; ++c)
141 if (n[c] > Nr[c]) inB = false;
142 if (inB) {
143 // A_i(n) = multinomial(n) prod_{c: n_c > 0} F(i,c)^{n_c}.
144 T Ai = num_multinomial<T>(n);
145 for (std::size_t c = 0; c < R; ++c)
146 if (n[c] > 0) Ai *= num_pow_int(F(i, c), static_cast<unsigned>(n[c]));
147 // sum_c n_c mu(i,c) with mu = 1/L, skipping the absent chains
148 // so that a zero demand never contributes 0 * Inf. A chain that
149 // occupies a server where it has zero demand has mu = Inf in
150 // MATLAB (mu = 1./L), so Ai*(mu*n)^(-1) = 0 and, since F(i,c) = 0
151 // there, Ai = 0 as well: the composition contributes nothing.
152 T rate = zero;
153 bool unbounded = false;
154 for (std::size_t c = 0; c < R; ++c) {
155 if (n[c] == 0) continue;
156 if (L(i, c) == zero) {
157 unbounded = true;
158 break;
159 }
160 rate += num_traits<T>::from_int(n[c]) / L(i, c);
161 }
162 if (!unbounded && rate != zero) {
163 Csum += Ai;
164 XRacc += Ai / rate;
165 for (std::size_t c = 0; c < R; ++c)
166 if (n[c] >= 1) {
167 Cx[c] += Ai;
168 XEacc[c] += Ai / rate;
169 }
170 }
171 }
172 more = next_composition(n);
173 }
174 if (Csum != zero) XRflat[i * R + r] = XRacc / Csum;
175 for (std::size_t c = 0; c < R; ++c)
176 if (Cx[c] != zero) XEflat[(i * R + r) * R + c] = XEacc[c] / Cx[c];
177 }
178 }
179}
180
181template <class T>
182void conway_forward_mva(const Matrix<T>& L, std::size_t M, std::size_t R,
183 const std::vector<int>& N_1, const std::vector<T>& Z,
184 const std::vector<int>& nservers, bool allFCFS,
185 const std::vector<Matrix<T>>& Q1, const Matrix<T>& P_1,
186 const std::vector<T>& PB_1, const Matrix<T>& T_1, Matrix<T>& Q,
187 Matrix<T>& W, std::vector<T>& X, Matrix<T>& P, std::vector<T>& PB) {
188 const T zero = num_traits<T>::from_int(0);
189 const T one = num_traits<T>::from_int(1);
190 std::vector<T> XR, XE;
191 conway_conditional_rates(L, M, R, N_1, nservers, T_1, XR, XE);
192
193 for (std::size_t i = 0; i < M; ++i)
194 for (std::size_t r = 0; r < R; ++r) {
195 if (nservers[i] == 1) {
196 W(i, r) = L(i, r);
197 for (std::size_t c = 0; c < R; ++c)
198 W(i, r) += (allFCFS ? L(i, c) : L(i, r)) * Q1[r + 1](i, c);
199 } else {
200 W(i, r) = L(i, r) + PB_1[i] * XR[i * R + r];
201 for (std::size_t c = 0; c < R; ++c)
202 W(i, r) += XE[(i * R + r) * R + c] * (Q1[r + 1](i, c) - L(i, c) * T_1(c, r));
203 }
204 }
205 for (std::size_t r = 0; r < R; ++r) {
206 T den = Z[r];
207 for (std::size_t i = 0; i < M; ++i) den += W(i, r);
208 if (N_1[r] <= 0) {
209 X[r] = zero;
210 } else {
211 if (den == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_conwayms: zero total residence time");
212 X[r] = num_traits<T>::from_int(N_1[r]) / den;
213 }
214 for (std::size_t i = 0; i < M; ++i) Q(i, r) = X[r] * W(i, r);
215 }
216 // Queue-length marginals. The relations
217 // p_j = A*p_{j-1}/j, pB = A*(pB + p_{ms-1})/ms, p_0 = 1 - pB - sum_j p_j
218 // with A = sum_s X_s*L_is the mean number of busy servers are solved in closed
219 // form rather than iterated. As a Jacobi iteration they amplify by A per sweep,
220 // and since the convergence test watches Q and W but not P the routine returned
221 // marginals whose mass had run to 334 behind the p_0 = max(0,1-...) floor.
222 // conway_estimate hands the same marginals to every reduced population, so the
223 // population corrections that pfqn_linearizerms carries here are all zero.
224 for (std::size_t i = 0; i < M; ++i) {
225 if (nservers[i] <= 1) continue;
226 const std::size_t ms = static_cast<std::size_t>(nservers[i]);
227 T A = zero;
228 for (std::size_t s = 0; s < R; ++s) A += L(i, s) * X[s];
229 for (std::size_t k = 0; k < P.cols(); ++k) P(i, k) = zero;
230 if (!(A < num_traits<T>::from_int(nservers[i]))) {
231 // Saturated: the closed form is singular and its limit is the degenerate
232 // marginal, every server busy with probability one. N = m with Z = 0
233 // reaches it exactly, so this is a legal input.
234 PB[i] = one;
235 continue;
236 }
237 std::vector<T> alpha(ms, zero);
238 alpha[0] = one;
239 T sumAlpha = zero;
240 for (std::size_t j = 1; j < ms; ++j) {
241 alpha[j] = A * alpha[j - 1] / num_traits<T>::from_int(static_cast<int>(j));
242 sumAlpha += alpha[j];
243 }
244 const T alphaB = A * alpha[ms - 1] / (num_traits<T>::from_int(nservers[i]) - A);
245 const T p0 = one / (one + sumAlpha + alphaB);
246 P(i, 0) = p0;
247 for (std::size_t j = 1; j < ms; ++j) P(i, j) = alpha[j] * p0;
248 PB[i] = alphaB * p0;
249 }
250}
251
252template <class T>
253int conway_core(const Matrix<T>& L, std::size_t M, std::size_t R, const std::vector<int>& N_1,
254 const std::vector<T>& Z, const std::vector<int>& nservers, bool allFCFS,
255 Matrix<T>& Q, Matrix<T>& P, std::vector<T>& PB,
256 const std::vector<Matrix<T>>& Delta, double tol, int maxiter, Matrix<T>& W,
257 std::vector<T>& X) {
258 const T zero = num_traits<T>::from_int(0);
259 W = L; // the reference seeds the residence times with the demands
260 Matrix<T> Wlast;
261 bool haveWlast = false;
262 int iter = 1;
263 while (true) {
264 const Matrix<T> Qlast = Q;
265 const Matrix<T> P_1 = P;
266 const std::vector<T> PB_1 = PB;
267 std::vector<Matrix<T>> Q1(R + 1, Matrix<T>(M, R, zero));
268 Matrix<T> T_1(R, R, zero);
269 conway_estimate(M, R, N_1, Q, Delta, W, Q1, T_1);
270 conway_forward_mva(L, M, R, N_1, Z, nservers, allFCFS, Q1, P_1, PB_1, T_1, Q, W, X, P, PB);
271 // W must enter the test: Q alone is satisfied on the FIRST sweep whenever Q
272 // cannot move (M=1 seeds Q at its own fixed point), and the residence times
273 // returned then are still the seed W=L, so T_1=Q/W is unbounded and the
274 // throughput exceeds the station's own service capacity.
275 double e = std::numeric_limits<double>::infinity();
276 if (haveWlast) e = std::max(enorm_diff(Q, Qlast), enorm_diff(W, Wlast));
277 Wlast = W;
278 haveWlast = true;
279 const bool done = e < tol || iter > maxiter;
280 ++iter;
281 if (done) break;
282 }
283 return iter;
284}
285
286} // namespace detail
287
288/**
289 * @brief Conway's multiserver Linearizer for chain-dependent FCFS queues
290 * (Conway 1989, "Fast Approximate Solution of Queueing Networks with
291 * Multi-Server Chain-Dependent FCFS Queues").
292 *
293 * @param L (M x R) service demands
294 * @param N (R) population per class
295 * @param Z (K x R) think times, summed over rows; may be empty
296 * @param nservers (M) number of servers per station, at least one
297 * @param type (M) scheduling discipline; empty means all-FCFS, the MATLAB
298 * default for this routine
299 * @param tol convergence tolerance
300 * @param maxiter total inner-iteration budget
301 * @param QN0 (M x R) warm start; empty for the default N/M
302 */
303template <class T>
304LinearizerResult<T> pfqn_conwayms(const Matrix<T>& L, const std::vector<int>& N,
305 const Matrix<T>& Z, const std::vector<int>& nservers,
306 const std::vector<SchedStrategy>& type, double tol, int maxiter,
307 const Matrix<T>& QN0) {
309 "pfqn_conwayms requires transcendental arithmetic");
310
311 const std::size_t M = L.rows();
312 const std::size_t R = N.size();
313 if (!L.empty() && L.cols() != R)
314 throw InputError(
315 "pfqn_conwayms: demand matrix and population vector disagree on the class count");
316 if (nservers.size() != M)
317 throw InputError("pfqn_conwayms: server-count vector has the wrong station count");
318 for (int c : nservers)
319 if (c < 1) throw InputError("pfqn_conwayms: server count below one");
320 if (!type.empty() && type.size() != M)
321 throw InputError("pfqn_conwayms: scheduling vector has the wrong station count");
322 if (tol <= 0) throw InputError("pfqn_conwayms: tolerance must be positive");
323 for (int v : N)
324 if (v < 0) throw InputError("pfqn_conwayms: negative population");
325
326 const T zero = num_traits<T>::from_int(0);
327 const std::vector<T> Zs = sum_rows(Z, R);
328
330 res.Q = Matrix<T>(M, R, zero);
331 res.U = Matrix<T>(M, R, zero);
332 res.W = Matrix<T>(M, R, zero);
333 res.C.assign(R, zero);
334 res.X.assign(R, zero);
335 res.totiter = 0;
336 if (M == 0) return res;
337
338 // default scheduling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
339 bool allFCFS = true;
340 for (std::size_t i = 0; i < type.size(); ++i)
341 if (type[i] != SchedStrategy::FCFS) allFCFS = false;
342
343 std::size_t cmax = 1;
344 for (int c : nservers) cmax = static_cast<std::size_t>(c) > cmax ? c : cmax;
345
346 // Initial queue lengths: N_1(r)/M, or the supplied warm start.
347 std::vector<Matrix<T>> Q(R + 1, Matrix<T>(M, R, zero));
348 const T mT = num_traits<T>::from_int(static_cast<long>(M));
349 for (std::size_t s = 0; s <= R; ++s) {
350 const std::vector<int> N_1 = oner(N, s);
351 for (std::size_t i = 0; i < M; ++i)
352 for (std::size_t r = 0; r < R; ++r)
353 Q[s](i, r) = QN0.empty() ? num_traits<T>::from_int(N_1[r]) / mT : QN0(i, r);
354 }
355 if (!QN0.empty() && (QN0.rows() != M || QN0.cols() != R))
356 throw InputError("pfqn_conwayms: initial queue lengths have the wrong shape");
357
358 std::vector<Matrix<T>> P(R + 1, Matrix<T>(M, cmax, zero));
359 std::vector<std::vector<T>> PB(R + 1, std::vector<T>(M, zero));
360 detail::linms_init_marginals(M, R, nservers, Q, N, P, PB);
361
362 std::vector<Matrix<T>> Delta(R, Matrix<T>(M, R, zero));
363 Matrix<T> W(M, R, zero);
364 std::vector<T> X(R, zero);
365
366 for (int I = 0; I < 2; ++I) {
367 for (std::size_t s = 0; s <= R; ++s) {
368 const std::vector<int> N_1 = oner(N, s);
369 bool feasible = true;
370 for (int v : N_1)
371 if (v < 0) feasible = false;
372 if (!feasible) continue;
373 res.totiter += detail::conway_core(L, M, R, N_1, Zs, nservers, allFCFS, Q[s], P[s],
374 PB[s], Delta, tol, maxiter - res.totiter, W, X);
375 }
376 // The reference refreshes Delta only for classes with N_s > 2.
377 for (std::size_t i = 0; i < M; ++i)
378 for (std::size_t r = 0; r < R; ++r) {
379 if (N[r] == 0) continue;
380 const T nrT = num_traits<T>::from_int(N[r]);
381 for (std::size_t s = 1; s <= R; ++s) {
382 if (N[s - 1] <= 2) continue;
383 const std::vector<int> Ns = oner(N, s);
384 if (Ns[r] > 0)
385 Delta[r](i, s - 1) =
386 Q[s](i, r) / num_traits<T>::from_int(Ns[r]) - Q[0](i, r) / nrT;
387 else
388 Delta[r](i, s - 1) = -Q[0](i, r) / nrT;
389 }
390 }
391 }
392
393 // The reference passes the full maxiter, not the remaining budget, to the
394 // final Core call; that is reproduced here.
395 res.totiter +=
396 detail::conway_core(L, M, R, N, Zs, nservers, allFCFS, Q[0], P[0], PB[0], Delta, tol,
397 maxiter, W, X);
398 res.Q = Q[0];
399 res.W = W;
400 res.X = X;
401 for (std::size_t i = 0; i < M; ++i)
402 for (std::size_t r = 0; r < R; ++r)
403 res.U(i, r) = nservers[i] == 1
404 ? T(X[r] * L(i, r))
405 : T(X[r] * L(i, r) / num_traits<T>::from_int(nservers[i]));
406 for (std::size_t r = 0; r < R; ++r)
407 res.C[r] = N[r] == 0 ? zero : num_traits<T>::from_int(N[r]) / X[r] - Zs[r];
408 return res;
409}
410
411/** MATLAB defaults: all stations FCFS, tol = 1e-8, maxiter = 1000. */
412template <class T>
413LinearizerResult<T> pfqn_conwayms(const Matrix<T>& L, const std::vector<int>& N,
414 const Matrix<T>& Z, const std::vector<int>& nservers) {
415 return pfqn_conwayms(L, N, Z, nservers, std::vector<SchedStrategy>(), 1e-8, 1000, Matrix<T>());
416}
417
418} // namespace pfqn
419} // namespace line
420
421#endif // LINE_API_PFQN_CONWAYMS_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
Dense matrix and non-owning view.
T num_multinomial(const std::vector< int > &m)
Multinomial coefficient sum(m)!
LinearizerResult< T > pfqn_conwayms(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< int > &nservers, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0)
Conway's multiserver Linearizer for chain-dependent FCFS queues (Conway 1989, "Fast Approximate Solut...
std::vector< T > sum_rows(const Matrix< T > &Z, std::size_t R)
Sum the rows of a think-time matrix into a length-R vector, the sum(Z,1) that every AMVA entry point ...
std::vector< int > oner(const std::vector< int > &N, std::size_t r)
matlab/src/util/oner.m: decrement position r of N, with r given 1-based and r == 0 meaning "leave N a...
void first_composition(std::vector< int > &n, int c)
matlab/src/util/multichoose.m and sprod.m, as an in-place odometer: the compositions of c into R non-...
double enorm_diff(const Matrix< T > &A, const Matrix< T > &B)
Frobenius norm of the difference of two equally shaped matrices, AS A DOUBLE.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:204
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's generalization of Chandy an...
Multiserver Linearizer (Krzesinski's Linearizer as described in Conway 1989, with De Souza e Silva an...
Return value of the Linearizer family, mirroring [Q,U,W,C,X,totiter].
Matrix< T > U
(M x R) utilization
std::vector< T > C
(R) cycle time, N_r/X_r - Z_r
std::vector< T > X
(R) per-class throughput
Matrix< T > W
(M x R) per-station residence time
Matrix< T > Q
(M x R) mean queue length
int totiter
total inner iterations across all Core calls