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 AMVA throughput.
27 *
28 * SELF-LOOPING CLASSES. A class that visits exactly one station and has no
29 * think time would drive U_k to 1. Extracting u_r^N_r from 1/(1-U_ist) is exact
30 * and leaves L(ist,r)^N_r with that station's factor raised to (1-V_ist)^-(1+N_r),
31 * so the class is dropped, the station replicated N_r times and N_r log L(ist,r)
32 * added to the exponent. Classes looping at the SAME station share one factor
33 * of exponent 1+sum N_r and add the multinomial (sum N_r)!/prod N_r!.
34 *
35 * ARITHMETIC. Logarithms, an asymptotic expansion and a Newton iteration, so
36 * gated on num_traits<T>::has_transcendental.
37 */
38
39#include <cmath>
40#include <cstddef>
41#include <vector>
42
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/lu.h"
49#include "line/util/matrix.h"
50
51namespace line {
52namespace pfqn {
53
54/** Return value of pfqn_kt, mirroring [G, lG, X, Q]. */
55template <class T>
56struct KtResult {
57 T G;
58 T lG;
59 std::vector<T> X; ///< AMVA throughput used to seed the saddle point
60 Matrix<T> Q; ///< AMVA queue lengths
61};
62
63/**
64 * @brief Knessl-Tier asymptotic expansion of the normalizing constant.
65 *
66 * @param L0 (M x R) demands
67 * @param N0 (R) population
68 * @param Z0 (R) think times
69 */
70template <class T>
71KtResult<T> pfqn_kt(const Matrix<T>& L0, const std::vector<T>& N0, const std::vector<T>& Z0) {
73 "pfqn_kt requires transcendental arithmetic (steepest-descent expansion of log G)");
74 using std::exp;
75 using std::log;
76 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
77 const T fineTol = num_traits<T>::from_double(1e-8); // GlobalConstants.FineTol
78
79 KtResult<T> res;
80 T Nt0 = zero;
81 for (const T& v : N0) Nt0 += v;
82 if (L0.empty() || N0.empty() || Nt0 == zero) {
83 res.G = one;
84 res.lG = zero;
85 return res;
86 }
87 const std::size_t Rorig = L0.cols(), Morig = L0.rows();
88 std::vector<T> Zin = Z0;
89 if (Zin.empty()) Zin.assign(Rorig, zero);
90 if (Zin.size() != Rorig || N0.size() != Rorig)
91 throw InputError("pfqn_kt: L, N and Z disagree on the class count");
92
93 // A class with no jobs contributes a factor of 1 to G, but its saddle point is
94 // u_r -> 0, where N_r*log(u_r) and N_r/u_r^2 are indeterminate and lG comes back
95 // NaN. Solve the reduced model, as the self-looping fold below already does.
96 std::size_t nkeep = 0;
97 for (std::size_t r = 0; r < Rorig; ++r)
98 if (N0[r] > zero) ++nkeep;
99 if (nkeep > 0 && nkeep < Rorig) {
100 Matrix<T> Lk(Morig, nkeep, zero);
101 std::vector<T> Nk, Zk;
102 Nk.reserve(nkeep);
103 Zk.reserve(nkeep);
104 std::size_t c = 0;
105 for (std::size_t r = 0; r < Rorig; ++r) {
106 if (!(N0[r] > zero)) continue;
107 for (std::size_t i = 0; i < Morig; ++i) Lk(i, c) = L0(i, r);
108 Nk.push_back(N0[r]);
109 Zk.push_back(Zin[r]);
110 ++c;
111 }
112 return pfqn_kt(Lk, Nk, Zk);
113 }
114
115 // Fold self-looping classes into replicated stations.
116 T slcdemandfactor = zero;
117 std::vector<bool> isslc(Rorig, false);
118 std::vector<std::vector<T>> rows; // extra replicated station rows
119 for (std::size_t i = 0; i < Morig; ++i) {
120 std::vector<T> row(Rorig);
121 for (std::size_t r = 0; r < Rorig; ++r) row[r] = L0(i, r);
122 rows.push_back(row);
123 }
124 std::vector<std::size_t> slcstation(Rorig, 0);
125 if (Rorig > 1) {
126 for (std::size_t r = 0; r < Rorig; ++r) {
127 std::size_t nnz = 0, ist = 0;
128 for (std::size_t i = 0; i < Morig; ++i)
129 if (L0(i, r) != zero) {
130 ++nnz;
131 ist = i;
132 }
133 if (nnz != 1 || Zin[r] != zero) continue;
134 isslc[r] = true;
135 slcstation[r] = ist;
136 }
137 // Classes looping at the SAME station share one (1-V)^-(1+sum N) factor
138 // and contribute the multinomial (sum N)!/prod N_r!.
139 std::vector<bool> done(Rorig, false);
140 for (std::size_t r = 0; r < Rorig; ++r) {
141 if (!isslc[r] || done[r]) continue;
142 const std::size_t ist = slcstation[r];
143 T ntot = zero;
144 for (std::size_t s = r; s < Rorig; ++s) {
145 if (!isslc[s] || slcstation[s] != ist) continue;
146 done[s] = true;
147 ntot += N0[s];
148 slcdemandfactor += T(N0[s] * log(L0(ist, s))) - detail::num_factln<T>(N0[s]);
149 }
150 slcdemandfactor += detail::num_factln<T>(ntot);
151 const long reps = static_cast<long>(num_traits<T>::to_double(ntot));
152 for (long k = 0; k < reps; ++k) {
153 std::vector<T> row(Rorig);
154 for (std::size_t s = 0; s < Rorig; ++s) row[s] = L0(ist, s);
155 rows.push_back(row);
156 }
157 }
158 }
159 std::vector<std::size_t> keep;
160 for (std::size_t r = 0; r < Rorig; ++r)
161 if (!isslc[r]) keep.push_back(r);
162 if (keep.empty()) { // every class self-loops: the demand factors are exact
163 res.lG = slcdemandfactor;
164 res.G = exp(slcdemandfactor);
165 return res;
166 }
167 const std::size_t M = rows.size(), R = keep.size();
168 Matrix<T> L(M, R);
169 for (std::size_t i = 0; i < M; ++i)
170 for (std::size_t r = 0; r < R; ++r) L(i, r) = rows[i][keep[r]];
171 std::vector<T> N(R), Z(R);
172 for (std::size_t r = 0; r < R; ++r) {
173 N[r] = N0[keep[r]];
174 Z[r] = Zin[keep[r]];
175 }
176 T Ntot = zero;
177 for (const T& v : N) Ntot += v;
178 if (Ntot == zero) { // only self-looping classes carried jobs
179 res.lG = slcdemandfactor;
180 res.G = exp(slcdemandfactor);
181 return res;
182 }
183
184 // AMVA seed.
185 const AmvaResult<T> amva =
186 (num_traits<T>::to_double(Ntot) <= 4.0) ? pfqn_bs(L, N, Z) : pfqn_aql(L, N, Z);
187 res.X = amva.XN;
188 res.Q = amva.QN;
189
190 std::vector<T> u = amva.XN;
191 // Keep the saddle point inside the domain U_k < 1.
192 T Umax = zero;
193 for (std::size_t k = 0; k < M; ++k) {
194 T s = zero;
195 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
196 if (s > Umax) Umax = s;
197 }
198 if (Umax >= one) {
199 const T f = T(T(one - num_traits<T>::from_double(1e-6)) / Umax);
200 for (std::size_t r = 0; r < R; ++r) u[r] = T(u[r] * f);
201 }
202
203 bool converged = false;
204 for (int it = 0; it < 200; ++it) {
205 std::vector<T> D(M);
206 for (std::size_t k = 0; k < M; ++k) {
207 T s = zero;
208 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
209 D[k] = T(one / T(one - s));
210 }
211 std::vector<T> g(R);
212 T gn = zero;
213 for (std::size_t r = 0; r < R; ++r) {
214 T s = zero;
215 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
216 g[r] = T(u[r] * T(Z[r] + s) - N[r]);
217 gn += g[r] * g[r];
218 }
219 using std::sqrt;
220 if (sqrt(gn) <= T(num_traits<T>::from_double(1e-12) * Ntot)) {
221 converged = true;
222 break;
223 }
224 Matrix<T> J(R, R, zero);
225 for (std::size_t r = 0; r < R; ++r) {
226 T s = zero;
227 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
228 J(r, r) = T(Z[r] + s);
229 for (std::size_t sIdx = 0; sIdx < R; ++sIdx) {
230 T acc = zero;
231 for (std::size_t k = 0; k < M; ++k) acc += L(k, r) * T(D[k] * D[k]) * L(k, sIdx);
232 J(r, sIdx) += u[r] * acc;
233 }
234 }
235 std::vector<T> rhs(R);
236 for (std::size_t r = 0; r < R; ++r) rhs[r] = T(-g[r]);
237 const std::vector<T> du = solve(J, rhs);
238 T alpha = one;
239 for (int b = 0; b < 60; ++b) {
240 bool ok = true;
241 for (std::size_t r = 0; r < R && ok; ++r)
242 if (T(u[r] + alpha * du[r]) <= zero) ok = false;
243 if (ok) {
244 for (std::size_t k = 0; k < M && ok; ++k) {
245 T s = zero;
246 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * T(u[r] + alpha * du[r]);
247 if (s >= one) ok = false;
248 }
249 }
250 if (ok) break;
251 alpha = T(alpha / num_traits<T>::from_int(2));
252 }
253 if (num_traits<T>::to_double(alpha) < 1e-12) break;
254 for (std::size_t r = 0; r < R; ++r) u[r] = T(u[r] + alpha * du[r]);
255 }
256
257 std::vector<T> us = converged ? u : amva.XN;
258 if (converged) {
259 // Second acceptance test of the reference, on the residual at u.
260 std::vector<T> D(M);
261 for (std::size_t k = 0; k < M; ++k) {
262 T s = zero;
263 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * u[r];
264 D[k] = T(one / T(one - s));
265 }
266 T gn = zero;
267 for (std::size_t r = 0; r < R; ++r) {
268 T s = zero;
269 for (std::size_t k = 0; k < M; ++k) s += L(k, r) * D[k];
270 const T gr = T(u[r] * T(Z[r] + s) - N[r]);
271 gn += gr * gr;
272 }
273 using std::sqrt;
274 if (!(sqrt(gn) <= T(num_traits<T>::from_double(1e-8) * Ntot))) us = amva.XN;
275 }
276
277 // Assemble the expansion at us.
278 std::vector<T> Uk(M), D(M);
279 for (std::size_t k = 0; k < M; ++k) {
280 T s = zero;
281 for (std::size_t r = 0; r < R; ++r) s += L(k, r) * us[r];
282 Uk[k] = s;
283 T den = T(one - s);
284 if (den < fineTol) den = fineTol;
285 D[k] = T(one / den);
286 }
287 Matrix<T> H(R, R, zero);
288 for (std::size_t r = 0; r < R; ++r) {
289 if (us[r] == zero) throw NumericError("pfqn_kt: zero saddle-point coordinate");
290 H(r, r) = T(N[r] / T(us[r] * us[r]));
291 for (std::size_t s = 0; s < R; ++s) {
292 T acc = zero;
293 for (std::size_t k = 0; k < M; ++k) acc += L(k, r) * T(D[k] * D[k]) * L(k, s);
294 H(r, s) += acc;
295 }
296 }
297 T F = zero;
298 for (std::size_t r = 0; r < R; ++r) F += Z[r] * us[r];
299 for (std::size_t k = 0; k < M; ++k) {
300 T den = T(one - Uk[k]);
301 if (den < fineTol) den = fineTol;
302 F -= log(den);
303 }
304 for (std::size_t r = 0; r < R; ++r) F -= N[r] * log(us[r]);
305
306 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
307 T lG = F;
308 for (std::size_t r = 0; r < R; ++r) lG -= log(us[r]);
309 lG -= T(num_traits<T>::from_rational(static_cast<long>(R), 2) * log(twopi));
310 lG -= T(num_traits<T>::from_rational(1, 2) * detail::pfqn_logdet(H));
311 lG += slcdemandfactor;
312 res.lG = lG;
313 res.G = exp(lG);
314 return res;
315}
316
317template <class T>
318KtResult<T> pfqn_kt(const Matrix<T>& L, const std::vector<T>& N) {
319 return pfqn_kt(L, N, std::vector<T>());
320}
321
322} // namespace pfqn
323} // namespace line
324
325#endif // LINE_API_PFQN_PFQN_KT_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
NumericError(const std::string &what)
Definition error.h:45
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:71
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
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.
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Matrix< T > QN
(M x R) queue length
Definition pfqn_bs.h:50
Return value of pfqn_kt, mirroring [G, lG, X, Q].
Definition pfqn_kt.h:56
Matrix< T > Q
AMVA queue lengths.
Definition pfqn_kt.h:60
std::vector< T > X
AMVA throughput used to seed the saddle point.
Definition pfqn_kt.h:59