LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_egflinearizer.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_EGFLINEARIZER_H
6#define LINE_API_PFQN_EGFLINEARIZER_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's
12 * generalization of Chandy and Neuse's Linearizer, with a per-class scaling
13 * exponent alpha_r).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_egflinearizer.m, cross-checked
16 * against jar/src/main/java/jline/api/pfqn/mva/Pfqn_egflinearizer.java. This
17 * is the single implementation behind pfqn_linearizer (alpha == 1) and
18 * pfqn_gflinearizer (alpha uniform), which are thin wrappers.
19 *
20 * The algorithm carries the queue lengths at the full population and at each
21 * of the R reduced populations N - e_s, and a correction
22 *
23 * Delta(i,r,s) = Q(i,r | N - e_s)/(N - e_s)_r^alpha_r - Q(i,r | N)/N_r^alpha_r
24 *
25 * held fixed while an inner MVA fixed point (Core) is iterated, then refreshed
26 * from the new queue lengths. Three refresh rounds are performed, as in Chandy
27 * and Neuse's original; the npasses argument exists so that pfqn_scat can ask
28 * for the single round that defines SCAT.
29 *
30 * Arithmetic: RUNTIME-GATED on the exponent, not compile-time gated.
31 *
32 * Two separate things could put this algorithm outside an exact field, and they
33 * deserve separate answers. The first is N_r^alpha_r, a real power, which is
34 * genuinely not a field operation for a general alpha; detail::num_pow_real
35 * therefore accepts it exactly when alpha is a non-negative integer -- which
36 * covers pfqn_linearizer, where alpha is pinned to 1, and the saturated
37 * Gompertz exponent 2 that pfqn_linearizermx produces for any population past
38 * about thirteen -- and REFUSES by name otherwise.
39 *
40 * The second is the stopping rule: the inner Core loop halts on
41 * enorm(Q_{k+1} - Q_k) < tol, so what comes back is the iterate the stopping
42 * rule selected, not the solution of a finite rational problem. That is a real
43 * caveat but it is not a reason to deny the exact backend: an exact run returns
44 * that iterate WITHOUT rounding error, which is precisely the quantity one
45 * wants when asking how much of a double run's residual is arithmetic and how
46 * much is the fixed point itself. Callers comparing the two backends must
47 * compare like for like -- same tol, same maxiter, same warm start -- because
48 * the two runs stop at different iterates otherwise.
49 *
50 * Scheduling. ForwardMVA in the reference uses the PS residence-time formula
51 * W = D (1 + sum_s Q_1(.,s)) for EVERY discipline; the `type` argument is
52 * accepted and carried but does not enter the recursion. The MATLAB source
53 * documents why: the FCFS correction needs per-visit service times S = D/V,
54 * and only chain-level demands D = V S are available here. That behaviour is
55 * reproduced exactly rather than "improved", so that the port agrees with the
56 * reference on FCFS models.
57 *
58 * One correction relative to the reference. An empty class (N_r == 0) makes
59 * the MATLAB Update_Delta step evaluate Q/N_r^alpha_r = 0/0 and returns an
60 * all-NaN solution. Empty classes are treated here as absent (zero queue
61 * length, zero throughput, zero Delta), which is the same convention the
62 * reference already applies inside pfqn_bs and inside its own Estimate step,
63 * and which the MATLAB comment there calls "required, not cosmetic".
64 */
65
66#include <cmath>
67#include <cstddef>
68#include <vector>
69
73#include "line/num/number.h"
74#include "line/util/error.h"
75#include "line/util/matrix.h"
76
77namespace line {
78namespace pfqn {
79
80/** Return value of the Linearizer family, mirroring [Q,U,W,C,X,totiter]. */
81template <class T>
83 Matrix<T> Q; ///< (M x R) mean queue length
84 Matrix<T> U; ///< (M x R) utilization
85 Matrix<T> W; ///< (M x R) per-station residence time
86 std::vector<T> C; ///< (R) cycle time, N_r/X_r - Z_r
87 std::vector<T> X; ///< (R) per-class throughput
88 int totiter; ///< total inner iterations across all Core calls
89};
90
91namespace detail {
92
93/**
94 * base^e.
95 *
96 * With transcendental arithmetic this is std::pow. Without it -- the exact
97 * rational backend -- a real power is not an operation of the field, but the
98 * SPECIAL CASE that matters here is: the Gompertz exponent alpha is derived
99 * from an integer population and, for any population past about thirteen,
100 * saturates at exactly 2 in double, and pfqn_linearizer pins it at exactly 1.
101 * Both are integers, and an integer power IS a field operation. So the exact
102 * backend computes it by repeated multiplication when the exponent is a
103 * non-negative integer and REFUSES otherwise, rather than silently rounding
104 * the exponent, which would make the "exact" answer exact about the wrong
105 * problem.
106 */
107template <class T>
108T num_pow_real(const T& base, const T& e) {
109 if constexpr (num_traits<T>::has_transcendental) {
110 using std::pow;
111 const T r = pow(base, e);
112 return r;
113 } else {
114 const double ed = num_traits<T>::to_double(e);
115 const double er = std::floor(ed + 0.5);
116 if (er >= 0.0 && er < 1e6 && e == num_traits<T>::from_double(er))
117 return num_pow_int(base, static_cast<unsigned>(er));
118 throw UnsupportedError(
119 "pfqn_egflinearizer: exact arithmetic cannot evaluate a non-integer real power; the "
120 "scaling exponent is " + num_traits<T>::to_string(e) +
121 ", which is not an integer, so this population needs the double or real backend");
122 }
123}
124
125/**
126 * Estimate step: the arrival-instant queue lengths at N_1 - e_s implied by the
127 * queue lengths at N_1 and the frozen Delta. Returns Q1[s](i,r) for s in 1..R.
128 */
129template <class T>
130std::vector<Matrix<T>> egflin_estimate(std::size_t M, std::size_t R, const std::vector<int>& N_1,
131 const Matrix<T>& Q, const std::vector<Matrix<T>>& Delta,
132 const std::vector<T>& alpha) {
133 const T zero = num_traits<T>::from_int(0);
134 std::vector<Matrix<T>> Q1(R + 1, Matrix<T>(M, R, zero));
135 for (std::size_t i = 0; i < M; ++i)
136 for (std::size_t r = 0; r < R; ++r)
137 for (std::size_t s = 1; s <= R; ++s) {
138 const std::vector<int> Ns = oner(N_1, s);
139 // zero-population guard rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
140 if (N_1[r] <= 0 || Ns[r] <= 0) {
141 Q1[s](i, r) = zero;
142 } else {
143 const T na = num_pow_real(num_traits<T>::from_int(Ns[r]), alpha[r]);
144 const T n1a = num_pow_real(num_traits<T>::from_int(N_1[r]), alpha[r]);
145 Q1[s](i, r) = na * (Q(i, r) / n1a + Delta[r](i, s - 1));
146 }
147 }
148 return Q1;
149}
150
151/** Forward MVA step: the PS residence-time formula, as in the reference. */
152template <class T>
153void egflin_forward_mva(const Matrix<T>& L, std::size_t M, std::size_t R,
154 const std::vector<int>& N_1, const std::vector<T>& Z,
155 const std::vector<Matrix<T>>& Q1, Matrix<T>& Q, Matrix<T>& W,
156 std::vector<T>& X) {
157 const T zero = num_traits<T>::from_int(0);
158 const T one = num_traits<T>::from_int(1);
159 for (std::size_t i = 0; i < M; ++i)
160 for (std::size_t r = 0; r < R; ++r) {
161 T acc = one;
162 for (std::size_t s = 0; s < R; ++s) acc += Q1[r + 1](i, s);
163 W(i, r) = L(i, r) * acc;
164 }
165 for (std::size_t r = 0; r < R; ++r) {
166 T den = Z[r];
167 for (std::size_t i = 0; i < M; ++i) den += W(i, r);
168 if (N_1[r] <= 0) {
169 X[r] = zero;
170 } else {
171 if (den == zero) throw NumericError("pfqn_egflinearizer: zero total residence time");
172 X[r] = num_traits<T>::from_int(N_1[r]) / den;
173 }
174 for (std::size_t i = 0; i < M; ++i) Q(i, r) = X[r] * W(i, r);
175 }
176}
177
178/** Core: iterate Estimate / ForwardMVA to the tolerance with Delta frozen. */
179template <class T>
180int egflin_core(const Matrix<T>& L, std::size_t M, std::size_t R, const std::vector<int>& N_1,
181 const std::vector<T>& Z, Matrix<T>& Q, const std::vector<Matrix<T>>& Delta,
182 const std::vector<T>& alpha, double tol, int maxiter, Matrix<T>& W,
183 std::vector<T>& X, bool cntest = false) {
184 int iter = 0;
185 if (cntest) {
186 // Chandy and Neuse (1982), p.129 and appendix: the cutoff is a function of the
187 // population Core is running at, so it is recomputed here rather than once for the
188 // whole Linearizer.
189 tol = pfqn_cntol(N_1);
190 }
191 while (true) {
192 const Matrix<T> Qlast = Q;
193 const std::vector<Matrix<T>> Q1 = egflin_estimate(M, R, N_1, Q, Delta, alpha);
194 egflin_forward_mva(L, M, R, N_1, Z, Q1, Q, W, X);
195 double e;
196 if (cntest) {
197 // max_{i,r} |dQ(i,r)| / N_r over the non-empty classes; an empty class would
198 // divide by zero and it carries no jobs to converge.
199 e = 0.0;
200 for (std::size_t i = 0; i < M; ++i)
201 for (std::size_t r = 0; r < R; ++r) {
202 if (N_1[r] <= 0) continue;
203 const double d = std::fabs(num_traits<T>::to_double(T(Q(i, r) - Qlast(i, r)))) /
204 static_cast<double>(N_1[r]);
205 if (d > e) e = d;
206 }
207 } else {
208 e = enorm_diff(Q, Qlast);
209 }
210 const bool done = e < tol || iter > maxiter;
211 ++iter;
212 if (done) break;
213 }
214 return iter;
215}
216
217} // namespace detail
218
219/**
220 * @brief Extended generalized fixed-point Linearizer (De Souza e Silva and
221 * Muntz's generalization of Chandy and Neuse's Linearizer, with a
222 * per-class scaling exponent alpha_r).
223 *
224 * @param L (M x R) service demands
225 * @param N (R) population per class
226 * @param Z (K x R) think times, summed over rows; may be empty
227 * @param type (M) scheduling discipline; accepted for interface parity,
228 * but the reference recursion is discipline-independent
229 * @param tol convergence tolerance on the Frobenius norm of dQ; NaN selects the
230 * published Linearizer termination test of Chandy and Neuse,
231 * Commun. ACM 25(2), 1982, p.129, under which each Core call stops
232 * when max_{i,r}|dQ(i,r)|/N_r falls below pfqn_cntol evaluated at
233 * the population Core is running at
234 * @param maxiter total inner-iteration budget
235 * @param alpha (R) per-class scaling exponent
236 * @param QN0 (M x R) warm start for the Bard-Schweitzer initialization
237 * @param npasses number of Delta refresh rounds; 3 is the Chandy-Neuse fixed
238 * rule, pfqn_scat passes 1
239 */
240template <class T>
241LinearizerResult<T> pfqn_egflinearizer(const Matrix<T>& L, const std::vector<int>& N,
242 const Matrix<T>& Z,
243 const std::vector<SchedStrategy>& type, double tol,
244 int maxiter, const std::vector<T>& alpha,
245 const Matrix<T>& QN0, int npasses = 3) {
246 // runtime gating rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
247
248 const std::size_t M = L.rows();
249 const std::size_t R = N.size();
250 if (!L.empty() && L.cols() != R)
251 throw InputError(
252 "pfqn_egflinearizer: demand matrix and population vector disagree on the class count");
253 if (alpha.size() != R)
254 throw InputError("pfqn_egflinearizer: alpha has the wrong class count");
255 if (!type.empty() && type.size() != M)
256 throw InputError("pfqn_egflinearizer: scheduling vector has the wrong station count");
257 // The Chandy-Neuse cutoff is population-dependent, so it is recomputed inside each
258 // egflin_core call rather than once here; tol stays NaN so that the pfqn_bs warm-start
259 // inherits the same test. NaN is not caught by the positivity check below (every
260 // comparison against NaN is false), which is exactly what the sentinel needs.
261 const bool cntest = is_cntol(tol);
262 if (!cntest && tol <= 0)
263 throw InputError("pfqn_egflinearizer: tolerance must be positive");
264 for (int v : N)
265 if (v < 0) throw InputError("pfqn_egflinearizer: negative population");
266
267 const T zero = num_traits<T>::from_int(0);
268 const std::vector<T> Zs = sum_rows(Z, R);
269
271 res.Q = Matrix<T>(M, R, zero);
272 res.U = Matrix<T>(M, R, zero);
273 res.W = Matrix<T>(M, R, zero);
274 res.C.assign(R, zero);
275 res.X.assign(R, zero);
276 res.totiter = 0;
277
278 // Delay-only model: every class is served entirely at the delay.
279 bool anyDemand = false;
280 for (std::size_t i = 0; i < M; ++i)
281 for (std::size_t r = 0; r < R; ++r)
282 if (L(i, r) != zero) anyDemand = true;
283 if (M == 0 || !anyDemand) {
284 for (std::size_t r = 0; r < R; ++r) {
285 if (N[r] == 0) continue;
286 if (Zs[r] == zero)
287 throw NumericError(
288 "pfqn_egflinearizer: a class has neither demand nor think time");
289 res.X[r] = num_traits<T>::from_int(N[r]) / Zs[r];
290 for (std::size_t i = 0; i < M; ++i) res.U(i, r) = res.X[r] * L(i, r);
291 }
292 return res;
293 }
294
295 // Initialize every population slice from Bard-Schweitzer.
296 std::vector<Matrix<T>> Q(R + 1, Matrix<T>(M, R, zero));
297 for (std::size_t s = 0; s <= R; ++s) {
298 const std::vector<int> N_1 = oner(N, s);
299 bool feasible = true;
300 for (int v : N_1)
301 if (v < 0) feasible = false;
302 if (!feasible) continue; // r == s with N_r == 0: the slice is unused
303 std::vector<T> Nt(R, zero);
304 for (std::size_t r = 0; r < R; ++r) Nt[r] = num_traits<T>::from_int(N_1[r]);
305 // QN0 Bard-Schweitzer seed rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
306 const AmvaResult<T> b = pfqn_bs(L, Nt, Zs);
307 Q[s] = b.QN;
308 }
309
310 // Delta[r](i,s) with s zero-based over 1..R of the MATLAB third index.
311 std::vector<Matrix<T>> Delta(R, Matrix<T>(M, R, zero));
312
313 Matrix<T> W(M, R, zero);
314 std::vector<T> X(R, zero);
315 for (int I = 0; I < npasses; ++I) {
316 for (std::size_t s = 0; s <= R; ++s) {
317 const std::vector<int> N_1 = oner(N, s);
318 bool feasible = true;
319 for (int v : N_1)
320 if (v < 0) feasible = false;
321 if (!feasible) continue;
322 res.totiter += detail::egflin_core(L, M, R, N_1, Zs, Q[s], Delta, alpha, tol,
323 maxiter - res.totiter, W, X, cntest);
324 }
325 for (std::size_t i = 0; i < M; ++i)
326 for (std::size_t r = 0; r < R; ++r) {
327 if (N[r] == 0) {
328 // Absent class: no jobs at any population, so no correction.
329 for (std::size_t s = 0; s < R; ++s) Delta[r](i, s) = zero;
330 continue;
331 }
332 if (N[r] == 1) {
333 // At N - e_r only class r itself vanishes.
334 Q[r + 1](i, r) = zero;
335 }
336 const T nra = detail::num_pow_real(num_traits<T>::from_int(N[r]), alpha[r]);
337 for (std::size_t s = 1; s <= R; ++s) {
338 const std::vector<int> Ns = oner(N, s);
339 if (Ns[r] > 0) {
340 const T nsa =
341 detail::num_pow_real(num_traits<T>::from_int(Ns[r]), alpha[r]);
342 Delta[r](i, s - 1) = Q[s](i, r) / nsa - Q[0](i, r) / nra;
343 } else {
344 // Chandy-Neuse 0/0 convention rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
345 Delta[r](i, s - 1) = -Q[0](i, r) / nra;
346 }
347 }
348 }
349 }
350
351 res.totiter += detail::egflin_core(L, M, R, N, Zs, Q[0], Delta, alpha, tol,
352 maxiter - res.totiter, W, X, cntest);
353 res.Q = Q[0];
354 res.W = W;
355 res.X = X;
356 for (std::size_t i = 0; i < M; ++i)
357 for (std::size_t r = 0; r < R; ++r) res.U(i, r) = X[r] * L(i, r);
358 for (std::size_t r = 0; r < R; ++r)
359 res.C[r] = N[r] == 0 ? zero : num_traits<T>::from_int(N[r]) / X[r] - Zs[r];
360 return res;
361}
362
363/** MATLAB defaults: tol = 1e-8, maxiter = 1000, no warm start. */
364template <class T>
365LinearizerResult<T> pfqn_egflinearizer(const Matrix<T>& L, const std::vector<int>& N,
366 const Matrix<T>& Z, const std::vector<T>& alpha) {
367 return pfqn_egflinearizer(L, N, Z, std::vector<SchedStrategy>(), 1e-8, 1000, alpha,
368 Matrix<T>());
369}
370
371} // namespace pfqn
372} // namespace line
373
374#endif // LINE_API_PFQN_EGFLINEARIZER_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
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
Dense matrix and non-owning view.
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...
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
LinearizerResult< T > pfqn_egflinearizer(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const std::vector< T > &alpha, const Matrix< T > &QN0, int npasses=3)
Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's generalization of Chandy an...
bool is_cntol(double tol)
True when tol is the sentinel requesting the Chandy-Neuse test.
Definition pfqn_cntol.h:72
double pfqn_cntol(const std::vector< T > &N)
Termination cutoff at the given population vector.
Definition pfqn_cntol.h:58
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.
Bard-Schweitzer approximate MVA.
Chandy-Neuse population-scaled termination cutoff for approximate MVA.
Matrix< T > QN
(M x R) queue length
Definition pfqn_bs.h:50
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