LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sens_mom.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_MOM_H
6#define LINE_API_PFQN_SENS_MOM_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact moments E[Q], Var[Q], E[Q^2] and E[Q^3] of the grouped queue lengths of
12 * a closed product-form network, by second-order differentiation of the MVA
13 * recursion.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_sens_mom.m. Theorem 3.1 of
16 * Strelen (Performance Evaluation 11:127-142, 1990) states that one further
17 * factor Q_i in a moment costs one differentiation with respect to x_i, the
18 * reciprocal capacity of station i, so with m_i = E[Q_i] and equation (3.2)
19 *
20 * Var[Q_i] = x_i dm_i/dx_i
21 * Cov[Q_i,Q_j] = x_j dm_i/dx_j
22 * E[Q_i^2] = x_i dm_i/dx_i + m_i^2
23 * E[Q_i^3] = x_i^2 d2m_i/dx_i^2 + (x_i + 3 x_i m_i) dm_i/dx_i + m_i^3
24 *
25 * The third moment therefore needs the SECOND derivative of the recursion,
26 * which is what this routine adds over pfqn_sens_mva and pfqn_sens. The
27 * parameter y_(h,g) rescales the demands of the classes of group g at station
28 * h; at y = 1 the y-derivatives are exactly the scaled x-derivatives that
29 * (3.2) asks for, because a pure rescaling gives d/dy = x d/dx and
30 * d2/dy2 = x^2 d2/dx2. The grouping is the class subset T of Theorem 1 of
31 * Akyildiz and Strelen (IEEE TComm 39(6):828-832, 1991); groups all equal to 0
32 * gives the per-station totals of Strelen's x_i, one group per class gives the
33 * per-class moments.
34 *
35 * Arithmetic. The recursion is field-only, so the moments instantiate at
36 * line::Rational and are exact rationals. The skewness is the one derived
37 * quantity that leaves the field (it divides by Var^1.5), so it is returned as
38 * a double rather than as a T, and the header carries no transcendental gate.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <vector>
44
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
50
51namespace line {
52namespace pfqn {
53
54template <class T>
56 std::vector<T> XN; ///< (R) throughput
57 Matrix<T> QN; ///< (M x R) mean queue length
58 Matrix<T> UN; ///< (M x R) utilization
59 Matrix<T> CN; ///< (M x R) residence time (MATLAB field .R)
60
61 Matrix<T> m; ///< (M x G) E[Q_(i,g)]
62 Matrix<T> d2m; ///< (M x G) the scaled pure second derivative
63 /// (M*G x M*G) Cov[Q_(i,g),Q_(j,g')] at row i*G+g, column j*G+g'.
65 Matrix<T> dm; ///< (M*G x M*G) the raw scaled first derivative, same layout
66 Matrix<T> Var; ///< (M x G)
67 Matrix<T> M2; ///< (M x G) E[Q^2]
68 Matrix<T> M3; ///< (M x G) E[Q^3]
69 Matrix<double> Skew; ///< (M x G) skewness, NaN where the variance vanishes
70 T CovAsym; ///< raw asymmetry of Cov before symmetrization
71};
72
73/**
74 * @brief Exact moments E[Q], Var[Q], E[Q^2] and E[Q^3] of the grouped queue
75 * lengths of a closed product-form network, by second-order
76 * differentiation of the MVA recursion.
77 *
78 * @param L (M x R) service demands
79 * @param N (R) population per class, closed only
80 * @param Z (R) think times, empty for none
81 * @param mi (M) station multiplicities, empty for all ones
82 * @param groups (R) 0-based group label of each class; empty means one group
83 * holding every class, i.e. the per-station totals
84 */
85template <class T>
86SensMomResult<T> pfqn_sens_mom(const Matrix<T>& L, const std::vector<int>& N,
87 const std::vector<T>& Z, const std::vector<int>& mi,
88 const std::vector<int>& groups) {
89 const std::size_t M = L.rows();
90 const std::size_t R = N.size();
91 if (!L.empty() && L.cols() != R)
92 throw InputError("pfqn_sens_mom: demand matrix and population vector disagree on the class count");
93 if (!Z.empty() && Z.size() != R)
94 throw InputError("pfqn_sens_mom: think-time vector has the wrong length");
95 if (!mi.empty() && mi.size() != M)
96 throw InputError("pfqn_sens_mom: multiplicity vector has the wrong length");
97
98 std::vector<int> grp = groups.empty() ? std::vector<int>(R, 0) : groups;
99 if (grp.size() != R)
100 throw InputError("pfqn_sens_mom: groups must have one label per class");
101 std::size_t G = 0;
102 for (int g : grp) {
103 if (g < 0) throw InputError("pfqn_sens_mom: group labels start at zero");
104 if (static_cast<std::size_t>(g) + 1 > G) G = static_cast<std::size_t>(g) + 1;
105 }
106 {
107 std::vector<bool> seen(G, false);
108 for (int g : grp) seen[static_cast<std::size_t>(g)] = true;
109 for (std::size_t g = 0; g < G; ++g)
110 if (!seen[g])
111 throw InputError("pfqn_sens_mom: groups must label the classes consecutively with no empty group");
112 }
113
114 const T zero = num_traits<T>::from_int(0);
115 const std::size_t P = M * G;
116
118 res.XN.assign(R, zero);
119 res.QN = Matrix<T>(M, R, zero);
120 res.UN = Matrix<T>(M, R, zero);
121 res.CN = Matrix<T>(M, R, zero);
122 res.m = Matrix<T>(M, G, zero);
123 res.d2m = Matrix<T>(M, G, zero);
124 res.Cov = Matrix<T>(P, P, zero);
125 res.dm = Matrix<T>(P, P, zero);
126 res.Var = Matrix<T>(M, G, zero);
127 res.M2 = Matrix<T>(M, G, zero);
128 res.M3 = Matrix<T>(M, G, zero);
129 res.Skew = Matrix<double>(M, G, 0.0);
130 res.CovAsym = zero;
131
132 bool anyPositive = false;
133 for (int v : N) {
134 if (v < 0) throw InputError("pfqn_sens_mom: negative population");
135 if (v > 0) anyPositive = true;
136 }
137 if (!anyPositive || M == 0 || R == 0) return res;
138
139 const auto Zr = [&](std::size_t r) -> T { return Z.empty() ? zero : Z[r]; };
140 const auto miT = [&](std::size_t i) -> T {
141 return num_traits<T>::from_int(mi.empty() ? 1 : mi[i]);
142 };
143 const auto pidx = [&](std::size_t h, std::size_t g) { return h * G + g; };
144
145 const std::vector<std::size_t> radix = sens_lattice_radix(N);
146 const std::size_t totpop = population_count(N);
147
148 Matrix<T> Qtot(totpop, M, zero);
149 std::vector<Matrix<T>> D1(totpop, Matrix<T>(M, P, zero));
150 std::vector<Matrix<T>> D2(totpop, Matrix<T>(M, P, zero));
151
152 // The group accumulators describe one population only and are overwritten
153 // on every sweep, so at the end of the walk they hold the values at N.
154 Matrix<T> Qg(M, G, zero);
155 std::vector<Matrix<T>> D1Qg(P, Matrix<T>(M, G, zero));
156 std::vector<Matrix<T>> D2Qg(P, Matrix<T>(M, G, zero));
157
158 std::vector<T> Cs(M, zero), dCNtot(P, zero), d2CNtot(P, zero), dX(P, zero), d2X(P, zero);
159 Matrix<T> dCs(M, P, zero), d2Cs(M, P, zero);
160
161 for (std::size_t k = 1; k < totpop; ++k) {
162 const std::vector<int> n = sens_lattice_decode(k, N, radix);
163 Qg.fill(zero);
164 for (std::size_t p = 0; p < P; ++p) {
165 D1Qg[p].fill(zero);
166 D2Qg[p].fill(zero);
167 }
168 for (std::size_t s = 0; s < R; ++s) {
169 const std::size_t row = n[s] > 0 ? k - radix[s] : 0;
170 const std::size_t gs = static_cast<std::size_t>(grp[s]);
171
172 // ---- residence times and their first two derivatives ----------
173 T CNtot = zero;
174 for (std::size_t p = 0; p < P; ++p) {
175 dCNtot[p] = zero;
176 d2CNtot[p] = zero;
177 }
178 for (std::size_t i = 0; i < M; ++i) {
179 const T A = miT(i) + Qtot(row, i);
180 Cs[i] = L(i, s) * A;
181 res.CN(i, s) = Cs[i];
182 CNtot += Cs[i];
183 for (std::size_t p = 0; p < P; ++p) {
184 const T dA = D1[row](i, p);
185 const T d2A = D2[row](i, p);
186 if (p == pidx(i, gs)) {
187 dCs(i, p) = L(i, s) * (A + dA);
188 d2Cs(i, p) = L(i, s) * (num_traits<T>::from_int(2) * dA + d2A);
189 } else {
190 dCs(i, p) = L(i, s) * dA;
191 d2Cs(i, p) = L(i, s) * d2A;
192 }
193 dCNtot[p] += dCs(i, p);
194 d2CNtot[p] += d2Cs(i, p);
195 }
196 }
197
198 // ---- throughput and its first two derivatives ------------------
199 const T den = Zr(s) + CNtot;
200 const T nsT = num_traits<T>::from_int(n[s]);
201 if (den == zero) {
202 res.XN[s] = zero;
203 for (std::size_t p = 0; p < P; ++p) {
204 dX[p] = zero;
205 d2X[p] = zero;
206 }
207 } else {
208 const T den2 = den * den;
209 const T den3 = den2 * den;
210 res.XN[s] = nsT / den;
211 for (std::size_t p = 0; p < P; ++p) {
212 dX[p] = -nsT * dCNtot[p] / den2;
213 d2X[p] = -nsT * d2CNtot[p] / den2 +
214 num_traits<T>::from_int(2) * nsT * dCNtot[p] * dCNtot[p] / den3;
215 }
216 }
217
218 // ---- queue lengths ---------------------------------------------
219 for (std::size_t i = 0; i < M; ++i) {
220 res.QN(i, s) = res.XN[s] * Cs[i];
221 Qtot(k, i) += res.QN(i, s);
222 Qg(i, gs) += res.QN(i, s);
223 for (std::size_t p = 0; p < P; ++p) {
224 const T dQ = dX[p] * Cs[i] + res.XN[s] * dCs(i, p);
225 const T d2Q = d2X[p] * Cs[i] +
226 num_traits<T>::from_int(2) * dX[p] * dCs(i, p) +
227 res.XN[s] * d2Cs(i, p);
228 D1[k](i, p) += dQ;
229 D2[k](i, p) += d2Q;
230 D1Qg[p](i, gs) += dQ;
231 D2Qg[p](i, gs) += d2Q;
232 }
233 }
234 }
235 }
236
237 for (std::size_t i = 0; i < M; ++i)
238 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
239
240 for (std::size_t i = 0; i < M; ++i)
241 for (std::size_t g = 0; g < G; ++g) {
242 res.m(i, g) = Qg(i, g);
243 for (std::size_t j = 0; j < M; ++j)
244 for (std::size_t g2 = 0; g2 < G; ++g2)
245 res.dm(pidx(i, g), pidx(j, g2)) = D1Qg[pidx(j, g2)](i, g);
246 res.d2m(i, g) = D2Qg[pidx(i, g)](i, g);
247 }
248
249 // Cov((i,g),(j,g')) and its transpose are distinct expressions for the same
250 // quantity; report the raw disagreement, then symmetrize.
251 for (std::size_t a = 0; a < P; ++a)
252 for (std::size_t b = 0; b < P; ++b) {
253 const T d = num_abs(T(res.dm(a, b) - res.dm(b, a)));
254 if (d > res.CovAsym) res.CovAsym = d;
255 }
256 for (std::size_t a = 0; a < P; ++a)
257 for (std::size_t b = 0; b < P; ++b)
258 res.Cov(a, b) = (res.dm(a, b) + res.dm(b, a)) / num_traits<T>::from_int(2);
259
260 for (std::size_t i = 0; i < M; ++i)
261 for (std::size_t g = 0; g < G; ++g) {
262 const T d1 = res.dm(pidx(i, g), pidx(i, g));
263 const T mi_g = res.m(i, g);
264 res.Var(i, g) = d1;
265 res.M2(i, g) = d1 + mi_g * mi_g;
266 res.M3(i, g) = res.d2m(i, g) +
268 mi_g * mi_g * mi_g;
269 const T mu3 = res.M3(i, g) - num_traits<T>::from_int(3) * mi_g * res.M2(i, g) +
270 num_traits<T>::from_int(2) * mi_g * mi_g * mi_g;
271 const double var = num_traits<T>::to_double(res.Var(i, g));
272 res.Skew(i, g) = var > 0.0 ? num_traits<T>::to_double(mu3) / std::pow(var, 1.5)
273 : std::nan("");
274 }
275
276 return res;
277}
278
279/** pfqn_sens_mom with unit multiplicities and per-station totals. */
280template <class T>
281SensMomResult<T> pfqn_sens_mom(const Matrix<T>& L, const std::vector<int>& N,
282 const std::vector<T>& Z) {
283 return pfqn_sens_mom(L, N, Z, std::vector<int>(), std::vector<int>());
284}
285
286} // namespace pfqn
287} // namespace line
288
289#endif // LINE_API_PFQN_SENS_MOM_H
InputError(const std::string &what)
Definition error.h:39
void fill(const T &x)
Definition matrix.h:108
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.
std::vector< int > sens_lattice_decode(std::size_t k, const std::vector< int > &N, const std::vector< std::size_t > &radix)
Decode a lattice index back into a population vector.
std::vector< std::size_t > sens_lattice_radix(const std::vector< int > &N)
Radix weights of the MVA population lattice, class R-1 varying fastest.
SensMomResult< T > pfqn_sens_mom(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< int > &mi, const std::vector< int > &groups)
Exact moments E[Q], Var[Q], E[Q^2] and E[Q^3] of the grouped queue lengths of a closed product-form n...
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Exact per-station queue-length variances and covariances of a closed product-form network,...
Population-vector enumeration and combinatorics.
Matrix< double > Skew
(M x G) skewness, NaN where the variance vanishes
Matrix< T > UN
(M x R) utilization
std::vector< T > XN
(R) throughput
Matrix< T > CN
(M x R) residence time (MATLAB field .R)
Matrix< T > M3
(M x G) E[Q^3]
Matrix< T > Cov
(M*G x M*G) Cov[Q_(i,g),Q_(j,g')] at row i*G+g, column j*G+g'.
T CovAsym
raw asymmetry of Cov before symmetrization
Matrix< T > d2m
(M x G) the scaled pure second derivative
Matrix< T > M2
(M x G) E[Q^2]
Matrix< T > QN
(M x R) mean queue length
Matrix< T > m
(M x G) E[Q_(i,g)]
Matrix< T > Var
(M x G)
Matrix< T > dm
(M*G x M*G) the raw scaled first derivative, same layout