LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_le.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_LE_H
6#define LINE_API_PFQN_PFQN_LE_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Logistic expansion (LE) asymptotic approximation of the normalizing constant
12 * of a closed product-form network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_le.m (Casale, "Accelerating
15 * performance inference over closed systems by asymptotic methods",
16 * SIGMETRICS 2017), including its four local functions pfqn_le_fpi,
17 * pfqn_le_fpiZ, pfqn_le_hessian and pfqn_le_hessianZ, which are exported here
18 * because pfqn_ls needs the same mode and Hessian.
19 *
20 * The integral representation of G is mapped to the simplex by a logistic
21 * transformation and evaluated by Laplace's method at the mode u* of the
22 * transformed integrand, giving
23 *
24 * log G = multinomialln([N, M-1]) + factln(M-1) + (M-1) log sqrt(2 pi)
25 * - log sqrt(det A) + sum_i log u*_i + sum_r N_r log(u*' L(:,r))
26 *
27 * with A the Hessian at the mode, and the analogous Z > 0 form in which the
28 * mode carries an extra scale variable v*. This is Cas17 eq. (34) as published;
29 * pfqn_ble (pfqn_ble.h) adds the eps->0 bias correction derived there.
30 *
31 * ARITHMETIC. Laplace's method is an asymptotic approximation and the formula
32 * itself is a sum of logarithms, so the routine is gated on
33 * num_traits<T>::has_transcendental: it has no meaning in exact arithmetic,
34 * and instantiating it there would silently produce a value that is not the
35 * normalizing constant.
36 *
37 * FIXED POINT. The mode is found by the same 1-norm fixed-point iteration as
38 * MATLAB, stopped at 1e-10; the tolerance is a double constant converted into
39 * T, so a Real<D> instantiation iterates to the same point, not further. That
40 * is deliberate: matching MATLAB is the contract, and the Laplace error
41 * dominates the fixed-point residual by many orders of magnitude anyway.
42 */
43
44#include <cmath>
45#include <cstddef>
46#include <vector>
47
50#include "line/num/number.h"
51#include "line/util/error.h"
52#include "line/util/matrix.h"
53
54namespace line {
55namespace pfqn {
56
57/** Mode of the logistic-transformed integrand, Z = 0 case (pfqn_le_fpi). */
58template <class T>
59std::vector<T> pfqn_le_fpi(const Matrix<T>& L, const std::vector<T>& N) {
60 static_assert(num_traits<T>::has_transcendental, "pfqn_le_fpi requires transcendental arithmetic");
61 const std::size_t M = L.rows(), R = L.cols();
62 if (N.size() != R) throw InputError("pfqn_le_fpi: L and N disagree on the class count");
63 T Ntot = num_traits<T>::from_int(0);
64 for (const T& v : N) Ntot += v;
65 const T eta = T(Ntot + num_traits<T>::from_int(static_cast<long>(M)));
66 std::vector<T> u(M, T(num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(M))));
67 std::vector<T> u1(M);
68 const T tol = num_traits<T>::from_double(1e-10);
69 for (int it = 0; it < 100000; ++it) {
70 u1 = u;
71 std::vector<T> uL(R, num_traits<T>::from_int(0));
72 for (std::size_t r = 0; r < R; ++r)
73 for (std::size_t i = 0; i < M; ++i) uL[r] += u1[i] * L(i, r);
74 for (std::size_t i = 0; i < M; ++i) {
75 T ui = T(num_traits<T>::from_int(1) / eta);
76 for (std::size_t r = 0; r < R; ++r) {
77 if (uL[r] == num_traits<T>::from_int(0)) continue;
78 ui += T(N[r] / eta) * L(i, r) * u1[i] / uL[r];
79 }
80 u[i] = ui;
81 }
83 for (std::size_t i = 0; i < M; ++i) d += num_abs(T(u[i] - u1[i]));
84 if (d <= tol) break;
85 }
86 return u;
87}
88
89/** Mode of the logistic-transformed integrand, Z > 0 case (pfqn_le_fpiZ). */
90template <class T>
91void pfqn_le_fpiZ(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
92 std::vector<T>& u, T& v) {
93 static_assert(num_traits<T>::has_transcendental, "pfqn_le_fpiZ requires transcendental arithmetic");
94 const std::size_t M = L.rows(), R = L.cols();
95 if (N.size() != R || Z.size() != R)
96 throw InputError("pfqn_le_fpiZ: L, N and Z disagree on the class count");
97 T Ntot = num_traits<T>::from_int(0);
98 for (const T& x : N) Ntot += x;
99 const T eta = T(Ntot + num_traits<T>::from_int(static_cast<long>(M)));
100 u.assign(M, T(num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(M))));
101 // Note: eq. (35) in the SIGMETRICS 2017 paper has a spurious +1 in the v
102 // equation; the correct stationary point is v = eta - sum_r xi_r*Z_r.
103 v = eta;
104 const T tol = num_traits<T>::from_double(1e-10);
105 std::vector<T> u1(M);
106 for (int it = 0; it < 100000; ++it) {
107 u1 = u;
108 std::vector<T> uL(R, num_traits<T>::from_int(0));
109 for (std::size_t r = 0; r < R; ++r)
110 for (std::size_t i = 0; i < M; ++i) uL[r] += u1[i] * L(i, r);
111 for (std::size_t i = 0; i < M; ++i) {
112 T ui = T(num_traits<T>::from_int(1) / eta);
113 for (std::size_t r = 0; r < R; ++r) {
114 const T den = T(Z[r] + v * uL[r]);
115 if (den == num_traits<T>::from_int(0)) continue;
116 ui += T(N[r] / eta) * T(Z[r] + v * L(i, r)) * u1[i] / den;
117 }
118 u[i] = ui;
119 }
120 T vnew = eta;
121 for (std::size_t r = 0; r < R; ++r) {
122 const T den = T(Z[r] + v * uL[r]);
123 if (den == num_traits<T>::from_int(0)) continue;
124 vnew -= T(N[r] / den) * Z[r];
125 }
127 for (std::size_t i = 0; i < M; ++i) d += num_abs(T(u[i] - u1[i]));
128 const T dv = num_abs(T(vnew - v));
129 v = vnew;
130 if (T(d + dv) <= tol) break;
131 }
132}
133
134/** Hessian of the Z = 0 logistic integrand at the mode ((M-1) x (M-1)). */
135template <class T>
136Matrix<T> pfqn_le_hessian(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& u0) {
137 const std::size_t M = L.rows(), R = L.cols();
138 if (M < 2) throw InputError("pfqn_le_hessian: at least two stations are required");
139 T Ntot = num_traits<T>::from_int(0);
140 for (const T& x : N) Ntot += x;
141 const T eta = T(Ntot + num_traits<T>::from_int(static_cast<long>(M)));
142 std::vector<T> uL(R, num_traits<T>::from_int(0));
143 for (std::size_t r = 0; r < R; ++r)
144 for (std::size_t i = 0; i < M; ++i) uL[r] += u0[i] * L(i, r);
145
146 Matrix<T> H(M - 1, M - 1, num_traits<T>::from_int(0));
147 for (std::size_t i = 0; i + 1 < M; ++i) {
148 for (std::size_t j = 0; j + 1 < M; ++j) {
149 if (i != j) {
150 T h = T(-eta * u0[i] * u0[j]);
151 for (std::size_t r = 0; r < R; ++r)
152 h += N[r] * L(i, r) * L(j, r) * T(u0[i] * u0[j]) / T(uL[r] * uL[r]);
153 H(i, j) = h;
154 } else {
155 T rest = num_traits<T>::from_int(0);
156 for (std::size_t k = 0; k < M; ++k)
157 if (k != i) rest += u0[k];
158 T h = T(eta * u0[i] * rest);
159 for (std::size_t r = 0; r < R; ++r) {
160 T restL = num_traits<T>::from_int(0);
161 for (std::size_t k = 0; k < M; ++k)
162 if (k != i) restL += u0[k] * L(k, r);
163 h -= N[r] * L(i, r) * u0[i] * restL / T(uL[r] * uL[r]);
164 }
165 H(i, i) = h;
166 }
167 }
168 }
169 return H;
170}
171
172/** Hessian of the Z > 0 logistic integrand at the mode (M x M). */
173template <class T>
174Matrix<T> pfqn_le_hessianZ(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
175 const std::vector<T>& u, const T& v) {
176 const std::size_t K = L.rows(), R = L.cols();
177 T Ntot = num_traits<T>::from_int(0);
178 for (const T& x : N) Ntot += x;
179 const T eta = T(Ntot + num_traits<T>::from_int(static_cast<long>(K)));
180 std::vector<T> uL(R, num_traits<T>::from_int(0));
181 for (std::size_t r = 0; r < R; ++r)
182 for (std::size_t i = 0; i < K; ++i) uL[r] += u[i] * L(i, r);
183 std::vector<T> csi(R);
184 // csi2N is csi[r]*csi[r]/N[r] rewritten as N[r]/c[r]^2. Identical where both are
185 // defined, but 0 rather than 0/0 for an empty class, which oner() makes routine
186 // in the mean-value pipeline of pfqn_nc.
187 std::vector<T> csi2N(R);
188 for (std::size_t r = 0; r < R; ++r) {
189 const T c = T(Z[r] + v * uL[r]);
190 csi[r] = T(N[r] / c);
191 csi2N[r] = T(N[r] / T(c * c));
192 }
193 Matrix<T> Lhat(K, R);
194 for (std::size_t k = 0; k < K; ++k)
195 for (std::size_t r = 0; r < R; ++r) Lhat(k, r) = T(Z[r] + v * L(k, r));
196
198 for (std::size_t i = 0; i < K; ++i)
199 for (std::size_t j = 0; j < K; ++j) {
200 if (i == j) continue;
201 T a = T(-eta * u[i] * u[j]);
202 for (std::size_t r = 0; r < R; ++r)
203 a += csi2N[r] * Lhat(i, r) * Lhat(j, r) * T(u[i] * u[j]);
204 A(i, j) = a;
205 }
206 for (std::size_t i = 0; i < K; ++i) {
208 for (std::size_t j = 0; j < K; ++j)
209 if (j != i) s += A(i, j);
210 A(i, i) = T(-s);
211 }
212 // MATLAB truncates A to (K-1)x(K-1) and then writes row/column K, so the
213 // assembled matrix keeps order K with its last row and column rebuilt.
215 for (std::size_t i = 0; i + 1 < K; ++i)
216 for (std::size_t j = 0; j + 1 < K; ++j) B(i, j) = A(i, j);
217 T akk = num_traits<T>::from_int(1);
218 for (std::size_t r = 0; r < R; ++r)
219 akk -= csi2N[r] * Z[r] * uL[r];
220 B(K - 1, K - 1) = T(v * akk);
221 for (std::size_t i = 0; i + 1 < K; ++i) {
223 for (std::size_t r = 0; r < R; ++r)
224 a += v * u[i] * T(csi2N[r] * Lhat(i, r) * uL[r] - csi[r] * L(i, r));
225 B(i, K - 1) = a;
226 B(K - 1, i) = a;
227 }
228 return B;
229}
230
231/** Return value of pfqn_le, mirroring [Gn, lGn]. */
232template <class T>
233struct LeResult {
234 T G;
235 T lG;
236};
237
238/**
239 * Logistic expansion estimate of the normalizing constant.
240 *
241 * @param L (M x R) demands, @param N (R) population, @param Z (R) think times
242 * (pass an empty vector or all zeros for the Z = 0 branch)
243 */
244template <class T>
245LeResult<T> pfqn_le(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z) {
247 "pfqn_le requires transcendental arithmetic (Laplace approximation of an integral)");
248 using std::exp;
249 using std::log;
250 using std::sqrt;
251 const std::size_t M = L.rows(), R = L.cols();
252 const T zero = num_traits<T>::from_int(0);
253 LeResult<T> res;
254
255 T Ntot = zero, Lsum = zero, Zsum = zero;
256 for (const T& x : N) Ntot += x;
257 for (std::size_t i = 0; i < M; ++i)
258 for (std::size_t r = 0; r < R; ++r) Lsum += L(i, r);
259 for (const T& x : Z) Zsum += x;
260
261 // Degenerate branch: no queueing stations, the delay carries everything.
262 if (M == 0 || N.empty() || Ntot == zero ||
263 num_traits<T>::to_double(Lsum) < 1e-4) {
264 T lG = zero;
265 for (std::size_t r = 0; r < R && r < N.size(); ++r) {
266 lG -= detail::num_factln<T>(N[r]);
267 if (!Z.empty() && Z[r] > zero) lG += N[r] * log(Z[r]);
268 }
269 res.lG = lG;
270 res.G = exp(lG);
271 return res;
272 }
273
274 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
275 if (Z.empty() || num_traits<T>::to_double(Zsum) < lang::GlobalConstants::Zero) {
276 const std::vector<T> umax = pfqn_le_fpi(L, N);
277 const Matrix<T> A = pfqn_le_hessian(L, N, umax);
278 T S = zero;
279 for (std::size_t r = 0; r < R; ++r) {
280 T uL = zero;
281 for (std::size_t i = 0; i < M; ++i) uL += umax[i] * L(i, r);
282 S += N[r] * log(uL);
283 }
284 // multinomialln([N, M-1]) = factln(sum N + M-1) - sum factln(N) - factln(M-1)
285 T mln = detail::num_factln<T>(T(Ntot + num_traits<T>::from_int(static_cast<long>(M) - 1)));
286 for (std::size_t r = 0; r < R; ++r) mln -= detail::num_factln<T>(N[r]);
287 mln -= detail::num_factln<T>(num_traits<T>::from_int(static_cast<long>(M) - 1));
288 T lG = T(mln + detail::num_factln<T>(num_traits<T>::from_int(static_cast<long>(M) - 1)));
289 lG += num_traits<T>::from_int(static_cast<long>(M) - 1) * log(sqrt(twopi));
290 lG -= log(sqrt(detail::pfqn_det(A)));
291 for (std::size_t i = 0; i < M; ++i) lG += log(umax[i]);
292 lG += S;
293 res.lG = lG;
294 res.G = exp(lG);
295 return res;
296 }
297
298 std::vector<T> umax;
299 T vmax = zero;
300 pfqn_le_fpiZ(L, N, Z, umax, vmax);
301 const Matrix<T> A = pfqn_le_hessianZ(L, N, Z, umax, vmax);
302 T S = zero;
303 for (std::size_t r = 0; r < R; ++r) {
304 T uL = zero;
305 for (std::size_t i = 0; i < M; ++i) uL += umax[i] * L(i, r);
306 S += N[r] * log(T(Z[r] + vmax * uL));
307 }
308 T lG = zero;
309 for (std::size_t r = 0; r < R; ++r) lG -= detail::num_factln<T>(N[r]);
310 lG -= vmax;
311 lG += num_traits<T>::from_int(static_cast<long>(M)) * log(vmax);
312 lG += num_traits<T>::from_int(static_cast<long>(M)) * log(sqrt(twopi));
313 lG -= log(sqrt(detail::pfqn_det(A)));
314 for (std::size_t i = 0; i < M; ++i) lG += log(umax[i]);
315 lG += S;
316 res.lG = lG;
317 res.G = exp(lG);
318 return res;
319}
320
321template <class T>
322LeResult<T> pfqn_le(const Matrix<T>& L, const std::vector<T>& N) {
323 return pfqn_le(L, N, std::vector<T>());
324}
325
326} // namespace pfqn
327} // namespace line
328
329#endif // LINE_API_PFQN_PFQN_LE_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.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
Dense matrix and non-owning view.
void pfqn_le_fpiZ(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, std::vector< T > &u, T &v)
Mode of the logistic-transformed integrand, Z > 0 case (pfqn_le_fpiZ).
Definition pfqn_le.h:91
std::vector< T > pfqn_le_fpi(const Matrix< T > &L, const std::vector< T > &N)
Mode of the logistic-transformed integrand, Z = 0 case (pfqn_le_fpi).
Definition pfqn_le.h:59
Matrix< T > pfqn_le_hessianZ(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< T > &u, const T &v)
Hessian of the Z > 0 logistic integrand at the mode (M x M).
Definition pfqn_le.h:174
LeResult< T > pfqn_le(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z)
Logistic expansion estimate of the normalizing constant.
Definition pfqn_le.h:245
Matrix< T > pfqn_le_hessian(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &u0)
Hessian of the Z = 0 logistic integrand at the mode ((M-1) x (M-1)).
Definition pfqn_le.h:136
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
static constexpr double Zero
Definition lang_types.h:670
Return value of pfqn_le, mirroring [Gn, lGn].
Definition pfqn_le.h:233