LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_dmlin.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_DMLIN_H
6#define LINE_API_PFQN_DMLIN_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * de Souza e Silva-Muntz Improved Linearizer (IL).
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_dmlin.m, cross-checked against
14 * jar/src/main/java/jline/api/pfqn/mva/Pfqn_dmlin.java. E. de Souza e Silva,
15 * R. R. Muntz, "A note on the computational cost of the Linearizer algorithm
16 * for queueing networks", IEEE Trans. Computers 39(6), 1990. Linearizer
17 * evaluates the arrival-instant queue length as
18 *
19 * A_k^(c)(n) = sum_i (n_i - delta_c^(i)) [Q_ik(n)/n_i + Delta^(i)_ck],
20 *
21 * re-summing the C Delta-terms at every Core iteration, at every one of the
22 * C+1 populations: O(K C^3) per refresh pass. IL splits that sum into the part
23 * that moves with the Core iterate and the part that does not,
24 *
25 * A_k^(c)(n) = sum_i (n_i - delta_c^(i)) Q_ik(n)/n_i + xi_ck(n),
26 * xi_ck(N) = sum_i (N_i - delta_c^(i)) Delta^(i)_ck,
27 * xi_ck(N - 1_j) = xi_ck(N) - Delta^(j)_ck,
28 *
29 * so the C K aggregates xi are computed ONCE per refresh pass and each Core
30 * iteration then costs O(K C) instead of O(K C^2). Time drops to O(K C^2) with
31 * the space unchanged at O(K C^2), and, because the split is an IDENTITY and
32 * not an approximation, the fixed point is the one Linearizer reaches:
33 * pfqn_dmlin and pfqn_linearizer agree to round-off. Transcribing (2.50) of
34 * the Wang (1997) survey literally -- xi_ck(N - 1_j) ~= xi_ck(N), dropping the
35 * Delta^(j)_ck correction -- breaks that agreement and costs about an order of
36 * magnitude of accuracy, so the correction is not optional.
37 *
38 * Arithmetic: field operations only, so each iterate is EXACT in rational
39 * arithmetic; the Core stops on enorm(Q_{k+1} - Q_k) < tol, so the returned
40 * value still depends on the stopping rule.
41 */
42
43#include <cstddef>
44#include <vector>
45
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace pfqn {
55
56namespace detail {
57
58/** Fixed point of the aggregated arrival-instant estimate with the MVA equations. */
59template <class T>
60struct DmlinCore {
61 Matrix<T> Q;
62 Matrix<T> W;
63 std::vector<T> X;
64 int iter;
65};
66
67template <class T>
68DmlinCore<T> dmlin_core(const Matrix<T>& L, const std::vector<int>& N1, const std::vector<T>& Z,
69 const Matrix<T>& Qin, const Matrix<T>& xi, double tol, int maxiter) {
70 const std::size_t M = L.rows(), R = L.cols();
71 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
72 DmlinCore<T> out;
73 out.Q = Qin;
74 out.W = Matrix<T>(M, R, zero);
75 out.X.assign(R, zero);
76 out.iter = 0;
77 bool converged = false;
78 while (!converged) {
79 const Matrix<T> Qlast = out.Q;
80 for (std::size_t c = 0; c < R; ++c)
81 for (std::size_t i = 0; i < M; ++i) {
82 T acc = zero;
83 for (std::size_t s = 0; s < R; ++s) {
84 if (N1[s] <= 0) continue;
85 const long nr = static_cast<long>(N1[s]) - (s == c ? 1L : 0L);
86 if (nr <= 0) continue;
87 acc += num_traits<T>::from_int(nr) * out.Q(i, s) /
88 num_traits<T>::from_int(static_cast<long>(N1[s]));
89 }
90 out.W(i, c) = L(i, c) * T(one + acc + xi(i, c));
91 }
92 for (std::size_t s = 0; s < R; ++s) {
93 T wsum = zero;
94 for (std::size_t i = 0; i < M; ++i) wsum += out.W(i, s);
95 const T zs = Z.empty() ? zero : Z[s];
96 if (N1[s] > 0) {
97 const T den = T(zs + wsum);
98 if (den == zero) throw NumericError("pfqn_dmlin: zero cycle time");
99 out.X[s] = num_traits<T>::from_int(static_cast<long>(N1[s])) / den;
100 } else {
101 out.X[s] = zero;
102 }
103 for (std::size_t i = 0; i < M; ++i) out.Q(i, s) = out.X[s] * out.W(i, s);
104 }
105 // enorm_diff, not enorm: sqrt is not an operation of the rational field,
106 // so a T-valued norm cannot be instantiated for the exact backend at all
107 // (pfqn_amva_common.h:62-72). line-cli instantiates this whole chain for
108 // cpp_rational via `-a normconst`, so enorm(diff) broke the C++ build.
109 // The sibling linearizers all use this form; it drops the temporary too.
110 if (enorm_diff(out.Q, Qlast) < tol || out.iter > maxiter) converged = true;
111 ++out.iter;
112 }
113 return out;
114}
115
116} // namespace detail
117
118/**
119 * @brief de Souza e Silva-Muntz Improved Linearizer (IL).
120 *
121 * @param L (M x R) service demands
122 * @param N (R) population per class
123 * @param Z (K x R) think times, summed over rows; may be empty
124 * @param type (M) scheduling discipline; accepted and unused, as in
125 * pfqn_linearizer, which treats every station as single-server PS
126 * @param tol convergence tolerance
127 * @param maxiter total inner-iteration budget
128 * @param QN0 (M x R) warm start of the Bard-Schweitzer seed; may be empty
129 * @param npasses number of xi refresh passes (3, the Chandy-Neuse rule)
130 */
131template <class T>
132LinearizerResult<T> pfqn_dmlin(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
133 const std::vector<SchedStrategy>& type, double tol, int maxiter,
134 const Matrix<T>& QN0, int npasses = 3) {
135 (void)type;
136 const std::size_t M = L.rows(), R = L.cols();
137 if (N.size() != R) throw InputError("pfqn_dmlin: L and N disagree on the class count");
138 const T zero = num_traits<T>::from_int(0);
139
140 std::vector<T> Zv(R, zero);
141 if (!Z.empty()) {
142 if (Z.cols() != R) throw InputError("pfqn_dmlin: Z has the wrong width");
143 for (std::size_t s = 0; s < R; ++s)
144 for (std::size_t k = 0; k < Z.rows(); ++k) Zv[s] += Z(k, s);
145 }
146
148 res.Q = Matrix<T>(M, R, zero);
149 res.U = Matrix<T>(M, R, zero);
150 res.W = Matrix<T>(M, R, zero);
151 res.C.assign(R, zero);
152 res.X.assign(R, zero);
153 res.totiter = 0;
154
155 bool allZero = true;
156 for (std::size_t i = 0; i < M && allZero; ++i)
157 for (std::size_t s = 0; s < R; ++s)
158 if (L(i, s) != zero) {
159 allZero = false;
160 break;
161 }
162 if (M == 0 || allZero) {
163 for (std::size_t s = 0; s < R; ++s) {
164 if (Zv[s] != zero) res.X[s] = num_traits<T>::from_int(N[s]) / Zv[s];
165 for (std::size_t i = 0; i < M; ++i) res.U(i, s) = res.X[s] * L(i, s);
166 }
167 return res;
168 }
169
170 // Initialize, as Linearizer does, from Bard-Schweitzer at every population
171 std::vector<Matrix<T> > Qs(R + 1);
172 for (std::size_t s = 0; s <= R; ++s) {
173 const std::vector<int> N1 = oner(N, s);
174 std::vector<T> Nt(R, zero);
175 for (std::size_t c = 0; c < R; ++c) Nt[c] = num_traits<T>::from_int(N1[c]);
176 const AmvaResult<T> seed =
177 pfqn_bs(L, Nt, Zv, std::vector<AmvaSched>(), tol, static_cast<std::size_t>(maxiter),
178 QN0);
179 Qs[s] = seed.QN;
180 }
181
182 // Delta[i][r][c] is the Delta^(r)_c term of station i
183 std::vector<std::vector<std::vector<T> > > Delta(
184 M, std::vector<std::vector<T> >(R, std::vector<T>(R, zero)));
185 Matrix<T> xi(M, R, zero);
186
187 for (int pass = 0; pass < npasses; ++pass) {
188 for (std::size_t s = 0; s <= R; ++s) {
189 const std::vector<int> N1 = oner(N, s);
190 // xi at population N - 1_s, exactly; s == 0 leaves xi at N
191 Matrix<T> xis(M, R, zero);
192 for (std::size_t i = 0; i < M; ++i)
193 for (std::size_t c = 0; c < R; ++c)
194 xis(i, c) = (s == 0) ? xi(i, c) : T(xi(i, c) - Delta[i][s - 1][c]);
195 const detail::DmlinCore<T> cr =
196 detail::dmlin_core(L, N1, Zv, Qs[s], xis, tol, maxiter - res.totiter);
197 Qs[s] = cr.Q;
198 res.totiter += cr.iter;
199 }
200 // Refresh the Delta-terms, then aggregate them into xi once per pass
201 for (std::size_t i = 0; i < M; ++i)
202 for (std::size_t r = 0; r < R; ++r) {
203 if (N[r] == 1) Qs[r + 1](i, r) = zero;
204 for (std::size_t s = 1; s <= R; ++s) {
205 const long ns = static_cast<long>(N[r]) - (r == s - 1 ? 1L : 0L);
206 if (N[r] > 0 && ns > 0)
207 Delta[i][r][s - 1] = Qs[s](i, r) / num_traits<T>::from_int(ns) -
208 Qs[0](i, r) / num_traits<T>::from_int(N[r]);
209 else if (N[r] > 0)
210 Delta[i][r][s - 1] = zero - Qs[0](i, r) / num_traits<T>::from_int(N[r]);
211 else
212 Delta[i][r][s - 1] = zero;
213 }
214 }
215 for (std::size_t i = 0; i < M; ++i)
216 for (std::size_t c = 0; c < R; ++c) {
217 T acc = zero;
218 for (std::size_t r = 0; r < R; ++r) {
219 const long w = static_cast<long>(N[r]) - (r == c ? 1L : 0L);
220 if (w > 0) acc += num_traits<T>::from_int(w) * Delta[i][r][c];
221 }
222 xi(i, c) = acc;
223 }
224 }
225
226 const detail::DmlinCore<T> fin =
227 detail::dmlin_core(L, N, Zv, Qs[0], xi, tol, maxiter - res.totiter);
228 res.totiter += fin.iter;
229 res.Q = fin.Q;
230 res.W = fin.W;
231 res.X = fin.X;
232 for (std::size_t i = 0; i < M; ++i)
233 for (std::size_t s = 0; s < R; ++s) res.U(i, s) = res.X[s] * L(i, s);
234 for (std::size_t s = 0; s < R; ++s)
235 res.C[s] = (res.X[s] == zero) ? zero
236 : T(num_traits<T>::from_int(N[s]) / res.X[s] - Zv[s]);
237 return res;
238}
239
240template <class T>
241LinearizerResult<T> pfqn_dmlin(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
242 return pfqn_dmlin(L, N, Z, std::vector<SchedStrategy>(), 1e-8, 1000, Matrix<T>());
243}
244
245template <class T>
246LinearizerResult<T> pfqn_dmlin(const Matrix<T>& L, const std::vector<int>& N) {
247 return pfqn_dmlin(L, N, Matrix<T>(), std::vector<SchedStrategy>(), 1e-8, 1000, Matrix<T>());
248}
249
250} // namespace pfqn
251} // namespace line
252
253#endif // LINE_API_PFQN_DMLIN_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.
LinearizerResult< T > pfqn_dmlin(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< SchedStrategy > &type, double tol, int maxiter, const Matrix< T > &QN0, int npasses=3)
de Souza e Silva-Muntz Improved Linearizer (IL).
Definition pfqn_dmlin.h:132
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
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