LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_linearizerms.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_LINEARIZERMS_H
6#define LINE_API_PFQN_LINEARIZERMS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Multiserver Linearizer (Krzesinski's Linearizer as described in Conway 1989,
12 * with De Souza e Silva and Muntz's presentation of the marginal-probability
13 * recursions).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_linearizerms.m, cross-checked
16 * against jar/src/main/java/jline/api/pfqn/mva/Pfqn_linearizerms.java.
17 *
18 * Beyond the single-server Linearizer this carries, per station, the marginal
19 * probabilities P(i,j) of finding j busy servers and the blocking probability
20 * PB(i), estimated at the reduced populations by freezing them, and adds the
21 * multiserver waiting term sum_{j<c-1} (c-1-j) P(i,j) to the residence time.
22 *
23 * Arithmetic: TRANSCENDENTAL-GATED. The inner Core loop stops on
24 * norm(Q_{k+1} - Q_k) < tol, so the returned value depends on the stopping
25 * rule and is not the solution of a finite rational problem. No transcendental
26 * function is called.
27 *
28 * Convergence norm. MATLAB tests norm(Q - Qlast), the SPECTRAL norm of the
29 * difference; this port tests the Frobenius norm, which dominates it. The
30 * fixed point is identical and the test is if anything stricter, so no
31 * solution accepted here would be rejected by the reference; the alternative
32 * would be a singular value decomposition inside the inner loop of an
33 * approximation, at every arithmetic, for no change in the answer.
34 *
35 * MATLAB-vs-JAR disagreement, resolved in MATLAB's favour. Both references
36 * select the FCFS residence-time formula from the WHOLE type vector rather
37 * than from the current station: MATLAB writes `if type == SchedStrategy.FCFS`
38 * on an (M x 1) vector, which MATLAB evaluates as all(type == FCFS). The JAR
39 * open-codes that test as `flag = true unless ANY station is FCFS` and then
40 * takes the FCFS arm when `flag` holds -- exactly the opposite selection. This
41 * port follows MATLAB: the FCFS arm is taken when every station is FCFS. Note
42 * that the per-station form (type[i] == FCFS) would be the defensible reading
43 * of the algorithm, but neither reference implements it and changing it here
44 * would silently disagree with both.
45 *
46 * One correction relative to MATLAB. The Update_Delta and Estimate steps
47 * divide by (N - e_s)_r and by N_r without guarding either against zero, so a
48 * class with N_r == 1 produces Q/0 = Inf and then 0*Inf = NaN across the whole
49 * solution, and an empty class produces 0/0. The guards of the single-server
50 * pfqn_egflinearizer.m -- the Chandy and Neuse (1982) eq. (10) 0/0 convention
51 * for (N - e_s)_r == 0, and an absent class contributing nothing -- are
52 * applied here as well, since the two files implement the same correction and
53 * only one of them carries the guard.
54 */
55
56#include <cstddef>
57#include <vector>
58
62#include "line/num/number.h"
63#include "line/util/error.h"
64#include "line/util/matrix.h"
65
66namespace line {
67namespace pfqn {
68
69namespace detail {
70
71/**
72 * ForwardMVA of the multiserver Linearizer. P and PB are read from the frozen
73 * copies P_1, PB_1 (in the reference these are (M x c x 1+R) and (M x 1+R)
74 * arrays whose entries are constant in the third index, so a plain copy of the
75 * current P, PB is an exact stand-in) and overwritten with the new estimates.
76 */
77template <class T>
78void linms_forward_mva(const Matrix<T>& L, std::size_t M, std::size_t R,
79 const std::vector<int>& N_1, const std::vector<T>& Z,
80 const std::vector<int>& nservers, bool allFCFS,
81 const std::vector<Matrix<T>>& Q1, const std::vector<Matrix<T>>& P1,
82 const std::vector<std::vector<T>>& PB1, Matrix<T>& Q, Matrix<T>& W,
83 std::vector<T>& X, Matrix<T>& P, std::vector<T>& PB) {
84 const T zero = num_traits<T>::from_int(0);
85 const T one = num_traits<T>::from_int(1);
86 for (std::size_t i = 0; i < M; ++i) {
87 const T c = num_traits<T>::from_int(nservers[i]);
88 for (std::size_t r = 0; r < R; ++r) {
89 W(i, r) = L(i, r) / c;
90 // Zero demand: this class does not visit the station.
91 if (L(i, r) == zero) continue;
92 for (std::size_t s = 0; s < R; ++s)
93 W(i, r) += (allFCFS ? L(i, s) : L(i, r)) / c * Q1[r + 1](i, s);
94 // Partially-idle-server correction. It compensates the 1/m scaling of
95 // the ARRIVING JOB'S OWN service, so it carries L(i,r)/m and no sum
96 // over the other classes: at N = e_r no queueing is possible, and the
97 // bracket must collapse to W = L(i,r) exactly. Summing L(i,s) over
98 // every class instead (and omitting the /m) inflated it by a factor
99 // that grew with both R and m -- 1.5x the demand at m=2, 3x at m=5.
100 if (nservers[i] > 1)
101 for (int j = 0; j <= nservers[i] - 2; ++j) {
102 const T wgt = num_traits<T>::from_int(nservers[i] - 1 - j);
103 W(i, r) += L(i, r) / c * wgt * P1[r + 1](i, static_cast<std::size_t>(j));
104 }
105 }
106 }
107 for (std::size_t r = 0; r < R; ++r) {
108 T den = Z[r];
109 for (std::size_t i = 0; i < M; ++i) den += W(i, r);
110 if (N_1[r] <= 0) {
111 X[r] = zero;
112 } else {
113 if (den == zero) throw NumericError("pfqn_linearizerms: zero total residence time");
114 X[r] = num_traits<T>::from_int(N_1[r]) / den;
115 }
116 for (std::size_t i = 0; i < M; ++i) Q(i, r) = X[r] * W(i, r);
117 }
118 // Queue-length marginals. The relations
119 // p_j = (A p_{j-1} + d_{j-1})/j, pB = (A (pB + p_{m-1}) + dB)/m,
120 // p_0 = 1 - pB - sum_j p_j
121 // with A = sum_s X_s L_is the mean number of busy servers, are solved in
122 // CLOSED FORM rather than iterated: as a Jacobi sweep they amplify by A per
123 // pass and diverge once A approaches m, which is what made the corrected
124 // residence times blow up (marginals reaching -13, throughput 64x too large)
125 // on near-saturated models. Same fixed point wherever the iteration
126 // converged, and unconditionally stable for any A < m.
127 for (std::size_t i = 0; i < M; ++i) {
128 const int ms = nservers[i];
129 if (ms <= 1) continue;
130 const std::size_t msz = static_cast<std::size_t>(ms);
131 T A = zero, dB = zero;
132 std::vector<T> d(msz, zero);
133 for (std::size_t s = 0; s < R; ++s) {
134 const T a_s = L(i, s) * X[s];
135 A += a_s;
136 for (std::size_t j = 0; j < msz; ++j)
137 d[j] += a_s * (P1[s + 1](i, j) - P1[0](i, j));
138 dB += a_s * (PB1[s + 1][i] - PB1[0][i]);
139 }
140 const T msT = num_traits<T>::from_int(ms);
141 if (!(A < msT))
142 throw NumericError(
143 "pfqn_linearizerms: the station offers as many busy servers as it has, so the "
144 "model is saturated and its queue-length marginals do not exist");
145 std::vector<T> alpha(msz, zero), beta(msz, zero);
146 alpha[0] = one;
147 for (std::size_t j = 1; j < msz; ++j) {
148 const T jT = num_traits<T>::from_int(static_cast<int>(j));
149 alpha[j] = A * alpha[j - 1] / jT;
150 beta[j] = (A * beta[j - 1] + d[j - 1]) / jT;
151 }
152 const T alphaB = A * alpha[msz - 1] / (msT - A);
153 const T betaB = (A * beta[msz - 1] + dB + d[msz - 1]) / (msT - A);
154 T num = one - betaB, den = one + alphaB;
155 for (std::size_t j = 1; j < msz; ++j) {
156 num -= beta[j];
157 den += alpha[j];
158 }
159 for (std::size_t k = 0; k < P.cols(); ++k) P(i, k) = zero;
160 P(i, 0) = num / den;
161 for (std::size_t j = 1; j < msz; ++j) P(i, j) = alpha[j] * P(i, 0) + beta[j];
162 PB[i] = alphaB * P(i, 0) + betaB;
163 }
164}
165
166/**
167 * Estimate step of the multiserver Linearizer.
168 *
169 * It returns the queue lengths AND the marginals at each reduced population.
170 * The marginals used to be a plain copy of the current P, PB at population N,
171 * on the reading that the reference's (M x c x 1+R) arrays are constant in the
172 * third index. They are not, once DeltaP/DeltaPB exist: mixing queue lengths
173 * reduced to N - e_s with marginals still at N breaks the identity
174 * Q + sum_{j<=m-2} (m-1-j) p_j >= m-1 that guarantees W >= D, which is exactly
175 * how the residence time came out below the mean service time.
176 */
177template <class T>
178void linms_estimate(std::size_t M, std::size_t R, const std::vector<int>& N_1, const Matrix<T>& Q,
179 const Matrix<T>& P, const std::vector<T>& PB,
180 const std::vector<Matrix<T>>& Delta, const std::vector<Matrix<T>>& DeltaP,
181 const Matrix<T>& DeltaPB, const std::vector<int>& nservers,
182 std::vector<Matrix<T>>& Q1, std::vector<Matrix<T>>& P1,
183 std::vector<std::vector<T>>& PB1) {
184 const T zero = num_traits<T>::from_int(0);
185 Q1.assign(R + 1, Matrix<T>(M, R, zero));
186 P1.assign(R + 1, Matrix<T>(M, P.cols(), zero));
187 PB1.assign(R + 1, std::vector<T>(M, zero));
188 for (std::size_t i = 0; i < M; ++i) {
189 if (nservers[i] > 1) {
190 for (std::size_t j = 0; j < P.cols(); ++j) {
191 P1[0](i, j) = P(i, j);
192 for (std::size_t s = 1; s <= R; ++s)
193 P1[s](i, j) = P(i, j) + DeltaP[s - 1](i, j);
194 }
195 PB1[0][i] = PB[i];
196 for (std::size_t s = 1; s <= R; ++s) PB1[s][i] = PB[i] + DeltaPB(i, s - 1);
197 }
198 for (std::size_t r = 0; r < R; ++r)
199 for (std::size_t s = 1; s <= R; ++s) {
200 const std::vector<int> Ns = oner(N_1, s);
201 if (N_1[r] <= 0 || Ns[r] <= 0) {
202 Q1[s](i, r) = zero;
203 } else {
204 Q1[s](i, r) = num_traits<T>::from_int(Ns[r]) *
205 (Q(i, r) / num_traits<T>::from_int(N_1[r]) + Delta[r](i, s - 1));
206 }
207 }
208 }
209}
210
211template <class T>
212int linms_core(const Matrix<T>& L, std::size_t M, std::size_t R, const std::vector<int>& N_1,
213 const std::vector<T>& Z, const std::vector<int>& nservers, bool allFCFS,
214 Matrix<T>& Q, Matrix<T>& P, std::vector<T>& PB,
215 const std::vector<Matrix<T>>& Delta, const std::vector<Matrix<T>>& DeltaP,
216 const Matrix<T>& DeltaPB, double tol, int maxiter, Matrix<T>& W,
217 std::vector<T>& X) {
218 int iter = 0;
219 std::vector<Matrix<T>> Q1, P1;
220 std::vector<std::vector<T>> PB1;
221 while (true) {
222 ++iter;
223 const Matrix<T> Qlast = Q;
224 linms_estimate(M, R, N_1, Q, P, PB, Delta, DeltaP, DeltaPB, nservers, Q1, P1, PB1);
225 linms_forward_mva(L, M, R, N_1, Z, nservers, allFCFS, Q1, P1, PB1, Q, W, X, P, PB);
226 const double e = enorm_diff(Q, Qlast);
227 if (e < tol || iter > maxiter) break;
228 }
229 return iter;
230}
231
232/** Initial marginal probabilities from the aggregate queue lengths. */
233template <class T>
234void linms_init_marginals(std::size_t M, std::size_t R, const std::vector<int>& nservers,
235 const std::vector<Matrix<T>>& Q, const std::vector<int>& N,
236 std::vector<Matrix<T>>& P, std::vector<std::vector<T>>& PB) {
237 const T zero = num_traits<T>::from_int(0);
238 const T one = num_traits<T>::from_int(1);
239 for (std::size_t s = 0; s <= R; ++s) {
240 const std::vector<int> N_1 = oner(N, s);
241 long pop = 0;
242 for (int v : N_1) pop += v;
243 if (pop <= 0) {
244 // Empty network: the station is idle with probability one. Skipping
245 // left p_0 at zero, i.e. a marginal that is not a distribution.
246 for (std::size_t i = 0; i < M; ++i) {
247 if (nservers[i] <= 1) continue;
248 for (std::size_t k = 0; k < P[s].cols(); ++k) P[s](i, k) = zero;
249 P[s](i, 0) = one;
250 PB[s][i] = zero;
251 }
252 continue;
253 }
254 const T popT = num_traits<T>::from_int(pop);
255 const T pop1T = num_traits<T>::from_int(pop + 1);
256 for (std::size_t i = 0; i < M; ++i) {
257 if (nservers[i] <= 1) continue;
258 T qsum = zero;
259 for (std::size_t r = 0; r < R; ++r) qsum += Q[s](i, r);
260 const T two = num_traits<T>::from_int(2);
261 for (int j = 1; j <= nservers[i] - 1; ++j)
262 P[s](i, static_cast<std::size_t>(j)) = two * qsum / (popT * pop1T);
263 // Fewer jobs than servers: they cannot all be busy, so pB is 0 rather
264 // than a division by the vanishing slack. MATLAB guards it as
265 // `pop > nservers(i)-1`; throwing here refused models the reference
266 // solves, and every population below the server count is one of them.
267 if (pop > nservers[i] - 1) {
268 const T slack = num_traits<T>::from_int(pop + 1 - nservers[i]);
269 PB[s][i] = two * qsum / slack / (popT * pop1T);
270 } else {
271 PB[s][i] = zero;
272 }
273 T p0 = one - PB[s][i];
274 for (int j = 1; j <= nservers[i] - 1; ++j) p0 -= P[s](i, static_cast<std::size_t>(j));
275 P[s](i, 0) = p0;
276 }
277 }
278}
279
280} // namespace detail
281
282/**
283 * @brief Multiserver Linearizer (Krzesinski's Linearizer as described in
284 * Conway 1989, with De Souza e Silva and Muntz's presentation of the
285 * marginal-probability recursions).
286 *
287 * @param L (M x R) service demands
288 * @param N (R) population per class
289 * @param Z (K x R) think times, summed over rows; may be empty
290 * @param nservers (M) number of servers per station, at least one
291 * @param type (M) scheduling discipline; the FCFS arm is taken only if
292 * every station is FCFS, as in MATLAB
293 * @param tol convergence tolerance
294 * @param maxiter total inner-iteration budget
295 * @param QN0 (M x R) warm start for the Bard-Schweitzer initialization
296 */
297template <class T>
298LinearizerResult<T> pfqn_linearizerms(const Matrix<T>& L, const std::vector<int>& N,
299 const Matrix<T>& Z, const std::vector<int>& nservers,
300 const std::vector<SchedStrategy>& type, double tol,
301 int maxiter, const Matrix<T>& QN0) {
302 // field-arithmetic rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
303
304 const std::size_t M = L.rows();
305 const std::size_t R = N.size();
306 if (!L.empty() && L.cols() != R)
307 throw InputError(
308 "pfqn_linearizerms: demand matrix and population vector disagree on the class count");
309 if (nservers.size() != M)
310 throw InputError("pfqn_linearizerms: server-count vector has the wrong station count");
311 for (int c : nservers)
312 if (c < 1) throw InputError("pfqn_linearizerms: server count below one");
313 if (!type.empty() && type.size() != M)
314 throw InputError("pfqn_linearizerms: scheduling vector has the wrong station count");
315 if (tol <= 0) throw InputError("pfqn_linearizerms: tolerance must be positive");
316 for (int v : N)
317 if (v < 0) throw InputError("pfqn_linearizerms: negative population");
318
319 const T zero = num_traits<T>::from_int(0);
320 const std::vector<T> Zs = sum_rows(Z, R);
321
323 res.Q = Matrix<T>(M, R, zero);
324 res.U = Matrix<T>(M, R, zero);
325 res.W = Matrix<T>(M, R, zero);
326 res.C.assign(R, zero);
327 res.X.assign(R, zero);
328 res.totiter = 0;
329 if (M == 0) return res;
330
331 bool allFCFS = !type.empty();
332 for (std::size_t i = 0; i < type.size(); ++i)
333 if (type[i] != SchedStrategy::FCFS) allFCFS = false;
334
335 std::size_t cmax = 1;
336 for (int c : nservers) cmax = static_cast<std::size_t>(c) > cmax ? c : cmax;
337
338 Matrix<T> Zm(1, R, zero);
339 for (std::size_t r = 0; r < R; ++r) Zm(0, r) = Zs[r];
340
341 std::vector<Matrix<T>> Q(R + 1, Matrix<T>(M, R, zero));
342 for (std::size_t s = 0; s <= R; ++s) {
343 const std::vector<int> N_1 = oner(N, s);
344 bool feasible = true;
345 for (int v : N_1)
346 if (v < 0) feasible = false;
347 if (!feasible) continue;
348 std::vector<T> Nt(R, zero);
349 for (std::size_t r = 0; r < R; ++r) Nt[r] = num_traits<T>::from_int(N_1[r]);
350 // pfqn_bs three-argument seed rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
351 const AmvaResult<T> b = pfqn_bs(L, Nt, Zs);
352 Q[s] = b.QN;
353 }
354
355 std::vector<Matrix<T>> P(R + 1, Matrix<T>(M, cmax, zero));
356 std::vector<std::vector<T>> PB(R + 1, std::vector<T>(M, zero));
357 detail::linms_init_marginals(M, R, nservers, Q, N, P, PB);
358
359 std::vector<Matrix<T>> Delta(R, Matrix<T>(M, R, zero));
360 // Linearizer corrections for the MARGINALS. Without them the marginals stay
361 // at population N while the queue lengths are reduced to N - e_s, which
362 // breaks Q + sum_j (m-1-j) p_j >= m-1 and lets W fall below the mean service
363 // time. Probabilities do not scale with the population, so the analogue of
364 // Delta is a plain difference rather than a per-job rate.
365 std::vector<Matrix<T>> DeltaP(R, Matrix<T>(M, cmax, zero));
366 Matrix<T> DeltaPB(M, R, zero);
367 Matrix<T> W(M, R, zero);
368 std::vector<T> X(R, zero);
369
370 for (int I = 0; I < 2; ++I) {
371 for (std::size_t s = 0; s <= R; ++s) {
372 const std::vector<int> N_1 = oner(N, s);
373 bool feasible = true;
374 for (int v : N_1)
375 if (v < 0) feasible = false;
376 if (!feasible) continue;
377 res.totiter +=
378 detail::linms_core(L, M, R, N_1, Zs, nservers, allFCFS, Q[s], P[s], PB[s], Delta,
379 DeltaP, DeltaPB, tol, maxiter - res.totiter, W, X);
380 }
381 for (std::size_t i = 0; i < M; ++i)
382 for (std::size_t r = 0; r < R; ++r) {
383 if (N[r] == 0) {
384 for (std::size_t s = 0; s < R; ++s) Delta[r](i, s) = zero;
385 continue;
386 }
387 const T nrT = num_traits<T>::from_int(N[r]);
388 for (std::size_t s = 1; s <= R; ++s) {
389 const std::vector<int> Ns = oner(N, s);
390 if (Ns[r] > 0) {
391 Delta[r](i, s - 1) =
392 Q[s](i, r) / num_traits<T>::from_int(Ns[r]) - Q[0](i, r) / nrT;
393 } else {
394 Delta[r](i, s - 1) = -Q[0](i, r) / nrT;
395 }
396 }
397 }
398 // Update_DeltaP: the marginals' counterpart of Update_Delta.
399 for (std::size_t i = 0; i < M; ++i) {
400 if (nservers[i] <= 1) continue;
401 for (std::size_t s = 1; s <= R; ++s) {
402 for (std::size_t j = 0; j < static_cast<std::size_t>(nservers[i]); ++j)
403 DeltaP[s - 1](i, j) = P[s](i, j) - P[0](i, j);
404 DeltaPB(i, s - 1) = PB[s][i] - PB[0][i];
405 }
406 }
407 }
408
409 res.totiter += detail::linms_core(L, M, R, N, Zs, nservers, allFCFS, Q[0], P[0], PB[0], Delta,
410 DeltaP, DeltaPB, tol, maxiter - res.totiter, W, X);
411 res.Q = Q[0];
412 res.W = W;
413 res.X = X;
414 for (std::size_t i = 0; i < M; ++i)
415 for (std::size_t r = 0; r < R; ++r)
416 res.U(i, r) = nservers[i] == 1
417 ? T(X[r] * L(i, r))
418 : T(X[r] * L(i, r) / num_traits<T>::from_int(nservers[i]));
419 for (std::size_t r = 0; r < R; ++r)
420 res.C[r] = N[r] == 0 ? zero : num_traits<T>::from_int(N[r]) / X[r] - Zs[r];
421 return res;
422}
423
424/** MATLAB defaults: all stations PS, tol = 1e-8, maxiter = 1000, no warm start. */
425template <class T>
426LinearizerResult<T> pfqn_linearizerms(const Matrix<T>& L, const std::vector<int>& N,
427 const Matrix<T>& Z, const std::vector<int>& nservers) {
428 return pfqn_linearizerms(L, N, Z, nservers, std::vector<SchedStrategy>(), 1e-8, 1000,
429 Matrix<T>());
430}
431
432} // namespace pfqn
433} // namespace line
434
435#endif // LINE_API_PFQN_LINEARIZERMS_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.
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_linearizerms(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)
Multiserver Linearizer (Krzesinski's Linearizer as described in Conway 1989, with De Souza e Silva an...
double enorm_diff(const Matrix< T > &A, const Matrix< T > &B)
Frobenius norm of the difference of two equally shaped matrices, AS A DOUBLE.
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Bard-Schweitzer approximate MVA.
Extended generalized fixed-point Linearizer (De Souza e Silva and Muntz's generalization of Chandy an...
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