LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sens_linearizer.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_SENS_LINEARIZER_H
6#define LINE_API_PFQN_SENS_LINEARIZER_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Approximate moments E[Q_i], Var[Q_i], Cov[Q_i,Q_j], E[Q_i^2] and E[Q_i^3] of
12 * the per-station total queue lengths of a closed product-form network, by the
13 * LINEARIZER-2 / LINEARIZER-3 algorithms of Strelen (Performance Evaluation
14 * 11:127-142, 1990, Section 5).
15 *
16 * Templated port of matlab/src/api/pfqn/pfqn_sens_linearizer.m. The exact
17 * moment analysis of pfqn_sens_mom walks the whole population lattice and so
18 * costs O(prod(N+1)); the Linearizer replaces that lattice by a fixed point
19 * over R+1 populations, and the reference observes that the same trick applies
20 * to the derivatives: differentiate the Linearizer equations, append them to
21 * the originals and iterate everything together. Carrying the first derivative
22 * is LINEARIZER-2, carrying the second as well is LINEARIZER-3; both are done
23 * here, so the third moment is available.
24 *
25 * CORE (equations (5.1)-(5.2)) estimates the queue lengths one job down by
26 * v_i(l) = m_i(l)/n(l), m_i^(n-e_l')(l) = (n - e_l')_l (v_i(l) + delta_i(l',l))
27 * and substitutes them into the exact MVA equations; delta comes from (5.3),
28 * delta_i(l',l) = v_i^(N-e_l')(l) - v_i^(N)(l), and is held fixed across
29 * populations by the heuristic (5.4). Differentiating (5.1)-(5.3) gives
30 * (5.5)-(5.8), carried alongside.
31 *
32 * Accuracy. The reference reports, over 51 networks, relative errors below
33 * 2.1% on E[Q], 4.1% on E[Q^2] and 6.2% on E[Q^3]. This is an approximation
34 * and is expected to disagree with pfqn_sens_mom by about that much. CovAsym
35 * is a genuine error indicator here, not a roundoff residual: the product form
36 * makes Cov symmetric but the Linearizer fixed point does not enforce it.
37 *
38 * Arithmetic. static_assert(has_transcendental) -- unlike every other member
39 * of the sensitivity family, this routine reaches a fixed point only to within
40 * a stopping tolerance, so its output is a function of the termination test
41 * rather than of the model alone and exact arithmetic buys nothing. Same
42 * rationale as pfqn_linearizer.h, on top of which this is built.
43 */
44
45#include <cmath>
46#include <cstddef>
47#include <vector>
48
49#include "line/num/number.h"
50#include "line/util/error.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace pfqn {
55
56template <class T>
58 std::vector<T> XN; ///< (R) throughput
59 Matrix<T> QN; ///< (M x R) queue length per class
60 Matrix<T> UN; ///< (M x R) utilization
61 Matrix<T> WN; ///< (M x R) residence time
62
63 std::vector<T> m; ///< (M) E[Q_i], the total queue at station i
64 Matrix<T> dm; ///< (M x M) dm(i,h) = x_h dm_i/dx_h
65 std::vector<T> d2m; ///< (M) x_i^2 d2m_i/dx_i^2
66 Matrix<T> Cov; ///< (M x M) symmetrized dm
67 std::vector<T> Var; ///< (M)
68 std::vector<T> M2; ///< (M) E[Q^2]
69 std::vector<T> M3; ///< (M) E[Q^3]
70 std::vector<double> Skew; ///< (M) skewness, NaN where the variance vanishes
71 T CovAsym; ///< raw asymmetry of Cov, an error indicator here
72 unsigned iter; ///< total CORE iterations performed
73};
74
75namespace detail {
76
77/** (5.1) and (5.5): v_i(l) = m_i(l)/n(l), likewise for the derivatives. */
78template <class T>
79void sens_lin_fractions(const Matrix<T>& m, const std::vector<Matrix<T>>& dm,
80 const std::vector<Matrix<T>>& d2m, const std::vector<int>& n,
81 Matrix<T>& v, std::vector<Matrix<T>>& dv, std::vector<Matrix<T>>& d2v) {
82 const std::size_t M = m.rows(), R = m.cols(), MM = dm.size();
83 const T zero = num_traits<T>::from_int(0);
84 v = Matrix<T>(M, R, zero);
85 dv.assign(MM, Matrix<T>(M, R, zero));
86 d2v.assign(MM, Matrix<T>(M, R, zero));
87 for (std::size_t l = 0; l < R; ++l) {
88 if (n[l] <= 0) continue;
89 const T nT = num_traits<T>::from_int(n[l]);
90 for (std::size_t i = 0; i < M; ++i) {
91 v(i, l) = m(i, l) / nT;
92 for (std::size_t h = 0; h < MM; ++h) {
93 dv[h](i, l) = dm[h](i, l) / nT;
94 d2v[h](i, l) = d2m[h](i, l) / nT;
95 }
96 }
97 }
98}
99
100/**
101 * CORE-2 of the reference extended to second derivatives: iterates (5.1),
102 * (5.2), the MVA equations (1.4)-(1.5) and their derivatives (5.5)-(5.6),
103 * (3.4) until the mean queue lengths and the variances both stop moving.
104 */
105template <class T>
106unsigned sens_lin_core2(const Matrix<T>& L, const std::vector<T>& Z, const std::vector<int>& n,
107 const std::vector<Matrix<T>>& delta,
108 const std::vector<std::vector<Matrix<T>>>& ddelta,
109 const std::vector<std::vector<Matrix<T>>>& d2delta, Matrix<T>& m,
110 std::vector<Matrix<T>>& dm, std::vector<Matrix<T>>& d2m,
111 std::vector<T>& lam, Matrix<T>& w, const T* tol, unsigned maxiter) {
112 const std::size_t M = L.rows(), R = L.cols();
113 const T zero = num_traits<T>::from_int(0);
114 int nc = 0;
115 for (int v : n) nc += v;
116 const T tolm = tol ? *tol
118 num_traits<T>::from_int(4000 + 16 * static_cast<long>(nc));
119 const T tolv = num_traits<T>::from_double(1e-3);
120
121 lam.assign(R, zero);
122 w = Matrix<T>(M, R, zero);
123 std::vector<T> varprev(M, zero);
124 unsigned it = 0;
125 for (unsigned iter = 1; iter <= maxiter; ++iter) {
126 it = iter;
127 const Matrix<T> mprev = m;
128
129 // ---- (5.1)-(5.2) and (5.5)-(5.6) ---------------------------------
130 Matrix<T> v;
131 std::vector<Matrix<T>> dv, d2v;
132 sens_lin_fractions(m, dm, d2m, n, v, dv, d2v);
133 Matrix<T> mtot(M, R, zero);
134 std::vector<Matrix<T>> dmtot(M, Matrix<T>(M, R, zero));
135 std::vector<Matrix<T>> d2mtot(M, Matrix<T>(M, R, zero));
136 for (std::size_t l = 0; l < R; ++l) {
137 if (n[l] <= 0) continue;
138 for (std::size_t i = 0; i < M; ++i) {
139 T acc = zero;
140 std::vector<T> dacc(M, zero), d2acc(M, zero);
141 for (std::size_t l2 = 0; l2 < R; ++l2) {
142 const int cnt = n[l2] - (l2 == l ? 1 : 0);
143 if (cnt <= 0) continue;
144 const T cT = num_traits<T>::from_int(cnt);
145 acc += cT * (v(i, l2) + delta[i](l, l2));
146 for (std::size_t h = 0; h < M; ++h) {
147 dacc[h] += cT * (dv[h](i, l2) + ddelta[i][h](l, l2));
148 d2acc[h] += cT * (d2v[h](i, l2) + d2delta[i][h](l, l2));
149 }
150 }
151 mtot(i, l) = acc;
152 for (std::size_t h = 0; h < M; ++h) {
153 dmtot[h](i, l) = dacc[h];
154 d2mtot[h](i, l) = d2acc[h];
155 }
156 }
157 }
158
159 // ---- MVA (1.4)-(1.5) and its derivatives (3.4) --------------------
160 w = Matrix<T>(M, R, zero);
161 std::vector<Matrix<T>> dw(M, Matrix<T>(M, R, zero)), d2w(M, Matrix<T>(M, R, zero));
162 const T one = num_traits<T>::from_int(1);
163 for (std::size_t l = 0; l < R; ++l) {
164 if (n[l] <= 0) continue;
165 for (std::size_t i = 0; i < M; ++i) {
166 const T A = one + mtot(i, l);
167 w(i, l) = L(i, l) * A;
168 for (std::size_t h = 0; h < M; ++h) {
169 const T dA = dmtot[h](i, l);
170 const T d2A = d2mtot[h](i, l);
171 if (i == h) {
172 dw[h](i, l) = L(i, l) * (A + dA);
173 d2w[h](i, l) = L(i, l) * (num_traits<T>::from_int(2) * dA + d2A);
174 } else {
175 dw[h](i, l) = L(i, l) * dA;
176 d2w[h](i, l) = L(i, l) * d2A;
177 }
178 }
179 }
180 }
181 lam.assign(R, zero);
182 Matrix<T> dlam(R, M, zero), d2lam(R, M, zero);
183 for (std::size_t l = 0; l < R; ++l) {
184 if (n[l] <= 0) continue;
185 T sw = zero;
186 for (std::size_t i = 0; i < M; ++i) sw += w(i, l);
187 const T den = (Z.empty() ? zero : Z[l]) + sw;
188 const T nT = num_traits<T>::from_int(n[l]);
189 lam[l] = nT / den;
190 const T den2 = den * den;
191 const T den3 = den2 * den;
192 for (std::size_t h = 0; h < M; ++h) {
193 T dden = zero, d2den = zero;
194 for (std::size_t i = 0; i < M; ++i) {
195 dden += dw[h](i, l);
196 d2den += d2w[h](i, l);
197 }
198 dlam(l, h) = -nT * dden / den2;
199 d2lam(l, h) = -nT * d2den / den2 + num_traits<T>::from_int(2) * nT * dden * dden / den3;
200 }
201 }
202 m = Matrix<T>(M, R, zero);
203 dm.assign(M, Matrix<T>(M, R, zero));
204 d2m.assign(M, Matrix<T>(M, R, zero));
205 for (std::size_t l = 0; l < R; ++l) {
206 if (n[l] <= 0) continue;
207 for (std::size_t i = 0; i < M; ++i) {
208 m(i, l) = lam[l] * w(i, l);
209 for (std::size_t h = 0; h < M; ++h) {
210 dm[h](i, l) = dlam(l, h) * w(i, l) + lam[l] * dw[h](i, l);
211 d2m[h](i, l) = d2lam(l, h) * w(i, l) +
212 num_traits<T>::from_int(2) * dlam(l, h) * dw[h](i, l) +
213 lam[l] * d2w[h](i, l);
214 }
215 }
216 }
217
218 // ---- termination test of the reference ----------------------------
219 T dev = zero;
220 for (std::size_t l = 0; l < R; ++l) {
221 if (n[l] <= 0) continue;
222 const T nT = num_traits<T>::from_int(n[l]);
223 for (std::size_t i = 0; i < M; ++i) {
224 const T d = num_abs(T(m(i, l) - mprev(i, l))) / nT;
225 if (d > dev) dev = d;
226 }
227 }
228 std::vector<T> varnow(M, zero);
229 T sv = zero;
230 for (std::size_t i = 0; i < M; ++i) {
231 T acc = zero;
232 for (std::size_t l = 0; l < R; ++l) acc += dm[i](i, l);
233 varnow[i] = acc;
234 sv += acc;
235 }
236 T vdev = zero;
237 if (sv > zero)
238 for (std::size_t i = 0; i < M; ++i) {
239 const T d = num_abs(T(varnow[i] - varprev[i])) / sv;
240 if (d > vdev) vdev = d;
241 }
242 varprev = varnow;
243 if (dev <= tolm && vdev <= tolv) break;
244 }
245 return it;
246}
247
248} // namespace detail
249
250/**
251 * @brief Approximate moments E[Q_i], Var[Q_i], Cov[Q_i,Q_j], E[Q_i^2] and
252 * E[Q_i^3] of the per-station total queue lengths of a closed
253 * product-form network, by the LINEARIZER-2 / LINEARIZER-3 algorithms
254 * of Strelen (Performance Evaluation 11:127-142, 1990, Section 5).
255 *
256 * @param L (M x R) service demands
257 * @param N (R) population per class, closed only
258 * @param Z (R) think times, empty for none
259 * @param tol stopping tolerance on the mean queue lengths; null for the
260 * reference's own test 1/(4000 + 16 sum(n))
261 * @param maxiter maximum CORE iterations, 200 in the reference
262 */
263template <class T>
264SensLinearizerResult<T> pfqn_sens_linearizer(const Matrix<T>& L, const std::vector<int>& N,
265 const std::vector<T>& Z, const T* tol,
266 unsigned maxiter) {
268 "pfqn_sens_linearizer requires transcendental arithmetic: it is a fixed point "
269 "stopped on a tolerance, so its result is a property of the stopping test rather "
270 "than of the model, and exact arithmetic buys nothing");
271
272 const std::size_t M = L.rows();
273 const std::size_t R = N.size();
274 if (!L.empty() && L.cols() != R)
275 throw InputError("pfqn_sens_linearizer: demand matrix and population vector disagree on the class count");
276 if (!Z.empty() && Z.size() != R)
277 throw InputError("pfqn_sens_linearizer: think-time vector has the wrong length");
278
279 const T zero = num_traits<T>::from_int(0);
280
282 res.XN.assign(R, zero);
283 res.QN = Matrix<T>(M, R, zero);
284 res.UN = Matrix<T>(M, R, zero);
285 res.WN = Matrix<T>(M, R, zero);
286 res.m.assign(M, zero);
287 res.dm = Matrix<T>(M, M, zero);
288 res.d2m.assign(M, zero);
289 res.Cov = Matrix<T>(M, M, zero);
290 res.Var.assign(M, zero);
291 res.M2.assign(M, zero);
292 res.M3.assign(M, zero);
293 res.Skew.assign(M, 0.0);
294 res.CovAsym = zero;
295 res.iter = 0;
296
297 bool anyPositive = false;
298 for (int v : N) {
299 if (v < 0) throw InputError("pfqn_sens_linearizer: negative population");
300 if (v > 0) anyPositive = true;
301 }
302 if (!anyPositive || M == 0 || R == 0) return res;
303
304 // pops: index 0 = N, index 1+l = N - e_l
305 const std::size_t npops = 1 + R;
306 std::vector<std::vector<int>> pv(npops, N);
307 for (std::size_t l = 0; l < R; ++l)
308 if (N[l] > 0) pv[1 + l][l] = N[l] - 1;
309
310 std::vector<Matrix<T>> mE(npops, Matrix<T>(M, R, zero));
311 std::vector<std::vector<Matrix<T>>> dmE(npops, std::vector<Matrix<T>>(M, Matrix<T>(M, R, zero)));
312 std::vector<std::vector<Matrix<T>>> d2mE(npops, std::vector<Matrix<T>>(M, Matrix<T>(M, R, zero)));
313 for (std::size_t p = 0; p < npops; ++p)
314 for (std::size_t l = 0; l < R; ++l)
315 for (std::size_t i = 0; i < M; ++i)
316 mE[p](i, l) = num_traits<T>::from_int(pv[p][l]) / num_traits<T>::from_int(static_cast<long>(M));
317
318 std::vector<Matrix<T>> delta(M, Matrix<T>(R, R, zero));
319 std::vector<std::vector<Matrix<T>>> ddelta(M, std::vector<Matrix<T>>(M, Matrix<T>(R, R, zero)));
320 std::vector<std::vector<Matrix<T>>> d2delta(M, std::vector<Matrix<T>>(M, Matrix<T>(R, R, zero)));
321
322 std::vector<T> lam;
323 Matrix<T> wmat;
324 for (int outer = 0; outer < 3; ++outer) {
325 res.iter += detail::sens_lin_core2(L, Z, N, delta, ddelta, d2delta, mE[0], dmE[0], d2mE[0],
326 lam, wmat, tol, maxiter);
327 if (outer == 2) break;
328
329 for (std::size_t l = 0; l < R; ++l) {
330 if (N[l] == 0) continue;
331 std::vector<T> lam2;
332 Matrix<T> w2;
333 res.iter += detail::sens_lin_core2(L, Z, pv[1 + l], delta, ddelta, d2delta, mE[1 + l],
334 dmE[1 + l], d2mE[1 + l], lam2, w2, tol, maxiter);
335 }
336
337 // ---- refresh delta from (5.1) and (5.3) ---------------------------
338 Matrix<T> vN;
339 std::vector<Matrix<T>> dvN, d2vN;
340 detail::sens_lin_fractions(mE[0], dmE[0], d2mE[0], N, vN, dvN, d2vN);
341 for (std::size_t lp = 0; lp < R; ++lp) {
342 if (N[lp] == 0) continue;
343 Matrix<T> vL;
344 std::vector<Matrix<T>> dvL, d2vL;
345 detail::sens_lin_fractions(mE[1 + lp], dmE[1 + lp], d2mE[1 + lp], pv[1 + lp], vL, dvL,
346 d2vL);
347 for (std::size_t i = 0; i < M; ++i)
348 for (std::size_t l = 0; l < R; ++l) {
349 delta[i](lp, l) = vL(i, l) - vN(i, l);
350 for (std::size_t h = 0; h < M; ++h) {
351 ddelta[i][h](lp, l) = dvL[h](i, l) - dvN[h](i, l);
352 d2delta[i][h](lp, l) = d2vL[h](i, l) - d2vN[h](i, l);
353 }
354 }
355 }
356 }
357
358 res.QN = mE[0];
359 res.WN = wmat;
360 res.XN = lam;
361 for (std::size_t i = 0; i < M; ++i) {
362 T acc = zero;
363 for (std::size_t l = 0; l < R; ++l) acc += res.QN(i, l);
364 res.m[i] = acc;
365 for (std::size_t h = 0; h < M; ++h) {
366 T d = zero;
367 for (std::size_t l = 0; l < R; ++l) d += dmE[0][h](i, l);
368 res.dm(i, h) = d;
369 }
370 T d2 = zero;
371 for (std::size_t l = 0; l < R; ++l) d2 += d2mE[0][i](i, l);
372 res.d2m[i] = d2;
373 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
374 }
375
376 for (std::size_t i = 0; i < M; ++i)
377 for (std::size_t j = 0; j < M; ++j) {
378 const T d = num_abs(T(res.dm(i, j) - res.dm(j, i)));
379 if (d > res.CovAsym) res.CovAsym = d;
380 }
381 for (std::size_t i = 0; i < M; ++i)
382 for (std::size_t j = 0; j < M; ++j)
383 res.Cov(i, j) = (res.dm(i, j) + res.dm(j, i)) / num_traits<T>::from_int(2);
384
385 for (std::size_t i = 0; i < M; ++i) {
386 const T d1 = res.dm(i, i);
387 const T mi = res.m[i];
388 res.Var[i] = d1;
389 res.M2[i] = d1 + mi * mi;
390 res.M3[i] = res.d2m[i] +
391 (num_traits<T>::from_int(1) + num_traits<T>::from_int(3) * mi) * d1 + mi * mi * mi;
392 const T mu3 = res.M3[i] - num_traits<T>::from_int(3) * mi * res.M2[i] +
393 num_traits<T>::from_int(2) * mi * mi * mi;
394 const double var = num_traits<T>::to_double(res.Var[i]);
395 res.Skew[i] = var > 0.0 ? num_traits<T>::to_double(mu3) / std::pow(var, 1.5) : std::nan("");
396 }
397 return res;
398}
399
400/** pfqn_sens_linearizer with the defaults of the reference. */
401template <class T>
402SensLinearizerResult<T> pfqn_sens_linearizer(const Matrix<T>& L, const std::vector<int>& N,
403 const std::vector<T>& Z) {
404 return pfqn_sens_linearizer(L, N, Z, static_cast<const T*>(nullptr), 200u);
405}
406
407} // namespace pfqn
408} // namespace line
409
410#endif // LINE_API_PFQN_SENS_LINEARIZER_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
The exception types the port throws.
Dense matrix and non-owning view.
SensLinearizerResult< T > pfqn_sens_linearizer(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const T *tol, unsigned maxiter)
Approximate moments E[Q_i], Var[Q_i], Cov[Q_i,Q_j], E[Q_i^2] and E[Q_i^3] of the per-station total qu...
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
unsigned iter
total CORE iterations performed
Matrix< T > dm
(M x M) dm(i,h) = x_h dm_i/dx_h
Matrix< T > UN
(M x R) utilization
std::vector< T > m
(M) E[Q_i], the total queue at station i
std::vector< T > XN
(R) throughput
std::vector< T > d2m
(M) x_i^2 d2m_i/dx_i^2
Matrix< T > Cov
(M x M) symmetrized dm
T CovAsym
raw asymmetry of Cov, an error indicator here
Matrix< T > QN
(M x R) queue length per class
std::vector< double > Skew
(M) skewness, NaN where the variance vanishes
Matrix< T > WN
(M x R) residence time