LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sens.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_H
6#define LINE_API_PFQN_SENS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact analytic derivatives of the mean performance measures {X,Q,U,R} of a
12 * closed product-form (BCMP) network with respect to the demands L(i,r) and
13 * the think times Z(r).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_sens.m. Two exact kernels are
16 * dispatched transparently, exactly as the reference does:
17 *
18 * - pfqn_sens_dmva : forward-mode differentiation of the exact
19 * Reiser-Lavenberg MVA recursion. Handles every model.
20 * - pfqn_sens_comom : the Class-Oriented Method of Moments specialization for
21 * the repairman model (one single-server queue plus a delay, every
22 * populated class having a strictly positive demand and think time).
23 *
24 * Both return the identical layout, so the choice is invisible to callers.
25 *
26 * Arithmetic. The derivatives are analytic, not finite differences, and every
27 * step of both kernels is a field operation, so both instantiate at
28 * line::Rational and return exact rationals. Where the reference is forced
29 * through logarithms this port is not: MATLAB's sens_comom evaluates the
30 * normalizing-constant ratios as exp(lgm(m,n-e_s) - lgm(m,n)) because
31 * pfqn_comomrm returns only a log, whereas this port takes the ratio of the
32 * ComomResult::G values directly. That is the same identity evaluated in the
33 * field instead of through a transcendental round trip, so it is exact at
34 * Rational and agrees with MATLAB to rounding at double. This is the same
35 * substitution pfqn_mva.h already makes for the normalizing constant.
36 *
37 * Reference: Z. Liu and P. Nain, INRIA RR-1144, 1989; X.-R. Cao and D.-J. Ma,
38 * Performance Evaluation 26:181-199, 1996; G. Casale, IEEE TSE 2011.
39 */
40
41#include <cstddef>
42#include <map>
43#include <utility>
44#include <vector>
45
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
52
53namespace line {
54namespace pfqn {
55
56/** One differentiation parameter. */
57struct SensParam {
58 char type; ///< 'L' for a demand, 'Z' for a think time
59 int station; ///< 0-based station index, -1 for a 'Z' parameter
60 std::size_t cls; ///< 0-based class index
61};
62
63template <class T>
64struct SensResult {
65 std::vector<T> XN; ///< (R) throughput
66 Matrix<T> QN; ///< (M x R) mean queue length
67 Matrix<T> UN; ///< (M x R) utilization
68 Matrix<T> CN; ///< (M x R) residence time (MATLAB field .R)
69
70 std::vector<SensParam> params; ///< (P) the differentiation parameters
71
72 Matrix<T> dX; ///< (R x P) dX(r)/dparam(p)
73 std::vector<Matrix<T>> dQ; ///< (P) matrices M x R, dQ[p](i,r)
74 std::vector<Matrix<T>> dU; ///< (P) matrices M x R
75 std::vector<Matrix<T>> dR; ///< (P) matrices M x R
76
77 /// (M*R x M*R) Cov[n(i,r),n(j,s)] at row i*R+r, column j*R+s.
79 Matrix<T> QVar; ///< (M x R)
80 std::vector<T> QTotVar; ///< (M)
81 T QCovAsym; ///< residual of the moment recursion
82};
83
84namespace detail {
85
86template <class T>
87SensResult<T> sens_pack(std::size_t M, std::size_t R, std::size_t P) {
88 const T zero = num_traits<T>::from_int(0);
90 s.XN.assign(R, zero);
91 s.QN = Matrix<T>(M, R, zero);
92 s.UN = Matrix<T>(M, R, zero);
93 s.CN = Matrix<T>(M, R, zero);
94 s.dX = Matrix<T>(R, P, zero);
95 s.dQ.assign(P, Matrix<T>(M, R, zero));
96 s.dU.assign(P, Matrix<T>(M, R, zero));
97 s.dR.assign(P, Matrix<T>(M, R, zero));
98 s.QCov = Matrix<T>(M * R, M * R, zero);
99 s.QVar = Matrix<T>(M, R, zero);
100 s.QTotVar.assign(M, zero);
101 s.QCovAsym = zero;
102 return s;
103}
104
105} // namespace detail
106
107/**
108 * Forward-mode differentiation of the exact MVA recursion. Parameters are
109 * ordered L(0,0), L(0,1), ..., L(M-1,R-1), then Z(0), ..., Z(R-1).
110 */
111template <class T>
112SensResult<T> pfqn_sens_dmva(const Matrix<T>& L, const std::vector<int>& N,
113 const std::vector<T>& Z, const std::vector<int>& mi) {
114 const std::size_t M = L.rows();
115 const std::size_t R = N.size();
116 if (!L.empty() && L.cols() != R)
117 throw InputError("pfqn_sens: demand matrix and population vector disagree on the class count");
118 if (!Z.empty() && Z.size() != R)
119 throw InputError("pfqn_sens: think-time vector has the wrong length");
120 if (!mi.empty() && mi.size() != M)
121 throw InputError("pfqn_sens: multiplicity vector has the wrong length");
122
123 const T zero = num_traits<T>::from_int(0);
124 const std::size_t P = M * R + R;
125
126 SensResult<T> res = detail::sens_pack<T>(M, R, P);
127 res.params.resize(P);
128 std::vector<std::vector<std::size_t>> pL(M, std::vector<std::size_t>(R, 0));
129 std::vector<std::size_t> pZ(R, 0);
130 {
131 std::size_t p = 0;
132 for (std::size_t i = 0; i < M; ++i)
133 for (std::size_t r = 0; r < R; ++r) {
134 res.params[p].type = 'L';
135 res.params[p].station = static_cast<int>(i);
136 res.params[p].cls = r;
137 pL[i][r] = p;
138 ++p;
139 }
140 for (std::size_t r = 0; r < R; ++r) {
141 res.params[p].type = 'Z';
142 res.params[p].station = -1;
143 res.params[p].cls = r;
144 pZ[r] = p;
145 ++p;
146 }
147 }
148
149 bool anyPositive = false;
150 for (int v : N) {
151 if (v < 0) throw InputError("pfqn_sens: negative population");
152 if (v > 0) anyPositive = true;
153 }
154 if (!anyPositive || M == 0 || R == 0) return res;
155
156 const auto Zr = [&](std::size_t r) -> T { return Z.empty() ? zero : Z[r]; };
157 const auto miT = [&](std::size_t i) -> T {
158 return num_traits<T>::from_int(mi.empty() ? 1 : mi[i]);
159 };
160
161 const std::vector<std::size_t> radix = sens_lattice_radix(N);
162 const std::size_t totpop = population_count(N);
163
164 Matrix<T> Qtot(totpop, M, zero);
165 std::vector<Matrix<T>> Qtotd(totpop, Matrix<T>(M, P, zero));
166
167 std::vector<T> CNtotd(P, zero), Xd(P, zero), Qd(P, zero);
168 Matrix<T> Cd(M, P, zero);
169
170 for (std::size_t k = 1; k < totpop; ++k) {
171 const std::vector<int> n = sens_lattice_decode(k, N, radix);
172 for (std::size_t s = 0; s < R; ++s) {
173 const std::size_t row = n[s] > 0 ? k - radix[s] : 0;
174 T CNtot = zero;
175 for (std::size_t p = 0; p < P; ++p) CNtotd[p] = zero;
176 for (std::size_t i = 0; i < M; ++i) {
177 const T base = miT(i) + Qtot(row, i);
178 res.CN(i, s) = L(i, s) * base;
179 for (std::size_t p = 0; p < P; ++p) Cd(i, p) = L(i, s) * Qtotd[row](i, p);
180 Cd(i, pL[i][s]) += base;
181 CNtot += res.CN(i, s);
182 for (std::size_t p = 0; p < P; ++p) CNtotd[p] += Cd(i, p);
183 }
184 const T den = Zr(s) + CNtot;
185 const T nsT = num_traits<T>::from_int(n[s]);
186 if (den == zero) {
187 res.XN[s] = zero;
188 for (std::size_t p = 0; p < P; ++p) Xd[p] = zero;
189 } else {
190 const T den2 = den * den;
191 res.XN[s] = nsT / den;
192 for (std::size_t p = 0; p < P; ++p) Xd[p] = -nsT * CNtotd[p] / den2;
193 Xd[pZ[s]] -= nsT / den2;
194 }
195 for (std::size_t p = 0; p < P; ++p) res.dX(s, p) = Xd[p];
196 for (std::size_t i = 0; i < M; ++i) {
197 res.QN(i, s) = res.XN[s] * res.CN(i, s);
198 for (std::size_t p = 0; p < P; ++p) {
199 Qd[p] = Xd[p] * res.CN(i, s) + res.XN[s] * Cd(i, p);
200 res.dQ[p](i, s) = Qd[p];
201 res.dR[p](i, s) = Cd(i, p);
202 Qtotd[k](i, p) += Qd[p];
203 }
204 Qtot(k, i) += res.QN(i, s);
205 }
206 }
207 }
208
209 for (std::size_t i = 0; i < M; ++i) {
210 for (std::size_t r = 0; r < R; ++r) {
211 res.UN(i, r) = res.XN[r] * L(i, r);
212 for (std::size_t p = 0; p < P; ++p) res.dU[p](i, r) = res.dX(r, p) * L(i, r);
213 res.dU[pL[i][r]](i, r) += res.XN[r];
214 }
215 }
216 return res;
217}
218
219/**
220 * CoMoM-backed kernel for the repairman model (M = 1). Parameters are ordered
221 * L(0,0), ..., L(0,R-1), then Z(0), ..., Z(R-1), matching pfqn_sens_dmva at
222 * M = 1.
223 */
224template <class T>
225SensResult<T> pfqn_sens_comom(const Matrix<T>& L, const std::vector<int>& N,
226 const std::vector<T>& Z) {
227 const std::size_t R = N.size();
228 if (L.rows() != 1) throw InputError("pfqn_sens_comom: the kernel takes a single station");
229 if (L.cols() != R || Z.size() != R)
230 throw InputError("pfqn_sens_comom: L, N and Z disagree on the class count");
231
232 const T zero = num_traits<T>::from_int(0);
233 const std::size_t P = 2 * R;
234
235 SensResult<T> res = detail::sens_pack<T>(1, R, P);
236 res.params.resize(P);
237 for (std::size_t r = 0; r < R; ++r) {
238 res.params[r].type = 'L';
239 res.params[r].station = 0;
240 res.params[r].cls = r;
241 res.params[R + r].type = 'Z';
242 res.params[R + r].station = -1;
243 res.params[R + r].cls = r;
244 }
245
246 bool anyPositive = false;
247 for (int v : N) {
248 if (v < 0) throw InputError("pfqn_sens_comom: negative population");
249 if (v > 0) anyPositive = true;
250 }
251 if (!anyPositive || R == 0) return res;
252
253 // memoized-constant class-stripping rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
254 std::map<std::pair<int, std::vector<int>>, T> cache;
255 const T one = num_traits<T>::from_int(1);
256 const auto Gm = [&](int m, const std::vector<int>& n) -> T {
257 std::vector<int> nz;
258 std::vector<std::size_t> keep;
259 for (std::size_t r = 0; r < R; ++r)
260 if (n[r] > 0) {
261 nz.push_back(n[r]);
262 keep.push_back(r);
263 }
264 if (nz.empty()) return one;
265 const std::pair<int, std::vector<int>> key(m, n);
266 auto it = cache.find(key);
267 if (it != cache.end()) return it->second;
268 Matrix<T> Ls(1, keep.size(), zero);
269 Matrix<T> Zs(1, keep.size(), zero);
270 for (std::size_t j = 0; j < keep.size(); ++j) {
271 Ls(0, j) = L(0, keep[j]);
272 Zs(0, j) = Z[keep[j]];
273 }
274 const T g = pfqn_comomrm(Ls, nz, Zs, m).G;
275 cache.emplace(key, g);
276 return g;
277 };
278 const auto minus = [&](const std::vector<int>& n, std::size_t s) {
279 std::vector<int> m = n;
280 m[s] -= 1;
281 return m;
282 };
283 // Mean class-s queue at population n of the m = 1 model.
284 const auto qmean = [&](const std::vector<int>& n, std::size_t s) -> T {
285 if (n[s] < 1) return zero;
286 return L(0, s) * Gm(2, minus(n, s)) / Gm(1, n);
287 };
288 // Class-s throughput of the m-replica model.
289 const auto xput = [&](int m, const std::vector<int>& n, std::size_t s) -> T {
290 if (n[s] < 1) return zero;
291 return Gm(m, minus(n, s)) / Gm(m, n);
292 };
293 // Class-s queue at one replica of the doubled station.
294 const auto qplus = [&](const std::vector<int>& n, std::size_t s) -> T {
295 if (n[s] < 1) return zero;
296 return L(0, s) * Gm(3, minus(n, s)) / Gm(2, n);
297 };
298
299 for (std::size_t r = 0; r < R; ++r)
300 if (N[r] >= 1) res.XN[r] = xput(1, N, r);
301 for (std::size_t s = 0; s < R; ++s) res.QN(0, s) = qmean(N, s);
302 for (std::size_t r = 0; r < R; ++r) {
303 res.UN(0, r) = res.XN[r] * L(0, r);
304 if (res.XN[r] != zero) res.CN(0, r) = res.QN(0, r) / res.XN[r];
305 }
306
307 for (std::size_t r = 0; r < R; ++r) {
308 if (N[r] < 1) continue; // empty class: X = Q = 0 and every derivative vanishes
309 const std::vector<int> Nr = minus(N, r);
310 const T Xr = res.XN[r];
311 const T Qr = res.QN(0, r);
312 for (std::size_t s = 0; s < R; ++s) {
313 const T dlt = num_traits<T>::from_int(r == s ? 1 : 0);
314 // L(0,s): D_s dQ_r/dD_s = Cov[n_r,n_s]
315 const T Vrs = Qr * (dlt + num_traits<T>::from_int(2) * qplus(Nr, s) - res.QN(0, s));
316 const T dQ_L = Vrs / L(0, s);
317 const T dX_L = Xr * (qmean(Nr, s) - res.QN(0, s)) / L(0, s);
318 const T dU_L = dX_L * L(0, r) + Xr * dlt;
319 res.dQ[s](0, r) = dQ_L;
320 res.dX(r, s) = dX_L;
321 res.dU[s](0, r) = dU_L;
322 if (Xr != zero) res.dR[s](0, r) = (dQ_L * Xr - Qr * dX_L) / (Xr * Xr);
323
324 // Z(s): d log G_m(n)/dZ_s = G_m(n - e_s)/G_m(n), exact for any Z_s >= 0
325 const std::size_t p = R + s;
326 const T dX_Z = Xr * (xput(1, Nr, s) - res.XN[s]);
327 const T dQ_Z = Qr * (xput(2, Nr, s) - res.XN[s]);
328 res.dQ[p](0, r) = dQ_Z;
329 res.dX(r, p) = dX_Z;
330 res.dU[p](0, r) = dX_Z * L(0, r);
331 if (Xr != zero) res.dR[p](0, r) = (dQ_Z * Xr - Qr * dX_Z) / (Xr * Xr);
332 }
333 }
334 return res;
335}
336
337/**
338 * @brief Exact analytic derivatives of the mean performance measures
339 * {X,Q,U,R} of a closed product-form (BCMP) network with respect to the
340 * demands L(i,r) and the think times Z(r).
341 *
342 * @param L (M x R) service demands
343 * @param N (R) population per class
344 * @param Z (R) think times, empty for none
345 * @param mi (M) station multiplicities, empty for all ones
346 */
347template <class T>
348SensResult<T> pfqn_sens(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
349 const std::vector<int>& mi) {
350 const std::size_t M = L.rows();
351 const std::size_t R = N.size();
352 const T zero = num_traits<T>::from_int(0);
353 std::vector<T> Zv = Z;
354 if (Zv.empty()) Zv.assign(R, zero);
355
356 // Dispatch: the repairman model goes to CoMoM, everything else to the
357 // differentiated MVA, exactly as the reference decides.
358 const T fineTol = num_traits<T>::from_double(1e-8); // GlobalConstants.FineTol
359 bool anyPopulated = false, useComom = (M == 1);
360 if (!mi.empty())
361 for (std::size_t i = 0; i < M; ++i)
362 if (mi[i] != 1) useComom = false;
363 for (std::size_t r = 0; r < R; ++r) {
364 if (N[r] <= 0) continue;
365 anyPopulated = true;
366 if (Zv[r] <= fineTol) useComom = false;
367 if (M == 1 && L(0, r) <= fineTol) useComom = false;
368 }
369 useComom = useComom && anyPopulated;
370
371 SensResult<T> res = useComom ? pfqn_sens_comom(L, N, Zv) : pfqn_sens_dmva(L, N, Zv, mi);
372
373 // queue-length covariance identity rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
374 const SensMvaResult<T> mom = pfqn_sens_mva(L, N, Zv, mi);
375 std::vector<std::vector<std::size_t>> pL(M, std::vector<std::size_t>(R, 0));
376 std::vector<std::vector<bool>> haveL(M, std::vector<bool>(R, false));
377 for (std::size_t p = 0; p < res.params.size(); ++p)
378 if (res.params[p].type == 'L') {
379 pL[static_cast<std::size_t>(res.params[p].station)][res.params[p].cls] = p;
380 haveL[static_cast<std::size_t>(res.params[p].station)][res.params[p].cls] = true;
381 }
382 for (std::size_t i = 0; i < M; ++i)
383 for (std::size_t r = 0; r < R; ++r)
384 for (std::size_t j = 0; j < M; ++j)
385 for (std::size_t s = 0; s < R; ++s) {
386 T v = zero;
387 if (i == j) {
388 v = mom.QCov[i](r, s);
389 } else if (haveL[j][s]) {
390 v = L(j, s) * res.dQ[pL[j][s]](i, r);
391 }
392 res.QCov(i * R + r, j * R + s) = v;
393 }
394 res.QVar = mom.QVar;
395 res.QTotVar = mom.QTotVar;
396 res.QCovAsym = mom.QCovAsym;
397 return res;
398}
399
400/** pfqn_sens with unit multiplicities. */
401template <class T>
402SensResult<T> pfqn_sens(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
403 return pfqn_sens(L, N, Z, std::vector<int>());
404}
405
406} // namespace pfqn
407} // namespace line
408
409#endif // LINE_API_PFQN_SENS_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,...
ComomResult< T > pfqn_comomrm(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, int m)
CoMoM (class-oriented method of moments) for the finite repairman model: one queueing station of mult...
SensResult< T > pfqn_sens_comom(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z)
CoMoM-backed kernel for the repairman model (M = 1).
Definition pfqn_sens.h:225
@ Gm
the reference's alias of 'cub'
Definition pfqn_nc.h:111
SensResult< T > pfqn_sens_dmva(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< int > &mi)
Forward-mode differentiation of the exact MVA recursion.
Definition pfqn_sens.h:112
SensResult< T > pfqn_sens(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< int > &mi)
Exact analytic derivatives of the mean performance measures {X,Q,U,R} of a closed product-form (BCMP)...
Definition pfqn_sens.h:348
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
Number-type abstraction for the templated API port.
CoMoM (class-oriented method of moments) for the finite repairman model: one queueing station of mult...
Exact per-station queue-length variances and covariances of a closed product-form network,...
Population-vector enumeration and combinatorics.
Matrix< T > QVar
(M x R) Var[n(i,r)]
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
std::vector< T > QTotVar
(M) Var[sum_r n(i,r)]
One differentiation parameter.
Definition pfqn_sens.h:57
int station
0-based station index, -1 for a 'Z' parameter
Definition pfqn_sens.h:59
std::size_t cls
0-based class index
Definition pfqn_sens.h:60
char type
'L' for a demand, 'Z' for a think time
Definition pfqn_sens.h:58
std::vector< Matrix< T > > dR
(P) matrices M x R
Definition pfqn_sens.h:75
std::vector< Matrix< T > > dU
(P) matrices M x R
Definition pfqn_sens.h:74
Matrix< T > QN
(M x R) mean queue length
Definition pfqn_sens.h:66
Matrix< T > UN
(M x R) utilization
Definition pfqn_sens.h:67
Matrix< T > QCov
(M*R x M*R) Cov[n(i,r),n(j,s)] at row i*R+r, column j*R+s.
Definition pfqn_sens.h:78
Matrix< T > QVar
(M x R)
Definition pfqn_sens.h:79
std::vector< T > QTotVar
(M)
Definition pfqn_sens.h:80
std::vector< SensParam > params
(P) the differentiation parameters
Definition pfqn_sens.h:70
Matrix< T > dX
(R x P) dX(r)/dparam(p)
Definition pfqn_sens.h:72
T QCovAsym
residual of the moment recursion
Definition pfqn_sens.h:81
std::vector< T > XN
(R) throughput
Definition pfqn_sens.h:65
std::vector< Matrix< T > > dQ
(P) matrices M x R, dQ[p](i,r)
Definition pfqn_sens.h:73
Matrix< T > CN
(M x R) residence time (MATLAB field .R)
Definition pfqn_sens.h:68