LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sens_mva.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_MVA_H
6#define LINE_API_PFQN_SENS_MVA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact per-station queue-length variances and covariances of a closed
12 * product-form network, by the MVA-like moment recursion of de Souza e Silva
13 * and Muntz (IEEE TC 37(9):1125-1129, 1988, Corollary 1).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_sens_mva.m. Writing
16 * W(k,i;v,j|n) = Cov[n(i,k),n(j,v)], differentiating the Reiser-Lavenberg
17 * equation and rescaling gives
18 *
19 * W(k,i;v,j|n) = Q(j,v|n) (Q(i,k|n-e_v) - Q(i,k|n))
20 * + [i==j && k==v] Q(j,v|n)
21 * + X(v|n) L(j,v) sum_t W(k,i;t,j|n-e_v)
22 *
23 * with W(.|0) = 0. Only the same-station case i==j is evaluated, because only
24 * then does the inner sum stay at the station and close on the single scalar
25 * Ssum(j,k|n) = sum_t W(k,j;t,j|n) carried along the lattice. The
26 * cross-station blocks are not self-contained and are left to pfqn_sens.
27 *
28 * Arithmetic. Every operation is an addition, a multiplication or a division
29 * by a lattice quantity, so the recursion stays in the field of the inputs and
30 * instantiates at line::Rational with no reformulation: the covariances of a
31 * rational model are exact rationals. There is deliberately no
32 * static_assert(has_transcendental) here.
33 */
34
35#include <cstddef>
36#include <vector>
37
38#include "line/num/number.h"
39#include "line/util/error.h"
40#include "line/util/matrix.h"
42
43namespace line {
44namespace pfqn {
45
46/**
47 * Radix weights of the MVA population lattice, class R-1 varying fastest.
48 * This is the transpose of line::plane_sizes and matches the `prods` vector of
49 * pfqn_mva.m, whose lattice index the sensitivity routines must reproduce
50 * entry by entry so that their base measures agree with pfqn_mva's.
51 */
52inline std::vector<std::size_t> sens_lattice_radix(const std::vector<int>& N) {
53 const std::size_t R = N.size();
54 std::vector<std::size_t> radix(R, 1);
55 for (std::size_t w = R; w-- > 0;) {
56 if (w + 1 == R)
57 radix[w] = 1;
58 else
59 radix[w] = radix[w + 1] * static_cast<std::size_t>(N[w + 1] + 1);
60 }
61 return radix;
62}
63
64/** Decode a lattice index back into a population vector. */
65inline std::vector<int> sens_lattice_decode(std::size_t k, const std::vector<int>& N,
66 const std::vector<std::size_t>& radix) {
67 const std::size_t R = N.size();
68 std::vector<int> n(R, 0);
69 for (std::size_t w = 0; w < R; ++w)
70 n[w] = static_cast<int>((k / radix[w]) % static_cast<std::size_t>(N[w] + 1));
71 return n;
72}
73
74template <class T>
76 std::vector<T> XN; ///< (R) throughput, identical to pfqn_mva
77 Matrix<T> QN; ///< (M x R) mean queue length
78 Matrix<T> UN; ///< (M x R) utilization
79 Matrix<T> CN; ///< (M x R) residence time (MATLAB field .R)
80 std::vector<Matrix<T>> QCov; ///< (M) matrices R x R, QCov[i](r,s) = Cov[n(i,r),n(i,s)]
81 Matrix<T> QVar; ///< (M x R) Var[n(i,r)]
82 std::vector<T> QTotVar; ///< (M) Var[sum_r n(i,r)]
83 T QCovAsym; ///< raw asymmetry of QCov before symmetrization
84};
85
86/**
87 * @brief Exact per-station queue-length variances and covariances of a closed
88 * product-form network, by the MVA-like moment recursion of de Souza e
89 * Silva and Muntz (IEEE TC 37(9):1125-1129, 1988, Corollary 1).
90 *
91 * @param L (M x R) service demands
92 * @param N (R) population per class, closed only
93 * @param Z (R) think times, empty for none
94 * @param mi (M) station multiplicities, empty for all ones
95 */
96template <class T>
97SensMvaResult<T> pfqn_sens_mva(const Matrix<T>& L, const std::vector<int>& N,
98 const std::vector<T>& Z, const std::vector<int>& mi) {
99 const std::size_t M = L.rows();
100 const std::size_t R = N.size();
101 if (!L.empty() && L.cols() != R)
102 throw InputError("pfqn_sens_mva: demand matrix and population vector disagree on the class count");
103 if (!Z.empty() && Z.size() != R)
104 throw InputError("pfqn_sens_mva: think-time vector has the wrong length");
105 if (!mi.empty() && mi.size() != M)
106 throw InputError("pfqn_sens_mva: multiplicity vector has the wrong length");
107
108 const T zero = num_traits<T>::from_int(0);
109
111 res.XN.assign(R, zero);
112 res.QN = Matrix<T>(M, R, zero);
113 res.UN = Matrix<T>(M, R, zero);
114 res.CN = Matrix<T>(M, R, zero);
115 res.QCov.assign(M, Matrix<T>(R, R, zero));
116 res.QVar = Matrix<T>(M, R, zero);
117 res.QTotVar.assign(M, zero);
118 res.QCovAsym = zero;
119
120 bool anyPositive = false;
121 for (int v : N) {
122 if (v < 0) throw InputError("pfqn_sens_mva: negative population");
123 if (v > 0) anyPositive = true;
124 }
125 if (!anyPositive || M == 0 || R == 0) return res;
126
127 const auto Zr = [&](std::size_t r) -> T { return Z.empty() ? zero : Z[r]; };
128 const auto miT = [&](std::size_t i) -> T {
129 return num_traits<T>::from_int(mi.empty() ? 1 : mi[i]);
130 };
131
132 const std::vector<std::size_t> radix = sens_lattice_radix(N);
133 const std::size_t totpop = population_count(N);
134
135 // Lattice state. Qcls and Ssum are the only per-population tables the
136 // recursion reads back; Qtot and Xall are needed by the MVA step itself.
137 Matrix<T> Qtot(totpop, M, zero);
138 std::vector<Matrix<T>> Qcls(totpop, Matrix<T>(M, R, zero));
139 Matrix<T> Xall(totpop, R, zero);
140 std::vector<Matrix<T>> Ssum(totpop, Matrix<T>(M, R, zero));
141
142 std::vector<std::size_t> rows(R, 0);
143
144 for (std::size_t k = 1; k < totpop; ++k) {
145 const std::vector<int> n = sens_lattice_decode(k, N, radix);
146
147 // ---- mean value analysis step -------------------------------------
148 for (std::size_t s = 0; s < R; ++s) {
149 // empty-population index collapse rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
150 const std::size_t row = n[s] > 0 ? k - radix[s] : 0;
151 rows[s] = row;
152 T CNtot = zero;
153 for (std::size_t i = 0; i < M; ++i) {
154 res.CN(i, s) = L(i, s) * (miT(i) + Qtot(row, i));
155 CNtot += res.CN(i, s);
156 }
157 const T den = Zr(s) + CNtot;
158 if (den == zero) {
159 res.XN[s] = zero;
160 } else {
161 res.XN[s] = num_traits<T>::from_int(n[s]) / den;
162 }
163 Xall(k, s) = res.XN[s];
164 for (std::size_t i = 0; i < M; ++i) {
165 res.QN(i, s) = res.XN[s] * res.CN(i, s);
166 Qcls[k](i, s) = res.QN(i, s);
167 Qtot(k, i) += res.QN(i, s);
168 }
169 }
170
171 // ---- moment step ---------------------------------------------------
172 for (std::size_t j = 0; j < M; ++j) {
173 for (std::size_t kk = 0; kk < R; ++kk) {
174 const T Qjk = Qcls[k](j, kk);
175 T sk = zero;
176 for (std::size_t t = 0; t < R; ++t) {
177 const T Qjt = Qcls[k](j, t);
178 T wkt = Qjt * (Qcls[rows[t]](j, kk) - Qjk);
179 if (kk == t) wkt += Qjt;
180 wkt += Xall(k, t) * L(j, t) * Ssum[rows[t]](j, kk);
181 res.QCov[j](kk, t) = wkt;
182 sk += wkt;
183 }
184 Ssum[k](j, kk) = sk;
185 }
186 }
187 }
188
189 for (std::size_t i = 0; i < M; ++i)
190 for (std::size_t r = 0; r < R; ++r) res.UN(i, r) = res.XN[r] * L(i, r);
191
192 // covariance-triangle symmetrization rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
193 for (std::size_t i = 0; i < M; ++i) {
194 for (std::size_t r = 0; r < R; ++r) {
195 for (std::size_t s = r + 1; s < R; ++s) {
196 const T d = num_abs(T(res.QCov[i](r, s) - res.QCov[i](s, r)));
197 if (d > res.QCovAsym) res.QCovAsym = d;
198 const T avg = (res.QCov[i](r, s) + res.QCov[i](s, r)) / num_traits<T>::from_int(2);
199 res.QCov[i](r, s) = avg;
200 res.QCov[i](s, r) = avg;
201 }
202 }
203 T tot = zero;
204 for (std::size_t r = 0; r < R; ++r) {
205 res.QVar(i, r) = res.QCov[i](r, r);
206 for (std::size_t s = 0; s < R; ++s) tot += res.QCov[i](r, s);
207 }
208 res.QTotVar[i] = tot;
209 }
210
211 return res;
212}
213
214/** pfqn_sens_mva with unit multiplicities. */
215template <class T>
216SensMvaResult<T> pfqn_sens_mva(const Matrix<T>& L, const std::vector<int>& N,
217 const std::vector<T>& Z) {
218 return pfqn_sens_mva(L, N, Z, std::vector<int>());
219}
220
221} // namespace pfqn
222} // namespace line
223
224#endif // LINE_API_PFQN_SENS_MVA_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.
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.
SensMvaResult< T > pfqn_sens_mva(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< int > &mi)
Exact per-station queue-length variances and covariances of a closed product-form network,...
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.
Population-vector enumeration and combinatorics.
Matrix< T > QVar
(M x R) Var[n(i,r)]
Matrix< T > CN
(M x R) residence time (MATLAB field .R)
Matrix< T > UN
(M x R) utilization
std::vector< Matrix< T > > QCov
(M) matrices R x R, QCov[i](r,s) = Cov[n(i,r),n(i,s)]
T QCovAsym
raw asymmetry of QCov before symmetrization
Matrix< T > QN
(M x R) mean queue length
std::vector< T > QTotVar
(M) Var[sum_r n(i,r)]
std::vector< T > XN
(R) throughput, identical to pfqn_mva