LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_traffic_idc.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_NPFQN_TRAFFIC_IDC_H
6#define LINE_API_NPFQN_TRAFFIC_IDC_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Traffic variability equations of the Robust Queueing Network Analyzer
12 * (W. Whitt and W. You 2018, "A Robust Queueing Network Analyzer Based on
13 * Indices of Dispersion").
14 *
15 * Templated port of matlab/src/api/npfqn/npfqn_traffic_idc.m. The JAR carries
16 * the same algorithm in jar/src/main/java/jline/api/npfqn/Npfqn_traffic_idc.java;
17 * both were cross-read and agree on every equation.
18 *
19 * The model is a single-class open network of K single-server FCFS queues with
20 * Markovian routing P. Two pieces are computed:
21 *
22 * 1. npfqn_traffic_idc assembles and solves the LIMITING variability
23 * equations (eq. 42/44) for the asymptotic arrival variability parameters
24 * c2_{a,i} = I_{a,i}(Inf), together with the flow rates (eq. 20-21), the
25 * fundamental matrix Xi = (I - P')^{-1}, the splitting correction
26 * alpha_{i,j} (eq. 34) and the superposition correction beta_i (eq. 38-39).
27 * Every operation there is an addition, a multiplication, a division or a
28 * linear solve in the field of the inputs, so this stays exact at
29 * T = Rational with no reformulation.
30 *
31 * 2. npfqn_traffic_idc_at solves the TIME-DEPENDENT IDC equations (eq. 40/43)
32 * at a single time t and returns I_{a,i}(t). It weights the equations by
33 * the canonical RBM correlation weight w*, which is an erfc and an exp, so
34 * that half requires transcendental arithmetic.
35 *
36 * Deviations from MATLAB, both mechanical:
37 * - the external-arrival and service IDC handles a0IdcFun and sIdcFun are
38 * arguments of npfqn_traffic_idc_at rather than fields of the context, so
39 * the context stays a plain value type with no std::function in it;
40 * - MATLAB signals "no weight available at this station" by setting the
41 * weight argument to Inf, which w* maps to 1. An exact field has no
42 * infinity, so the port tests the same condition (h_i > 0 and c2x_i > 0)
43 * and uses the weight 1 directly. The two are identical in double.
44 */
45
46#include <cstddef>
47#include <functional>
48#include <vector>
49
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/lu.h"
55#include "line/util/matrix.h"
56
57namespace line {
58namespace npfqn {
59
60/** Toggle for the two correction terms, mirroring MATLAB's `corrections`. */
62 bool alpha = true; ///< splitting correction alpha_{i,j}, eq. (34)
63 bool beta = true; ///< superposition correction beta_i, eqs. (38)-(39)
64};
65
66/** Context returned by npfqn_traffic_idc, mirroring the MATLAB `ctx` struct. */
67template <class T>
69 std::size_t K = 0; ///< number of queues
70 std::vector<T> lambda; ///< (K) total arrival rate at each queue
71 std::vector<T> lambda0; ///< (K) external arrival rate at each queue
72 std::vector<T> mu; ///< (K) service rate
73 std::vector<T> rho; ///< (K) utilization lambda_i / mu_i
74 std::vector<T> cs2; ///< (K) service SCV
75 std::vector<T> c2a0; ///< (K) external arrival asymptotic IDC
76 std::vector<T> c2a; ///< (K) total arrival asymptotic IDC
77 std::vector<T> c2d; ///< (K) departure asymptotic IDC
78 std::vector<T> c2x; ///< (K) c2a + cs2
79 Matrix<T> P; ///< (K x K) routing matrix
80 Matrix<T> Xi; ///< (K x K) fundamental matrix (I - P')^{-1}
81 Matrix<T> lam_ji; ///< (K x K) lam_ji(j,i) = lambda_j p_{j,i}
82 Matrix<T> c2aij; ///< (K x K) flow (i,j) asymptotic IDC
83 Matrix<T> c2alpha; ///< (K x K) splitting correction
84 std::vector<Matrix<T>> zetaAll; ///< (K) each (K x K), zetaAll[i](j,k) = zeta_{j,i;k,i}
85};
86
87namespace detail {
88
89/** Inverse of a square matrix by LU, one solve per column of the identity. */
90template <class T>
91Matrix<T> matrix_inverse(const Matrix<T>& A) {
92 const std::size_t n = A.rows();
93 if (A.cols() != n) throw InputError("npfqn_traffic_idc: matrix is not square");
94 Matrix<T> LU = A;
95 const std::vector<std::size_t> piv = lu_factor(LU);
97 for (std::size_t j = 0; j < n; ++j) {
98 std::vector<T> e(n, num_traits<T>::from_int(0));
100 lu_solve(LU, piv, e);
101 for (std::size_t i = 0; i < n; ++i) inv(i, j) = e[i];
102 }
103 return inv;
104}
105
106} // namespace detail
107
108/**
109 * @brief Traffic variability equations of the Robust Queueing Network
110 * Analyzer (W. Whitt and W. You 2018, "A Robust Queueing Network
111 * Analyzer Based on Indices of Dispersion").
112 *
113 * @param lambda0 (K) external arrival rate into each queue
114 * @param P (K x K) routing matrix among the queues, P(i,j) = p_{i,j}
115 * @param c2a0 (K) asymptotic IDC (SCV) of each external arrival process
116 * @param mu (K) service rate at each queue
117 * @param cs2 (K) service SCV c2_{s,i}
118 * @param corrections which of the two correction terms to include
119 */
120template <class T>
121TrafficIdcContext<T> npfqn_traffic_idc(const std::vector<T>& lambda0, const Matrix<T>& P,
122 const std::vector<T>& c2a0, const std::vector<T>& mu,
123 const std::vector<T>& cs2,
124 const TrafficIdcCorrections& corrections) {
125 const std::size_t K = mu.size();
126 if (P.rows() != K || P.cols() != K)
127 throw InputError("npfqn_traffic_idc: routing matrix and service rates disagree on K");
128 if (lambda0.size() != K || c2a0.size() != K || cs2.size() != K)
129 throw InputError("npfqn_traffic_idc: input vectors disagree on K");
130
131 const T zero = num_traits<T>::from_int(0);
132 const T one = num_traits<T>::from_int(1);
133 const T two = num_traits<T>::from_int(2);
134
136 ctx.K = K;
137 ctx.lambda0 = lambda0;
138 ctx.mu = mu;
139 ctx.cs2 = cs2;
140 ctx.c2a0 = c2a0;
141 ctx.P = P;
142
143 // ----- traffic rate equations (eq. 20-21) -----
144 Matrix<T> ImPt(K, K, zero);
145 for (std::size_t i = 0; i < K; ++i)
146 for (std::size_t j = 0; j < K; ++j) ImPt(i, j) = (i == j ? one : zero) - P(j, i);
147 ctx.Xi = detail::matrix_inverse(ImPt);
148
149 ctx.lambda.assign(K, zero);
150 for (std::size_t i = 0; i < K; ++i)
151 for (std::size_t j = 0; j < K; ++j) ctx.lambda[i] += ctx.Xi(i, j) * lambda0[j];
152
153 ctx.rho.assign(K, zero);
154 for (std::size_t i = 0; i < K; ++i) ctx.rho[i] = ctx.lambda[i] / mu[i];
155
156 ctx.lam_ji = Matrix<T>(K, K, zero);
157 for (std::size_t j = 0; j < K; ++j)
158 for (std::size_t i = 0; i < K; ++i) ctx.lam_ji(j, i) = ctx.lambda[j] * P(j, i);
159
160 // ----- correction terms (asymptotic, w*(Inf) = 1) -----
161 // alpha: c2alpha_{i,j} = 2 Xi_{i,j} p_{i,j} (1 - p_{i,j})
162 ctx.c2alpha = Matrix<T>(K, K, zero);
163 if (corrections.alpha) {
164 for (std::size_t i = 0; i < K; ++i)
165 for (std::size_t j = 0; j < K; ++j)
166 ctx.c2alpha(i, j) = two * ctx.Xi(i, j) * P(i, j) * (one - P(i, j));
167 }
168
169 // beta: splitting covariance Sigma_l at each station, then the Brownian
170 // variance-rate matrix Amat, then zeta_{j,i;k,i} of eq. (39)
171 std::vector<Matrix<T>> Sigma(K, Matrix<T>(K, K, zero));
172 for (std::size_t l = 0; l < K; ++l) {
173 Matrix<T> Sl(K, K, zero);
174 for (std::size_t a = 0; a < K; ++a)
175 for (std::size_t b = 0; b < K; ++b) Sl(a, b) = -P(l, a) * P(l, b) * ctx.lambda[l];
176 for (std::size_t a = 0; a < K; ++a) Sl(a, a) = P(l, a) * (one - P(l, a)) * ctx.lambda[l];
177 Sigma[l] = Sl;
178 }
179 Matrix<T> Amat(K, K, zero);
180 for (std::size_t i = 0; i < K; ++i) Amat(i, i) = c2a0[i] * lambda0[i];
181 for (std::size_t l = 0; l < K; ++l)
182 for (std::size_t a = 0; a < K; ++a)
183 for (std::size_t b = 0; b < K; ++b) Amat(a, b) += Sigma[l](a, b);
184
185 ctx.zetaAll.assign(K, Matrix<T>(K, K, zero));
186 std::vector<T> c2beta(K, zero);
187 for (std::size_t i = 0; i < K; ++i) {
188 // nu(l,:) = p_{l,i} Xi(l,:)
189 Matrix<T> nu(K, K, zero);
190 for (std::size_t l = 0; l < K; ++l)
191 for (std::size_t b = 0; b < K; ++b) nu(l, b) = P(l, i) * ctx.Xi(l, b);
192 // Z = nu Amat nu'
193 Matrix<T> nuA(K, K, zero);
194 for (std::size_t a = 0; a < K; ++a)
195 for (std::size_t b = 0; b < K; ++b) {
196 T s = zero;
197 for (std::size_t c = 0; c < K; ++c) s += nu(a, c) * Amat(c, b);
198 nuA(a, b) = s;
199 }
200 Matrix<T> Z(K, K, zero);
201 for (std::size_t a = 0; a < K; ++a)
202 for (std::size_t b = 0; b < K; ++b) {
203 T s = zero;
204 for (std::size_t c = 0; c < K; ++c) s += nuA(a, c) * nu(b, c);
205 Z(a, b) = s;
206 }
207 // cross terms nu_k Sigma_j e_i + nu_j Sigma_k e_i
208 for (std::size_t j = 0; j < K; ++j)
209 for (std::size_t k = 0; k < K; ++k) {
210 T s = zero;
211 for (std::size_t c = 0; c < K; ++c)
212 s += nu(k, c) * Sigma[j](c, i) + nu(j, c) * Sigma[k](c, i);
213 Z(j, k) += s;
214 }
215 ctx.zetaAll[i] = Z;
216 T s = zero;
217 for (std::size_t j = 0; j < K; ++j)
218 for (std::size_t k = j + 1; k < K; ++k) s += Z(j, k);
219 if (ctx.lambda[i] > zero) c2beta[i] = (two / ctx.lambda[i]) * s;
220 }
221 if (!corrections.beta) {
222 c2beta.assign(K, zero);
223 ctx.zetaAll.assign(K, Matrix<T>(K, K, zero));
224 }
225
226 // ----- limiting variability equations (eq. 44): (E - Minf) c = binf -----
227 // variable ordering: [c2a(1..K), c2aij(i,j) row-major, c2d(1..K)]
228 const std::size_t Na = K, Naij = K * K, N = Na + Naij + Na;
229 Matrix<T> E(N, N, zero);
230 std::vector<T> binf(N, zero);
231 for (std::size_t i = 0; i < N; ++i) E(i, i) = one;
232 for (std::size_t i = 0; i < K; ++i) {
233 if (ctx.lambda[i] > zero) {
234 for (std::size_t j = 0; j < K; ++j)
235 E(i, Na + j * K + i) -= ctx.lam_ji(j, i) / ctx.lambda[i];
236 binf[i] = (lambda0[i] / ctx.lambda[i]) * c2a0[i] + c2beta[i];
237 }
238 for (std::size_t j = 0; j < K; ++j) {
239 E(Na + i * K + j, Na + Naij + i) -= P(i, j);
240 binf[Na + i * K + j] = (one - P(i, j)) + ctx.c2alpha(i, j);
241 }
242 E(Na + Naij + i, i) -= one;
243 }
244 const std::vector<T> csol = solve(E, binf);
245
246 ctx.c2a.assign(csol.begin(), csol.begin() + static_cast<long>(K));
247 ctx.c2aij = Matrix<T>(K, K, zero);
248 for (std::size_t i = 0; i < K; ++i)
249 for (std::size_t j = 0; j < K; ++j) ctx.c2aij(i, j) = csol[Na + i * K + j];
250 ctx.c2d.assign(csol.begin() + static_cast<long>(Na + Naij), csol.end());
251 ctx.c2x.assign(K, zero);
252 for (std::size_t i = 0; i < K; ++i) ctx.c2x[i] = ctx.c2a[i] + cs2[i];
253 return ctx;
254}
255
256/** Default corrections (both on), matching MATLAB's `nargin < 8` branch. */
257template <class T>
258TrafficIdcContext<T> npfqn_traffic_idc(const std::vector<T>& lambda0, const Matrix<T>& P,
259 const std::vector<T>& c2a0, const std::vector<T>& mu,
260 const std::vector<T>& cs2) {
261 return npfqn_traffic_idc(lambda0, P, c2a0, mu, cs2, TrafficIdcCorrections());
262}
263
264/**
265 * Time-dependent IDC equations (eq. 43) at a single time t, the port of the
266 * MATLAB nested function local_idc_at reached through ctx.IaFun.
267 *
268 * @param ctx context from npfqn_traffic_idc
269 * @param t time argument
270 * @param a0IdcFun external-arrival IDC, a0IdcFun(t) -> (K) vector
271 * @param sIdcFun service IDC, sIdcFun(rho .* t) -> (K) vector
272 * @return (K) total arrival IDCs I_{a,i}(t)
273 */
274template <class T>
275std::vector<T> npfqn_traffic_idc_at(const TrafficIdcContext<T>& ctx, const T& t,
276 const std::function<std::vector<T>(const T&)>& a0IdcFun,
277 const std::function<std::vector<T>(const std::vector<T>&)>& sIdcFun) {
279 "npfqn_traffic_idc_at requires transcendental arithmetic");
280 const std::size_t K = ctx.K;
281 const T zero = num_traits<T>::from_int(0);
282 const T one = num_traits<T>::from_int(1);
283
284 // tuning function h(rho) = rho^2
285 std::vector<T> h(K, zero);
286 for (std::size_t i = 0; i < K; ++i) h[i] = ctx.rho[i] * ctx.rho[i];
287
288 // departure weights w_i(t) = w*((1-rho_i)^2 lambda_i t / (h_i c2x_i))
289 std::vector<T> w(K, one);
290 for (std::size_t i = 0; i < K; ++i) {
291 if (h[i] > zero && ctx.c2x[i] > zero) {
292 const T num = (one - ctx.rho[i]) * (one - ctx.rho[i]) * ctx.lambda[i] * t;
293 w[i] = npfqn_rqna_weight(T(num / (h[i] * ctx.c2x[i])));
294 } // else MATLAB passes Inf, whose weight is 1
295 }
296
297 const std::vector<T> Ia0 = a0IdcFun(t);
298 std::vector<T> rhot(K, zero);
299 for (std::size_t i = 0; i < K; ++i) rhot[i] = ctx.rho[i] * t;
300 const std::vector<T> Is = sIdcFun(rhot); // service IDC at scaled time rho*t
301 if (Ia0.size() != K || Is.size() != K)
302 throw InputError("npfqn_traffic_idc_at: an IDC handle returned the wrong length");
303
304 // alpha_{i,j}(t) = c2alpha_{i,j} w_i(t)
305 Matrix<T> alpha_t(K, K, zero);
306 for (std::size_t i = 0; i < K; ++i)
307 for (std::size_t j = 0; j < K; ++j) alpha_t(i, j) = ctx.c2alpha(i, j) * w[i];
308
309 // beta_i(t) = (1/lambda_i) sum_{j != k} zeta_{j,i;k,i} w*(arg_j)
310 std::vector<T> beta_t(K, zero);
311 for (std::size_t i = 0; i < K; ++i) {
312 const Matrix<T>& Z = ctx.zetaAll[i];
313 std::vector<T> wj(K, zero);
314 for (std::size_t j = 0; j < K; ++j) {
315 if (h[j] > zero && ctx.c2x[j] > zero && ctx.P(j, i) > zero) {
316 const T aj = (one - ctx.rho[j]) * (one - ctx.rho[j]) * ctx.P(j, i) * ctx.lambda[j] * t;
317 wj[j] = npfqn_rqna_weight(T(aj / (h[j] * ctx.c2x[j])));
318 }
319 }
320 T s = zero;
321 for (std::size_t j = 0; j < K; ++j)
322 for (std::size_t k = 0; k < K; ++k)
323 if (j != k) s += Z(j, k) * wj[j];
324 if (ctx.lambda[i] > zero) beta_t[i] = s / ctx.lambda[i];
325 }
326
327 // assemble (E - M(t)) I = b(t)
328 const std::size_t Na = K, Naij = K * K, N = Na + Naij + Na;
329 Matrix<T> E(N, N, zero);
330 std::vector<T> b(N, zero);
331 for (std::size_t i = 0; i < N; ++i) E(i, i) = one;
332 for (std::size_t i = 0; i < K; ++i) {
333 if (ctx.lambda[i] > zero) {
334 for (std::size_t j = 0; j < K; ++j)
335 E(i, Na + j * K + i) -= ctx.lam_ji(j, i) / ctx.lambda[i];
336 b[i] = (ctx.lambda0[i] / ctx.lambda[i]) * Ia0[i] + beta_t[i];
337 }
338 for (std::size_t j = 0; j < K; ++j) {
339 E(Na + i * K + j, Na + Naij + i) -= ctx.P(i, j);
340 b[Na + i * K + j] = (one - ctx.P(i, j)) + alpha_t(i, j);
341 }
342 E(Na + Naij + i, i) -= w[i];
343 b[Na + Naij + i] = (one - w[i]) * Is[i];
344 }
345 const std::vector<T> sol = solve(E, b);
346 return std::vector<T>(sol.begin(), sol.begin() + static_cast<long>(K));
347}
348
349} // namespace npfqn
350} // namespace line
351
352#endif // LINE_API_NPFQN_TRAFFIC_IDC_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
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
TrafficIdcContext< T > npfqn_traffic_idc(const std::vector< T > &lambda0, const Matrix< T > &P, const std::vector< T > &c2a0, const std::vector< T > &mu, const std::vector< T > &cs2, const TrafficIdcCorrections &corrections)
Traffic variability equations of the Robust Queueing Network Analyzer (W.
T npfqn_rqna_weight(const T &t)
Canonical reflected-Brownian-motion correlation weight w*(t) used by the Robust Queueing Network Anal...
std::vector< T > npfqn_traffic_idc_at(const TrafficIdcContext< T > &ctx, const T &t, const std::function< std::vector< T >(const T &)> &a0IdcFun, const std::function< std::vector< T >(const std::vector< T > &)> &sIdcFun)
Time-dependent IDC equations (eq.
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Canonical reflected-Brownian-motion correlation weight w*(t) used by the Robust Queueing Network Anal...
Shared arithmetic helpers for the templated npfqn port.
Number-type abstraction for the templated API port.
Context returned by npfqn_traffic_idc, mirroring the MATLAB ctx struct.
Matrix< T > c2alpha
(K x K) splitting correction
std::vector< T > c2a0
(K) external arrival asymptotic IDC
std::size_t K
number of queues
Matrix< T > c2aij
(K x K) flow (i,j) asymptotic IDC
std::vector< T > rho
(K) utilization lambda_i / mu_i
std::vector< T > c2d
(K) departure asymptotic IDC
Matrix< T > P
(K x K) routing matrix
std::vector< T > c2x
(K) c2a + cs2
std::vector< T > lambda0
(K) external arrival rate at each queue
std::vector< T > c2a
(K) total arrival asymptotic IDC
Matrix< T > Xi
(K x K) fundamental matrix (I - P')^{-1}
Matrix< T > lam_ji
(K x K) lam_ji(j,i) = lambda_j p_{j,i}
std::vector< T > mu
(K) service rate
std::vector< T > cs2
(K) service SCV
std::vector< Matrix< T > > zetaAll
(K) each (K x K), zetaAll[i](j,k) = zeta_{j,i;k,i}
std::vector< T > lambda
(K) total arrival rate at each queue
Toggle for the two correction terms, mirroring MATLAB's corrections.
bool beta
superposition correction beta_i, eqs. (38)-(39)
bool alpha
splitting correction alpha_{i,j}, eq. (34)