LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ls.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_LS_H
6#define LINE_API_PFQN_LS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Logistic-sampling estimate of the normalizing constant of a closed
12 * product-form network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_ls.m, cross-checked against
15 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_ls.java. Reference: G. Casale,
16 * "Accelerating performance inference over closed systems by asymptotic
17 * methods", ACM SIGMETRICS 2017.
18 *
19 * The integral representation used by pfqn_le is estimated by importance
20 * sampling rather than by a Laplace expansion: the proposal is the Gaussian
21 * N(x0, A^{-1}) whose mode x0 and precision A are exactly the fixed point and
22 * Hessian that pfqn_le computes (pfqn_le_fpi / pfqn_le_hessian for Z = 0,
23 * pfqn_le_fpiZ / pfqn_le_hessianZ otherwise), and the estimate is the
24 * self-normalized average of the log-weights lr = log h(x) - log q(x). Where
25 * pfqn_le keeps only the quadratic term of the expansion, pfqn_ls corrects it
26 * by Monte Carlo, so it is the same asymptotic device made consistent.
27 *
28 * Everything stays in the log domain: exponentiating either the integrand or
29 * the normal density overflows once lG reaches a few hundred nats, and the
30 * exp(-gammaln) prefactor then underflows, which is the NaN the reference's
31 * comments record.
32 *
33 * Deviations from the reference, both deliberate:
34 *
35 * - MATLAB routes an all-zero Z to the Z > 0 branch because it tests only
36 * isempty(Z). Here an all-zero Z goes to the Z = 0 branch, which is the same
37 * integral evaluated without running the fixed point pfqn_le_fpiZ on a
38 * degenerate v. This matches the convention already used by pfqn_le in this
39 * tree, so the two stay consistent with each other.
40 * - The proposal draw is z ~ N(0,I) mapped by the Cholesky factor of A^{-1},
41 * which is what MATLAB's mvnrnd does; the factor is computed here rather
42 * than delegated, so a non-positive-definite covariance raises a numeric
43 * error instead of a library-specific one.
44 *
45 * Arithmetic: INEXACT BY CONSTRUCTION. The estimate is a random variable; the
46 * proposal is Gaussian, the mode comes from a tolerance-stopped fixed-point
47 * iteration, and every weight is a log.
48 *
49 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
50 * distribution, never stream for stream; reproducible within this port only
51 * when the generator is passed in the same state.
52 */
53
54#include <cmath>
55#include <cstddef>
56#include <limits>
57#include <vector>
58
62#include "line/num/number.h"
63#include "line/util/error.h"
64#include "line/util/linalg.h"
65#include "line/util/matrix.h"
66
67namespace line {
68namespace pfqn {
69
70/** Return value of pfqn_ls, mirroring [Gn, lGn]. */
71template <class T>
72struct LsResult {
73 T G;
74 T lG;
75};
76
77namespace detail {
78
79/**
80 * Upper-triangular Cholesky factor C of a symmetric positive-definite A, so
81 * that A = C^T C. Returns false, leaving C unspecified, when a pivot is
82 * non-positive; that is MATLAB's chol flag.
83 */
84template <class T>
85bool ls_chol_upper(const Matrix<T>& A, Matrix<T>& C) {
86 using std::sqrt;
87 const std::size_t n = A.rows();
89 for (std::size_t i = 0; i < n; ++i) {
90 for (std::size_t j = i; j < n; ++j) {
91 T s = A(i, j);
92 for (std::size_t k = 0; k < i; ++k) s -= C(k, i) * C(k, j);
93 if (i == j) {
94 if (!(s > num_traits<T>::from_int(0))) return false;
95 C(i, i) = sqrt(s);
96 } else {
97 C(i, j) = s / C(i, i);
98 }
99 }
100 }
101 return true;
102}
103
104} // namespace detail
105
106/**
107 * @brief Logistic-sampling estimate of the normalizing constant of a closed
108 * product-form network.
109 *
110 * @param L0 (M x R) demands; rows whose total demand is below 1e-4 are
111 * dropped, as in the reference
112 * @param N (R) populations
113 * @param Z (R) think times; empty or all zero selects the Z = 0 branch
114 * @param I number of importance samples
115 * @param rng explicit generator, advanced by the call
116 */
117template <class T>
118LsResult<T> pfqn_ls(const Matrix<T>& L0, const std::vector<T>& N, const std::vector<T>& Z,
119 std::size_t I, McRng& rng) {
121 "pfqn_ls requires transcendental arithmetic: it importance-samples a Gaussian "
122 "proposal centred on a tolerance-stopped fixed point and averages log-weights");
123 using std::exp;
124 using std::log;
125
126 const T zero = num_traits<T>::from_int(0);
127 const T one = num_traits<T>::from_int(1);
128 const std::size_t R = N.size();
129 if (!L0.empty() && L0.cols() != R)
130 throw InputError("pfqn_ls: L and N disagree on the class count");
131 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_ls: Z has the wrong length");
132 if (I == 0) throw InputError("pfqn_ls: at least one sample is required");
133
134 // ---- drop the stations that carry no demand ----------------------------
135 std::vector<std::size_t> keep;
136 for (std::size_t i = 0; i < L0.rows(); ++i) {
137 T s = zero;
138 for (std::size_t r = 0; r < R; ++r) s += L0(i, r);
139 if (num_traits<T>::to_double(s) > 1e-4) keep.push_back(i);
140 }
141 Matrix<T> L(keep.size(), R, zero);
142 for (std::size_t i = 0; i < keep.size(); ++i)
143 for (std::size_t r = 0; r < R; ++r) L(i, r) = L0(keep[i], r);
144 const std::size_t M = L.rows();
145
146 T Ntot = zero, Lsum = zero, Zsum = zero;
147 for (const T& x : N) Ntot += x;
148 for (std::size_t i = 0; i < M; ++i)
149 for (std::size_t r = 0; r < R; ++r) Lsum += L(i, r);
150 for (const T& x : Z) Zsum += x;
151
152 LsResult<T> res;
153 // ---- degenerate model: the delay carries everything --------------------
154 if (M == 0 || R == 0 || Ntot == zero || num_traits<T>::to_double(Lsum) < 1e-4) {
155 T lG = zero;
156 for (std::size_t r = 0; r < R; ++r) {
157 lG -= detail::num_factln<T>(N[r]);
158 if (!Z.empty() && Z[r] > zero) lG += N[r] * log(Z[r]);
159 }
160 res.lG = lG;
161 res.G = exp(lG);
162 return res;
163 }
164 if (M < 2)
165 throw InputError(
166 "pfqn_ls: at least two loaded stations are required; the logistic transform maps the "
167 "simplex to R^{M-1}, which is empty for a single station");
168
169 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
170 const bool zeroZ = Z.empty() || Zsum == zero;
171
172 // ---- proposal: mode x0 and precision A ---------------------------------
173 std::size_t d = 0;
174 std::vector<T> x0;
175 Matrix<T> A;
176 std::vector<T> umax;
177 T vmax = zero;
178 if (zeroZ) {
179 umax = pfqn_le_fpi(L, N);
180 A = pfqn_le_hessian(L, N, umax);
181 d = M - 1;
182 x0.resize(d);
183 for (std::size_t i = 0; i < d; ++i) x0[i] = log(T(umax[i] / umax[M - 1]));
184 } else {
185 pfqn_le_fpiZ(L, N, Z, umax, vmax);
186 A = pfqn_le_hessianZ(L, N, Z, umax, vmax);
187 d = M;
188 x0.resize(d);
189 for (std::size_t i = 0; i + 1 < M; ++i) x0[i] = log(T(umax[i] / umax[M - 1]));
190 x0[M - 1] = log(vmax);
191 }
192 // Symmetrize away the small asymmetries the closed-form Hessian carries.
193 const T half = num_traits<T>::from_rational(1, 2);
194 for (std::size_t i = 0; i < d; ++i)
195 for (std::size_t j = i + 1; j < d; ++j) {
196 const T m = T(A(i, j) + A(j, i)) * half;
197 A(i, j) = m;
198 A(j, i) = m;
199 }
200 const Matrix<T> iA = inverse(A);
201
202 // log|A| from its Cholesky factor, falling back to the determinant.
204 T logdetA = zero;
205 if (detail::ls_chol_upper(A, Ca)) {
206 for (std::size_t i = 0; i < d; ++i) logdetA += log(Ca(i, i));
207 logdetA *= num_traits<T>::from_int(2);
208 } else {
209 const T det = detail::pfqn_det(A);
210 logdetA = log(T(det < zero ? T(-det) : det));
211 }
212
213 Matrix<T> Ci;
214 if (!detail::ls_chol_upper(iA, Ci))
215 throw NumericError(
216 "pfqn_ls: the proposal covariance (inverse Hessian at the mode) is not positive "
217 "definite, so no Gaussian proposal exists there");
218
219 // ---- draw, evaluate the integrand and the proposal density -------------
220 std::vector<double> lr(I);
221 std::vector<T> xs(d), z(d), diff(d);
222 const T eN = num_traits<T>::from_double(1e-10) * Ntot;
223 const T eta = T(Ntot + num_traits<T>::from_int(static_cast<long>(M)) * T(one + eN));
224
225 for (std::size_t s = 0; s < I; ++s) {
226 for (std::size_t i = 0; i < d; ++i) z[i] = num_traits<T>::from_double(mc_normal01(rng));
227 // x = x0 + z C, with C upper triangular and C^T C = A^{-1}.
228 for (std::size_t j = 0; j < d; ++j) {
229 T acc = x0[j];
230 for (std::size_t i = 0; i <= j; ++i) acc += z[i] * Ci(i, j);
231 xs[j] = acc;
232 }
233
234 // ---- log of the integrand -----------------------------------------
235 T lT = zero;
236 if (zeroZ) {
237 // simplex_logfun: v = [exp(x), 1].
238 T vsum = one;
239 std::vector<T> vv(M, one);
240 for (std::size_t i = 0; i < d; ++i) {
241 vv[i] = exp(xs[i]);
242 vsum += vv[i];
243 }
244 for (std::size_t r = 0; r < R; ++r) {
245 T vl = zero;
246 for (std::size_t i = 0; i < M; ++i) vl += vv[i] * L(i, r);
247 lT += N[r] * log(vl);
248 }
249 for (std::size_t i = 0; i < d; ++i) lT += xs[i];
250 lT -= T(Ntot + num_traits<T>::from_int(static_cast<long>(M))) * log(vsum);
251 } else {
252 const T v = exp(xs[M - 1]);
253 T esum = zero;
254 std::vector<T> e(M - 1, zero);
255 for (std::size_t i = 0; i + 1 < M; ++i) {
256 e[i] = exp(xs[i]);
257 esum += e[i];
258 }
259 lT = -v;
260 lT += num_traits<T>::from_int(static_cast<long>(M)) * T(one + eN) * xs[M - 1];
261 for (std::size_t r = 0; r < R; ++r) {
262 T inner = T(L(M - 1, r) * v + Z[r]);
263 for (std::size_t i = 0; i + 1 < M; ++i) inner += e[i] * T(L(i, r) * v + Z[r]);
264 lT += N[r] * log(inner);
265 }
266 for (std::size_t i = 0; i + 1 < M; ++i) lT += xs[i];
267 lT -= eta * log(T(one + esum));
268 }
269
270 // ---- log of the proposal density, from the precision matrix --------
271 for (std::size_t i = 0; i < d; ++i) diff[i] = T(xs[i] - x0[i]);
272 T q = zero;
273 for (std::size_t i = 0; i < d; ++i) {
274 T row = zero;
275 for (std::size_t j = 0; j < d; ++j) row += diff[j] * A(j, i);
276 q += row * diff[i];
277 }
278 const T ldpdf =
279 half * logdetA - num_traits<T>::from_rational(static_cast<long>(d), 2) * log(twopi) -
280 half * q;
281 lr[s] = num_traits<T>::to_double(T(lT - ldpdf));
282 }
283
284 // ---- self-normalized average, factored by the largest weight ----------
285 const double lmean = mc_logmeanexp(lr);
286 T lG = num_traits<T>::from_double(lmean);
287 if (zeroZ) {
288 // multinomialln([N, M-1]) + factln(M-1) = factln(sum N + M - 1) - sum factln(N)
289 lG += detail::num_factln<T>(T(Ntot + num_traits<T>::from_int(static_cast<long>(M) - 1)));
290 for (std::size_t r = 0; r < R; ++r) lG -= detail::num_factln<T>(N[r]);
291 } else {
292 for (std::size_t r = 0; r < R; ++r) lG -= detail::num_factln<T>(N[r]);
293 }
294 res.lG = lG;
295 res.G = exp(lG);
296 return res;
297}
298
299/** Reference default of 1e5 samples. */
300template <class T>
301LsResult<T> pfqn_ls(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
302 McRng& rng) {
303 return pfqn_ls(L, N, Z, static_cast<std::size_t>(100000), rng);
304}
305
306} // namespace pfqn
307} // namespace line
308
309#endif // LINE_API_PFQN_LS_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.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_logmeanexp(const std::vector< double > &v)
log(mean(exp(v))), computed by factoring out the maximum so that the exponentials stay in range.
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
LsResult< T > pfqn_ls(const Matrix< T > &L0, const std::vector< T > &N, const std::vector< T > &Z, std::size_t I, McRng &rng)
Logistic-sampling estimate of the normalizing constant of a closed product-form network.
Definition pfqn_ls.h:118
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
double mc_normal01(McRng &g)
Standard normal deviate by the Box-Muller transform.
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
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Logistic expansion (LE) asymptotic approximation of the normalizing constant of a closed product-form...
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Return value of pfqn_ls, mirroring [Gn, lGn].
Definition pfqn_ls.h:72