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.
149 T rate = zero;
150 for (std::size_t c = 0; c < R; ++c) {
151 if (n[c] == 0) continue;
152 if (L(i, c) == zero)
153 throw NumericError(
154 "pfqn_conwayms: a chain occupies a server at a station where it "
155 "has zero demand, so its service rate is unbounded");
156 rate += num_traits<T>::from_int(n[c]) / L(i, c);
157 }
158 if (rate != zero) {
159 Csum += Ai;
160 XRacc += Ai / rate;
161 for (std::size_t c = 0; c < R; ++c)
162 if (n[c] >= 1) {
163 Cx[c] += Ai;
164 XEacc[c] += Ai / rate;
165 }
166 }
167 }
168 more = next_composition(n);
169 }
170 if (Csum != zero) XRflat[i * R + r] = XRacc / Csum;
171 for (std::size_t c = 0; c < R; ++c)
172 if (Cx[c] != zero) XEflat[(i * R + r) * R + c] = XEacc[c] / Cx[c];
173 }
174 }
175}
176
177template <class T>
178void conway_forward_mva(const Matrix<T>& L, std::size_t M, std::size_t R,
179 const std::vector<int>& N_1, const std::vector<T>& Z,
180 const std::vector<int>& nservers, bool allFCFS,
181 const std::vector<Matrix<T>>& Q1, const Matrix<T>& P_1,
182 const std::vector<T>& PB_1, const Matrix<T>& T_1, Matrix<T>& Q,
183 Matrix<T>& W, std::vector<T>& X, Matrix<T>& P, std::vector<T>& PB) {
184 const T zero = num_traits<T>::from_int(0);
185 const T one = num_traits<T>::from_int(1);
186 std::vector<T> XR, XE;
187 conway_conditional_rates(L, M, R, N_1, nservers, T_1, XR, XE);
188
189 for (std::size_t i = 0; i < M; ++i)
190 for (std::size_t r = 0; r < R; ++r) {
191 if (nservers[i] == 1) {
192 W(i, r) = L(i, r);
193 for (std::size_t c = 0; c < R; ++c)
194 W(i, r) += (allFCFS ? L(i, c) : L(i, r)) * Q1[r + 1](i, c);
195 } else {
196 W(i, r) = L(i, r) + PB_1[i] * XR[i * R + r];
197 for (std::size_t c = 0; c < R; ++c)
198 W(i, r) += XE[(i * R + r) * R + c] * (Q1[r + 1](i, c) - L(i, c) * T_1(c, r));
199 }
200 }
201 for (std::size_t r = 0; r < R; ++r) {
202 T den = Z[r];
203 for (std::size_t i = 0; i < M; ++i) den += W(i, r);
204 if (N_1[r] <= 0) {
205 X[r] = zero;
206 } else {
207 if (den == zero) throw NumericError("pfqn_conwayms: zero total residence time");
208 X[r] = num_traits<T>::from_int(N_1[r]) / den;
209 }
210 for (std::size_t i = 0; i < M; ++i) Q(i, r) = X[r] * W(i, r);
211 }
212 // Queue-length marginals. The relations
213 // p_j = A*p_{j-1}/j, pB = A*(pB + p_{ms-1})/ms, p_0 = 1 - pB - sum_j p_j
214 // with A = sum_s X_s*L_is the mean number of busy servers are solved in closed
215 // form rather than iterated. As a Jacobi iteration they amplify by A per sweep,
216 // and since the convergence test watches Q and W but not P the routine returned
217 // marginals whose mass had run to 334 behind the p_0 = max(0,1-...) floor.
218 // conway_estimate hands the same marginals to every reduced population, so the
219 // population corrections that pfqn_linearizerms carries here are all zero.
220 for (std::size_t i = 0; i < M; ++i) {
221 if (nservers[i] <= 1) continue;
222 const std::size_t ms = static_cast<std::size_t>(nservers[i]);
223 T A = zero;
224 for (std::size_t s = 0; s < R; ++s) A += L(i, s) * X[s];
225 for (std::size_t k = 0; k < P.cols(); ++k) P(i, k) = zero;
226 if (!(A < num_traits<T>::from_int(nservers[i]))) {
227 // Saturated: the closed form is singular and its limit is the degenerate
228 // marginal, every server busy with probability one. N = m with Z = 0
229 // reaches it exactly, so this is a legal input.
230 PB[i] = one;
231 continue;
232 }
233 std::vector<T> alpha(ms, zero);
234 alpha[0] = one;
235 T sumAlpha = zero;
236 for (std::size_t j = 1; j < ms; ++j) {
237 alpha[j] = A * alpha[j - 1] / num_traits<T>::from_int(static_cast<int>(j));
238 sumAlpha += alpha[j];
239 }
240 const T alphaB = A * alpha[ms - 1] / (num_traits<T>::from_int(nservers[i]) - A);
241 const T p0 = one / (one + sumAlpha + alphaB);
242 P(i, 0) = p0;
243 for (std::size_t j = 1; j < ms; ++j) P(i, j) = alpha[j] * p0;
244 PB[i] = alphaB * p0;
245 }
246}
247
248template <class T>
249int conway_core(const Matrix<T>& L, std::size_t M, std::size_t R, const std::vector<int>& N_1,
250 const std::vector<T>& Z, const std::vector<int>& nservers, bool allFCFS,
251 Matrix<T>& Q, Matrix<T>& P, std::vector<T>& PB,
252 const std::vector<Matrix<T>>& Delta, double tol, int maxiter, Matrix<T>& W,
253 std::vector<T>& X) {
254 const T zero = num_traits<T>::from_int(0);
255 W = L; // the reference seeds the residence times with the demands
256 Matrix<T> Wlast;
257 bool haveWlast = false;
258 int iter = 1;
259 while (true) {
260 const Matrix<T> Qlast = Q;
261 const Matrix<T> P_1 = P;
262 const std::vector<T> PB_1 = PB;
263 std::vector<Matrix<T>> Q1(R + 1, Matrix<T>(M, R, zero));
264 Matrix<T> T_1(R, R, zero);
265 conway_estimate(M, R, N_1, Q, Delta, W, Q1, T_1);
266 conway_forward_mva(L, M, R, N_1, Z, nservers, allFCFS, Q1, P_1, PB_1, T_1, Q, W, X, P, PB);
267 // W must enter the test: Q alone is satisfied on the FIRST sweep whenever Q
268 // cannot move (M=1 seeds Q at its own fixed point), and the residence times
269 // returned then are still the seed W=L, so T_1=Q/W is unbounded and the
270 // throughput exceeds the station's own service capacity.
271 double e = std::numeric_limits<double>::infinity();
272 if (haveWlast) e = std::max(enorm_diff(Q, Qlast), enorm_diff(W, Wlast));
273 Wlast = W;
274 haveWlast = true;
275 const bool done = e < tol || iter > maxiter;
276 ++iter;
277 if (done) break;
278 }
279 return iter;
280}
281
282} // namespace detail
283
284/**
285 * @brief Conway's multiserver Linearizer for chain-dependent FCFS queues
286 * (Conway 1989, "Fast Approximate Solution of Queueing Networks with
287 * Multi-Server Chain-Dependent FCFS Queues").
288 *
289 * @param L (M x R) service demands
290 * @param N (R) population per class
291 * @param Z (K x R) think times, summed over rows; may be empty
292 * @param nservers (M) number of servers per station, at least one
293 * @param type (M) scheduling discipline; empty means all-FCFS, the MATLAB
294 * default for this routine
295 * @param tol convergence tolerance
296 * @param maxiter total inner-iteration budget
297 * @param QN0 (M x R) warm start; empty for the default N/M
298 */
299template <class T>
300LinearizerResult<T> pfqn_conwayms(const Matrix<T>& L, const std::vector<int>& N,
301 const Matrix<T>& Z, const std::vector<int>& nservers,
302 const std::vector<SchedStrategy>& type, double tol, int maxiter,
303 const Matrix<T>& QN0) {
305 "pfqn_conwayms requires transcendental arithmetic");
306
307 const std::size_t M = L.rows();
308 const std::size_t R = N.size();
309 if (!L.empty() && L.cols() != R)
310 throw InputError(
311 "pfqn_conwayms: demand matrix and population vector disagree on the class count");
312 if (nservers.size() != M)
313 throw InputError("pfqn_conwayms: server-count vector has the wrong station count");
314 for (int c : nservers)
315 if (c < 1) throw InputError("pfqn_conwayms: server count below one");
316 if (!type.empty() && type.size() != M)
317 throw InputError("pfqn_conwayms: scheduling vector has the wrong station count");
318 if (tol <= 0) throw InputError("pfqn_conwayms: tolerance must be positive");
319 for (int v : N)
320 if (v < 0) throw InputError("pfqn_conwayms: negative population");
321
322 const T zero = num_traits<T>::from_int(0);
323 const std::vector<T> Zs = sum_rows(Z, R);
324
326 res.Q = Matrix<T>(M, R, zero);
327 res.U = Matrix<T>(M, R, zero);
328 res.W = Matrix<T>(M, R, zero);
329 res.C.assign(R, zero);
330 res.X.assign(R, zero);
331 res.totiter = 0;
332 if (M == 0) return res;
333
334 // default scheduling rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
335 bool allFCFS = true;
336 for (std::size_t i = 0; i < type.size(); ++i)
337 if (type[i] != SchedStrategy::FCFS) allFCFS = false;
338
339 std::size_t cmax = 1;
340 for (int c : nservers) cmax = static_cast<std::size_t>(c) > cmax ? c : cmax;
341
342 // Initial queue lengths: N_1(r)/M, or the supplied warm start.
343 std::vector<Matrix<T>> Q(R + 1, Matrix<T>(M, R, zero));
344 const T mT = num_traits<T>::from_int(static_cast<long>(M));
345 for (std::size_t s = 0; s <= R; ++s) {
346 const std::vector<int> N_1 = oner(N, s);
347 for (std::size_t i = 0; i < M; ++i)
348 for (std::size_t r = 0; r < R; ++r)
349 Q[s](i, r) = QN0.empty() ? num_traits<T>::from_int(N_1[r]) / mT : QN0(i, r);
350 }
351 if (!QN0.empty() && (QN0.rows() != M || QN0.cols() != R))
352 throw InputError("pfqn_conwayms: initial queue lengths have the wrong shape");
353
354 std::vector<Matrix<T>> P(R + 1, Matrix<T>(M, cmax, zero));
355 std::vector<std::vector<T>> PB(R + 1, std::vector<T>(M, zero));
356 detail::linms_init_marginals(M, R, nservers, Q, N, P, PB);
357
358 std::vector<Matrix<T>> Delta(R, Matrix<T>(M, R, zero));
359 Matrix<T> W(M, R, zero);
360 std::vector<T> X(R, zero);
361
362 for (int I = 0; I < 2; ++I) {
363 for (std::size_t s = 0; s <= R; ++s) {
364 const std::vector<int> N_1 = oner(N, s);
365 bool feasible = true;
366 for (int v : N_1)
367 if (v < 0) feasible = false;
368 if (!feasible) continue;
369 res.totiter += detail::conway_core(L, M, R, N_1, Zs, nservers, allFCFS, Q[s], P[s],
370 PB[s], Delta, tol, maxiter - res.totiter, W, X);
371 }
372 // The reference refreshes Delta only for classes with N_s > 2.
373 for (std::size_t i = 0; i < M; ++i)
374 for (std::size_t r = 0; r < R; ++r) {
375 if (N[r] == 0) continue;
376 const T nrT = num_traits<T>::from_int(N[r]);
377 for (std::size_t s = 1; s <= R; ++s) {
378 if (N[s - 1] <= 2) continue;
379 const std::vector<int> Ns = oner(N, s);
380 if (Ns[r] > 0)
381 Delta[r](i, s - 1) =
382 Q[s](i, r) / num_traits<T>::from_int(Ns[r]) - Q[0](i, r) / nrT;
383 else
384 Delta[r](i, s - 1) = -Q[0](i, r) / nrT;
385 }
386 }
387 }
388
389 // The reference passes the full maxiter, not the remaining budget, to the
390 // final Core call; that is reproduced here.
391 res.totiter +=
392 detail::conway_core(L, M, R, N, Zs, nservers, allFCFS, Q[0], P[0], PB[0], Delta, tol,
393 maxiter, W, X);
394 res.Q = Q[0];
395 res.W = W;
396 res.X = X;
397 for (std::size_t i = 0; i < M; ++i)
398 for (std::size_t r = 0; r < R; ++r)
399 res.U(i, r) = nservers[i] == 1
400 ? T(X[r] * L(i, r))
401 : T(X[r] * L(i, r) / num_traits<T>::from_int(nservers[i]));
402 for (std::size_t r = 0; r < R; ++r)
403 res.C[r] = N[r] == 0 ? zero : num_traits<T>::from_int(N[r]) / X[r] - Zs[r];
404 return res;
405}
406
407/** MATLAB defaults: all stations FCFS, tol = 1e-8, maxiter = 1000. */
408template <class T>
409LinearizerResult<T> pfqn_conwayms(const Matrix<T>& L, const std::vector<int>& N,
410 const Matrix<T>& Z, const std::vector<int>& nservers) {
411 return pfqn_conwayms(L, N, Z, nservers, std::vector<SchedStrategy>(), 1e-8, 1000, Matrix<T>());
412}
413
414} // namespace pfqn
415} // namespace line
416
417#endif // LINE_API_PFQN_CONWAYMS_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
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.
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
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