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(ErrorKind{"Degenerate"}, "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(ErrorKind{"Instability"},
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 * Do not copy P, PB at population N into every slot: the reference's
171 * (M x c x 1+R) arrays vary in the third index once DeltaP/DeltaPB exist, and
172 * mixing queue lengths at N - e_s with marginals at N breaks the identity
173 * Q + sum_{j<=m-2} (m-1-j) p_j >= m-1 that guarantees W >= D.
174 */
175template <class T>
176void linms_estimate(std::size_t M, std::size_t R, const std::vector<int>& N_1, const Matrix<T>& Q,
177 const Matrix<T>& P, const std::vector<T>& PB,
178 const std::vector<Matrix<T>>& Delta, const std::vector<Matrix<T>>& DeltaP,
179 const Matrix<T>& DeltaPB, const std::vector<int>& nservers,
180 std::vector<Matrix<T>>& Q1, std::vector<Matrix<T>>& P1,
181 std::vector<std::vector<T>>& PB1) {
182 const T zero = num_traits<T>::from_int(0);
183 Q1.assign(R + 1, Matrix<T>(M, R, zero));
184 P1.assign(R + 1, Matrix<T>(M, P.cols(), zero));
185 PB1.assign(R + 1, std::vector<T>(M, zero));
186 for (std::size_t i = 0; i < M; ++i) {
187 if (nservers[i] > 1) {
188 for (std::size_t j = 0; j < P.cols(); ++j) {
189 P1[0](i, j) = P(i, j);
190 for (std::size_t s = 1; s <= R; ++s)
191 P1[s](i, j) = P(i, j) + DeltaP[s - 1](i, j);
192 }
193 PB1[0][i] = PB[i];
194 for (std::size_t s = 1; s <= R; ++s) PB1[s][i] = PB[i] + DeltaPB(i, s - 1);
195 }
196 for (std::size_t r = 0; r < R; ++r)
197 for (std::size_t s = 1; s <= R; ++s) {
198 const std::vector<int> Ns = oner(N_1, s);
199 if (N_1[r] <= 0 || Ns[r] <= 0) {
200 Q1[s](i, r) = zero;
201 } else {
202 Q1[s](i, r) = num_traits<T>::from_int(Ns[r]) *
203 (Q(i, r) / num_traits<T>::from_int(N_1[r]) + Delta[r](i, s - 1));
204 }
205 }
206 }
207}
208
209template <class T>
210int linms_core(const Matrix<T>& L, std::size_t M, std::size_t R, const std::vector<int>& N_1,
211 const std::vector<T>& Z, const std::vector<int>& nservers, bool allFCFS,
212 Matrix<T>& Q, Matrix<T>& P, std::vector<T>& PB,
213 const std::vector<Matrix<T>>& Delta, const std::vector<Matrix<T>>& DeltaP,
214 const Matrix<T>& DeltaPB, double tol, int maxiter, Matrix<T>& W,
215 std::vector<T>& X) {
216 int iter = 0;
217 std::vector<Matrix<T>> Q1, P1;
218 std::vector<std::vector<T>> PB1;
219 while (true) {
220 ++iter;
221 const Matrix<T> Qlast = Q;
222 linms_estimate(M, R, N_1, Q, P, PB, Delta, DeltaP, DeltaPB, nservers, Q1, P1, PB1);
223 linms_forward_mva(L, M, R, N_1, Z, nservers, allFCFS, Q1, P1, PB1, Q, W, X, P, PB);
224 const double e = enorm_diff(Q, Qlast);
225 if (e < tol || iter > maxiter) break;
226 }
227 return iter;
228}
229
230/** Initial marginal probabilities from the aggregate queue lengths. */
231template <class T>
232void linms_init_marginals(std::size_t M, std::size_t R, const std::vector<int>& nservers,
233 const std::vector<Matrix<T>>& Q, const std::vector<int>& N,
234 std::vector<Matrix<T>>& P, std::vector<std::vector<T>>& PB) {
235 const T zero = num_traits<T>::from_int(0);
236 const T one = num_traits<T>::from_int(1);
237 for (std::size_t s = 0; s <= R; ++s) {
238 const std::vector<int> N_1 = oner(N, s);
239 long pop = 0;
240 for (int v : N_1) pop += v;
241 if (pop <= 0) {
242 // Empty network: the station is idle with probability one. Skipping
243 // left p_0 at zero, i.e. a marginal that is not a distribution.
244 for (std::size_t i = 0; i < M; ++i) {
245 if (nservers[i] <= 1) continue;
246 for (std::size_t k = 0; k < P[s].cols(); ++k) P[s](i, k) = zero;
247 P[s](i, 0) = one;
248 PB[s][i] = zero;
249 }
250 continue;
251 }
252 const T popT = num_traits<T>::from_int(pop);
253 const T pop1T = num_traits<T>::from_int(pop + 1);
254 for (std::size_t i = 0; i < M; ++i) {
255 if (nservers[i] <= 1) continue;
256 T qsum = zero;
257 for (std::size_t r = 0; r < R; ++r) qsum += Q[s](i, r);
258 const T two = num_traits<T>::from_int(2);
259 for (int j = 1; j <= nservers[i] - 1; ++j)
260 P[s](i, static_cast<std::size_t>(j)) = two * qsum / (popT * pop1T);
261 // Fewer jobs than servers: they cannot all be busy, so pB is 0 rather
262 // than a division by the vanishing slack. MATLAB guards it as
263 // `pop > nservers(i)-1`; throwing here refused models the reference
264 // solves, and every population below the server count is one of them.
265 if (pop > nservers[i] - 1) {
266 const T slack = num_traits<T>::from_int(pop + 1 - nservers[i]);
267 PB[s][i] = two * qsum / slack / (popT * pop1T);
268 } else {
269 PB[s][i] = zero;
270 }
271 T p0 = one - PB[s][i];
272 for (int j = 1; j <= nservers[i] - 1; ++j) p0 -= P[s](i, static_cast<std::size_t>(j));
273 P[s](i, 0) = p0;
274 }
275 }
276}
277
278} // namespace detail
279
280/**
281 * @brief Multiserver Linearizer (Krzesinski's Linearizer as described in
282 * Conway 1989, with De Souza e Silva and Muntz's presentation of the
283 * marginal-probability recursions).
284 *
285 * @param L (M x R) service demands
286 * @param N (R) population per class
287 * @param Z (K x R) think times, summed over rows; may be empty
288 * @param nservers (M) number of servers per station, at least one
289 * @param type (M) scheduling discipline; the FCFS arm is taken only if
290 * every station is FCFS, as in MATLAB
291 * @param tol convergence tolerance
292 * @param maxiter total inner-iteration budget
293 * @param QN0 (M x R) warm start for the Bard-Schweitzer initialization
294 */
295template <class T>
296LinearizerResult<T> pfqn_linearizerms(const Matrix<T>& L, const std::vector<int>& N,
297 const Matrix<T>& Z, const std::vector<int>& nservers,
298 const std::vector<SchedStrategy>& type, double tol,
299 int maxiter, const Matrix<T>& QN0) {
300 // field-arithmetic rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
301
302 const std::size_t M = L.rows();
303 const std::size_t R = N.size();
304 if (!L.empty() && L.cols() != R)
305 throw InputError(
306 "pfqn_linearizerms: demand matrix and population vector disagree on the class count");
307 if (nservers.size() != M)
308 throw InputError("pfqn_linearizerms: server-count vector has the wrong station count");
309 for (int c : nservers)
310 if (c < 1) throw InputError("pfqn_linearizerms: server count below one");
311 if (!type.empty() && type.size() != M)
312 throw InputError("pfqn_linearizerms: scheduling vector has the wrong station count");
313 if (tol <= 0) throw InputError("pfqn_linearizerms: tolerance must be positive");
314 for (int v : N)
315 if (v < 0) throw InputError("pfqn_linearizerms: negative population");
316
317 const T zero = num_traits<T>::from_int(0);
318 const std::vector<T> Zs = sum_rows(Z, R);
319
321 res.Q = Matrix<T>(M, R, zero);
322 res.U = Matrix<T>(M, R, zero);
323 res.W = Matrix<T>(M, R, zero);
324 res.C.assign(R, zero);
325 res.X.assign(R, zero);
326 res.totiter = 0;
327 if (M == 0) return res;
328
329 bool allFCFS = !type.empty();
330 for (std::size_t i = 0; i < type.size(); ++i)
331 if (type[i] != SchedStrategy::FCFS) allFCFS = false;
332
333 std::size_t cmax = 1;
334 for (int c : nservers) cmax = static_cast<std::size_t>(c) > cmax ? c : cmax;
335
336 Matrix<T> Zm(1, R, zero);
337 for (std::size_t r = 0; r < R; ++r) Zm(0, r) = Zs[r];
338
339 std::vector<Matrix<T>> Q(R + 1, Matrix<T>(M, R, zero));
340 for (std::size_t s = 0; s <= R; ++s) {
341 const std::vector<int> N_1 = oner(N, s);
342 bool feasible = true;
343 for (int v : N_1)
344 if (v < 0) feasible = false;
345 if (!feasible) continue;
346 std::vector<T> Nt(R, zero);
347 for (std::size_t r = 0; r < R; ++r) Nt[r] = num_traits<T>::from_int(N_1[r]);
348 // pfqn_bs three-argument seed rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
349 const AmvaResult<T> b = pfqn_bs(L, Nt, Zs);
350 Q[s] = b.QN;
351 }
352
353 std::vector<Matrix<T>> P(R + 1, Matrix<T>(M, cmax, zero));
354 std::vector<std::vector<T>> PB(R + 1, std::vector<T>(M, zero));
355 detail::linms_init_marginals(M, R, nservers, Q, N, P, PB);
356
357 std::vector<Matrix<T>> Delta(R, Matrix<T>(M, R, zero));
358 // Linearizer corrections for the MARGINALS. Without them the marginals stay
359 // at population N while the queue lengths are reduced to N - e_s, which
360 // breaks Q + sum_j (m-1-j) p_j >= m-1 and lets W fall below the mean service
361 // time. Probabilities do not scale with the population, so the analogue of
362 // Delta is a plain difference rather than a per-job rate.
363 std::vector<Matrix<T>> DeltaP(R, Matrix<T>(M, cmax, zero));
364 Matrix<T> DeltaPB(M, R, zero);
365 Matrix<T> W(M, R, zero);
366 std::vector<T> X(R, zero);
367
368 for (int I = 0; I < 2; ++I) {
369 for (std::size_t s = 0; s <= R; ++s) {
370 const std::vector<int> N_1 = oner(N, s);
371 bool feasible = true;
372 for (int v : N_1)
373 if (v < 0) feasible = false;
374 if (!feasible) continue;
375 res.totiter +=
376 detail::linms_core(L, M, R, N_1, Zs, nservers, allFCFS, Q[s], P[s], PB[s], Delta,
377 DeltaP, DeltaPB, tol, maxiter - res.totiter, W, X);
378 }
379 for (std::size_t i = 0; i < M; ++i)
380 for (std::size_t r = 0; r < R; ++r) {
381 if (N[r] == 0) {
382 for (std::size_t s = 0; s < R; ++s) Delta[r](i, s) = zero;
383 continue;
384 }
385 const T nrT = num_traits<T>::from_int(N[r]);
386 for (std::size_t s = 1; s <= R; ++s) {
387 const std::vector<int> Ns = oner(N, s);
388 if (Ns[r] > 0) {
389 Delta[r](i, s - 1) =
390 Q[s](i, r) / num_traits<T>::from_int(Ns[r]) - Q[0](i, r) / nrT;
391 } else {
392 Delta[r](i, s - 1) = -Q[0](i, r) / nrT;
393 }
394 }
395 }
396 // Update_DeltaP: the marginals' counterpart of Update_Delta.
397 for (std::size_t i = 0; i < M; ++i) {
398 if (nservers[i] <= 1) continue;
399 for (std::size_t s = 1; s <= R; ++s) {
400 for (std::size_t j = 0; j < static_cast<std::size_t>(nservers[i]); ++j)
401 DeltaP[s - 1](i, j) = P[s](i, j) - P[0](i, j);
402 DeltaPB(i, s - 1) = PB[s][i] - PB[0][i];
403 }
404 }
405 }
406
407 res.totiter += detail::linms_core(L, M, R, N, Zs, nservers, allFCFS, Q[0], P[0], PB[0], Delta,
408 DeltaP, DeltaPB, tol, maxiter - res.totiter, W, X);
409 res.Q = Q[0];
410 res.W = W;
411 res.X = X;
412 for (std::size_t i = 0; i < M; ++i)
413 for (std::size_t r = 0; r < R; ++r)
414 res.U(i, r) = nservers[i] == 1
415 ? T(X[r] * L(i, r))
416 : T(X[r] * L(i, r) / num_traits<T>::from_int(nservers[i]));
417 for (std::size_t r = 0; r < R; ++r)
418 res.C[r] = N[r] == 0 ? zero : num_traits<T>::from_int(N[r]) / X[r] - Zs[r];
419 return res;
420}
421
422/** MATLAB defaults: all stations PS, tol = 1e-8, maxiter = 1000, no warm start. */
423template <class T>
424LinearizerResult<T> pfqn_linearizerms(const Matrix<T>& L, const std::vector<int>& N,
425 const Matrix<T>& Z, const std::vector<int>& nservers) {
426 return pfqn_linearizerms(L, N, Z, nservers, std::vector<SchedStrategy>(), 1e-8, 1000,
427 Matrix<T>());
428}
429
430} // namespace pfqn
431} // namespace line
432
433#endif // LINE_API_PFQN_LINEARIZERMS_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.
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.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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