LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_kt.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_PFQN_KT_H
6#define LINE_API_PFQN_PFQN_KT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Knessl-Tier asymptotic expansion of the normalizing constant.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_kt.m, including the two fixes the
14 * MATLAB header documents (the think-time term, absent from stock pfqn_kt in
15 * both the exponent and the Hessian, and the evaluation of the exponent at the
16 * exact saddle point rather than at the AQL throughput). In LINE's convention
17 * Cauchy extraction plus steepest descent on
18 *
19 * F(u) = sum_r Z_r u_r - sum_k log(1 - U_k) - sum_r N_r log u_r, U_k = L(k,:) u
20 *
21 * gives log G = F(u*) - sum_r log u*_r - (R/2) log(2 pi) - (1/2) log det H with
22 *
23 * H_rs = delta_rs N_r/u_r^2 + sum_k L_kr L_ks/(1 - U_k)^2
24 *
25 * and u* the solution of N_r = u_r (Z_r + sum_k L_kr/(1 - U_k)), found by
26 * damped Newton from the closed-form start 0.5 N_r / sum_k L_kr, with the AMVA
27 * throughput (pfqn_bs/pfqn_aql) as a second start only if that fails. The X and
28 * Q outputs are the saddle values X_r = u*_r, Q_kr = u*_r L_kr/(1-U_k).
29 *
30 * SELF-LOOPING CLASSES. A class that visits exactly one station and has no
31 * think time would drive U_k to 1. Extracting u_r^N_r from 1/(1-U_ist) is exact
32 * and leaves L(ist,r)^N_r with that station's factor raised to (1-V_ist)^-(1+N_r),
33 * so the class is dropped, the station replicated N_r times and N_r log L(ist,r)
34 * added to the exponent. Classes looping at the SAME station share one factor
35 * of exponent 1+sum N_r and add the multinomial (sum N_r)!/prod N_r!.
36 *
37 * ARITHMETIC. Logarithms, an asymptotic expansion and a Newton iteration, so
38 * gated on num_traits<T>::has_transcendental.
39 */
40
41#include <cmath>
42#include <cstddef>
43#include <vector>
44
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/lu.h"
51#include "line/util/matrix.h"
52
53namespace line {
54namespace pfqn {
55
56namespace detail {
57
58/**
59 * Damped Newton on the saddle-point equations g_r(u) = u_r (Z_r + sum_k
60 * L_kr/(1-U_k)) - N_r = 0 from the start u0 (MATLAB pfqn_kt_newton). Returns
61 * true only if the residual also passes the outer 1e-8 check, so a start that
62 * stalls is reported rather than silently used; u receives the final iterate.
63 */
64template <class T>
65bool pfqn_kt_newton(const Matrix<T>& L, const std::vector<T>& Z, const std::vector<T>& N,
66 const std::vector<T>& u0, const T& Ntot, std::vector<T>& u) {
67 using std::sqrt;
68 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
69 const std::size_t M = L.rows(), R = L.cols();
70 u = u0;
71 // Keep the saddle point inside the domain U_k < 1.
72 T Umax = zero;
73 for (std::size_t k = 0; k < M; ++k) {
74 T s = zero;
75 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
76 if (s > Umax) Umax = s;
77 }
78 if (Umax >= one) {
79 const T f = T(T(one - num_traits<T>::from_double(1e-6)) / Umax);
80 for (std::size_t r = 0; r < R; ++r) u[r] = T(u[r] * f);
81 }
82
83 bool converged = false;
84 for (int it = 0; it < 200; ++it) {
85 std::vector<T> D(M);
86 for (std::size_t k = 0; k < M; ++k) {
87 T s = zero;
88 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
89 D[k] = T(one / T(one - s));
90 }
91 std::vector<T> g(R);
92 T gn = zero;
93 for (std::size_t r = 0; r < R; ++r) {
94 T s = zero;
95 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
96 g[r] = T(u[r] * T(Z[r] + s) - N[r]);
97 gn += g[r] * g[r];
98 }
99 if (sqrt(gn) <= T(num_traits<T>::from_double(1e-12) * Ntot)) {
100 converged = true;
101 break;
102 }
103 Matrix<T> J(R, R, zero);
104 for (std::size_t r = 0; r < R; ++r) {
105 T s = zero;
106 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
107 J(r, r) = T(Z[r] + s);
108 for (std::size_t sIdx = 0; sIdx < R; ++sIdx) {
109 T acc = zero;
110 for (std::size_t k = 0; k < M; ++k) acc += L(k, r) * T(D[k] * D[k]) * L(k, sIdx);
111 J(r, sIdx) += u[r] * acc;
112 }
113 }
114 std::vector<T> rhs(R);
115 for (std::size_t r = 0; r < R; ++r) rhs[r] = T(-g[r]);
116 const std::vector<T> du = solve(J, rhs);
117 T alpha = one;
118 for (int b = 0; b < 60; ++b) {
119 bool ok = true;
120 for (std::size_t r = 0; r < R && ok; ++r)
121 if (T(u[r] + alpha * du[r]) <= zero) ok = false;
122 if (ok) {
123 for (std::size_t k = 0; k < M && ok; ++k) {
124 T s = zero;
125 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * T(u[r] + alpha * du[r]);
126 if (s >= one) ok = false;
127 }
128 }
129 if (ok) break;
130 alpha = T(alpha / num_traits<T>::from_int(2));
131 }
132 if (num_traits<T>::to_double(alpha) < 1e-12) break;
133 for (std::size_t r = 0; r < R; ++r) u[r] = T(u[r] + alpha * du[r]);
134 }
135 if (!converged) return false;
136 // Second acceptance test of the reference, on the residual at u.
137 std::vector<T> D(M);
138 for (std::size_t k = 0; k < M; ++k) {
139 T s = zero;
140 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
141 D[k] = T(one / T(one - s));
142 }
143 T gn = zero;
144 for (std::size_t r = 0; r < R; ++r) {
145 T s = zero;
146 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
147 const T gr = T(u[r] * T(Z[r] + s) - N[r]);
148 gn += gr * gr;
149 }
150 return sqrt(gn) <= T(num_traits<T>::from_double(1e-8) * Ntot);
151}
152
153} // namespace detail
154
155/** Return value of pfqn_kt, mirroring [G, lG, X, Q]. */
156template <class T>
157struct KtResult {
158 T G;
159 T lG;
160 std::vector<T> X; ///< saddle-point throughput X_r = u*_r
161 Matrix<T> Q; ///< saddle-point queue lengths Q_kr = u*_r L_kr/(1-U_k)
162};
163
164/**
165 * @brief Knessl-Tier asymptotic expansion of the normalizing constant.
166 *
167 * @param L0 (M x R) demands
168 * @param N0 (R) population
169 * @param Z0 (R) think times
170 */
171template <class T>
172KtResult<T> pfqn_kt(const Matrix<T>& L0, const std::vector<T>& N0, const std::vector<T>& Z0) {
174 "pfqn_kt requires transcendental arithmetic (steepest-descent expansion of log G)");
175 using std::exp;
176 using std::log;
177 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
178 const T fineTol = num_traits<T>::from_double(1e-8); // GlobalConstants.FineTol
179
180 KtResult<T> res;
181 T Nt0 = zero;
182 for (const T& v : N0) Nt0 += v;
183 if (L0.empty() || N0.empty() || Nt0 == zero) {
184 res.G = one;
185 res.lG = zero;
186 return res;
187 }
188 const std::size_t Rorig = L0.cols(), Morig = L0.rows();
189 std::vector<T> Zin = Z0;
190 if (Zin.empty()) Zin.assign(Rorig, zero);
191 if (Zin.size() != Rorig || N0.size() != Rorig)
192 throw InputError("pfqn_kt: L, N and Z disagree on the class count");
193
194 // A class with no jobs contributes a factor of 1 to G, but its saddle point is
195 // u_r -> 0, where N_r*log(u_r) and N_r/u_r^2 are indeterminate and lG comes back
196 // NaN. Solve the reduced model, as the self-looping fold below already does.
197 std::size_t nkeep = 0;
198 for (std::size_t r = 0; r < Rorig; ++r)
199 if (N0[r] > zero) ++nkeep;
200 if (nkeep > 0 && nkeep < Rorig) {
201 Matrix<T> Lk(Morig, nkeep, zero);
202 std::vector<T> Nk, Zk;
203 Nk.reserve(nkeep);
204 Zk.reserve(nkeep);
205 std::size_t c = 0;
206 for (std::size_t r = 0; r < Rorig; ++r) {
207 if (!(N0[r] > zero)) continue;
208 for (std::size_t i = 0; i < Morig; ++i) Lk(i, c) = L0(i, r);
209 Nk.push_back(N0[r]);
210 Zk.push_back(Zin[r]);
211 ++c;
212 }
213 return pfqn_kt(Lk, Nk, Zk);
214 }
215
216 // Fold self-looping classes into replicated stations.
217 T slcdemandfactor = zero;
218 std::vector<bool> isslc(Rorig, false);
219 std::vector<std::vector<T>> rows; // extra replicated station rows
220 for (std::size_t i = 0; i < Morig; ++i) {
221 std::vector<T> row(Rorig);
222 for (std::size_t r = 0; r < Rorig; ++r) row[r] = L0(i, r);
223 rows.push_back(row);
224 }
225 std::vector<std::size_t> slcstation(Rorig, 0);
226 if (Rorig > 1) {
227 for (std::size_t r = 0; r < Rorig; ++r) {
228 std::size_t nnz = 0, ist = 0;
229 for (std::size_t i = 0; i < Morig; ++i)
230 if (L0(i, r) != zero) {
231 ++nnz;
232 ist = i;
233 }
234 if (nnz != 1 || Zin[r] != zero) continue;
235 isslc[r] = true;
236 slcstation[r] = ist;
237 }
238 // Classes looping at the SAME station share one (1-V)^-(1+sum N) factor
239 // and contribute the multinomial (sum N)!/prod N_r!.
240 std::vector<bool> done(Rorig, false);
241 for (std::size_t r = 0; r < Rorig; ++r) {
242 if (!isslc[r] || done[r]) continue;
243 const std::size_t ist = slcstation[r];
244 T ntot = zero;
245 for (std::size_t s = r; s < Rorig; ++s) {
246 if (!isslc[s] || slcstation[s] != ist) continue;
247 done[s] = true;
248 ntot += N0[s];
249 slcdemandfactor += T(N0[s] * log(L0(ist, s))) - detail::num_factln<T>(N0[s]);
250 }
251 slcdemandfactor += detail::num_factln<T>(ntot);
252 const long reps = static_cast<long>(num_traits<T>::to_double(ntot));
253 for (long k = 0; k < reps; ++k) {
254 std::vector<T> row(Rorig);
255 for (std::size_t s = 0; s < Rorig; ++s) row[s] = L0(ist, s);
256 rows.push_back(row);
257 }
258 }
259 }
260 std::vector<std::size_t> keep;
261 for (std::size_t r = 0; r < Rorig; ++r)
262 if (!isslc[r]) keep.push_back(r);
263 if (keep.empty()) { // every class self-loops: the demand factors are exact
264 res.lG = slcdemandfactor;
265 res.G = exp(slcdemandfactor);
266 return res;
267 }
268 const std::size_t M = rows.size(), R = keep.size();
269 Matrix<T> L(M, R);
270 for (std::size_t i = 0; i < M; ++i)
271 for (std::size_t r = 0; r < R; ++r) L(i, r) = rows[i][keep[r]];
272 std::vector<T> N(R), Z(R);
273 for (std::size_t r = 0; r < R; ++r) {
274 N[r] = N0[keep[r]];
275 Z[r] = Zin[keep[r]];
276 }
277 T Ntot = zero;
278 for (const T& v : N) Ntot += v;
279 if (Ntot == zero) { // only self-looping classes carried jobs
280 res.lG = slcdemandfactor;
281 res.G = exp(slcdemandfactor);
282 return res;
283 }
284
285 // Solve the saddle-point equations by damped Newton from the closed-form
286 // start 0.5*N./max(sum(L,1),eps); pfqn_bs/pfqn_aql are consulted lazily,
287 // only if that fails, as a second start and as the last resort (MATLAB).
288 std::vector<T> u0(R);
289 const T epsT = num_traits<T>::from_double(2.220446049250313e-16);
290 for (std::size_t r = 0; r < R; ++r) {
291 T colsum = zero;
292 for (std::size_t k = 0; k < M; ++k) colsum += L(k, r);
293 if (colsum < epsT) colsum = epsT;
294 u0[r] = T(num_traits<T>::from_rational(1, 2) * N[r] / colsum);
295 }
296 std::vector<T> us;
297 if (!detail::pfqn_kt_newton(L, Z, N, u0, Ntot, us)) {
298 const AmvaResult<T> amva =
299 (num_traits<T>::to_double(Ntot) <= 4.0) ? pfqn_bs(L, N, Z) : pfqn_aql(L, N, Z);
300 if (!detail::pfqn_kt_newton(L, Z, N, amva.XN, Ntot, us))
301 us = amva.XN; // fallback: AQL/BS throughput (stationarity limits the damage)
302 }
303 // X and Q come from the saddle rather than from the approximate MVA solve:
304 // u* is the asymptotic MVA fixed point, so X_r = u*_r and
305 // Q_kr = u*_r L_kr / max(FineTol, 1-U_k).
306 res.X = us;
307 res.Q = Matrix<T>(M, R, zero);
308 for (std::size_t k = 0; k < M; ++k) {
309 T s = zero;
310 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * us[r];
311 T den = T(one - s);
312 if (den < fineTol) den = fineTol;
313 for (std::size_t r = 0; r < R; ++r) res.Q(k, r) = T(L(k, r) * us[r] / den);
314 }
315
316 // Assemble the expansion at us.
317 std::vector<T> Uk(M), D(M);
318 for (std::size_t k = 0; k < M; ++k) {
319 T s = zero;
320 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * us[r];
321 Uk[k] = s;
322 T den = T(one - s);
323 if (den < fineTol) den = fineTol;
324 D[k] = T(one / den);
325 }
326 Matrix<T> H(R, R, zero);
327 for (std::size_t r = 0; r < R; ++r) {
328 if (us[r] == zero) throw NumericError(ErrorKind{"Degenerate"}, "pfqn_kt: zero saddle-point coordinate");
329 H(r, r) = T(N[r] / T(us[r] * us[r]));
330 for (std::size_t s = 0; s < R; ++s) {
331 T acc = zero;
332 for (std::size_t k = 0; k < M; ++k) acc += L(k, r) * T(D[k] * D[k]) * L(k, s);
333 H(r, s) += acc;
334 }
335 }
336 T F = zero;
337 for (std::size_t r = 0; r < R; ++r) F += Z[r] * us[r];
338 for (std::size_t k = 0; k < M; ++k) {
339 T den = T(one - Uk[k]);
340 if (den < fineTol) den = fineTol;
341 F -= log(den);
342 }
343 for (std::size_t r = 0; r < R; ++r) F -= N[r] * log(us[r]);
344
345 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
346 T lG = F;
347 for (std::size_t r = 0; r < R; ++r) lG -= log(us[r]);
348 lG -= T(num_traits<T>::from_rational(static_cast<long>(R), 2) * log(twopi));
349 lG -= T(num_traits<T>::from_rational(1, 2) * detail::pfqn_logdet(H));
350 lG += slcdemandfactor;
351 res.lG = lG;
352 res.G = exp(lG);
353 return res;
354}
355
356template <class T>
357KtResult<T> pfqn_kt(const Matrix<T>& L, const std::vector<T>& N) {
358 return pfqn_kt(L, N, std::vector<T>());
359}
360
361} // namespace pfqn
362} // namespace line
363
364#endif // LINE_API_PFQN_PFQN_KT_H
InputError(const std::string &what)
Definition error.h:105
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
NumericError(const std::string &what)
Definition error.h:112
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
KtResult< T > pfqn_kt(const Matrix< T > &L0, const std::vector< T > &N0, const std::vector< T > &Z0)
Knessl-Tier asymptotic expansion of the normalizing constant.
Definition pfqn_kt.h:172
AmvaResult< T > pfqn_aql(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, double tol=1e-7, std::size_t maxiter=1000)
Aggregate Queue Length (AQL) approximate MVA.
Definition pfqn_aql.h:51
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
Number-type abstraction for the templated API port.
Aggregate Queue Length (AQL) approximate MVA.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Bard-Schweitzer approximate MVA.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Return value of pfqn_kt, mirroring [G, lG, X, Q].
Definition pfqn_kt.h:157
Matrix< T > Q
saddle-point queue lengths Q_kr = u*_r L_kr/(1-U_k)
Definition pfqn_kt.h:161
std::vector< T > X
saddle-point throughput X_r = u*_r
Definition pfqn_kt.h:160