LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_asympt_common.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_ASYMPT_COMMON_H
6#define LINE_API_PFQN_ASYMPT_COMMON_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Shared scalar machinery for the integration / asymptotic members of the
12 * pfqn family (pfqn_le, pfqn_lap, pfqn_ls, pfqn_cub, pfqn_kt, pfqn_panacea,
13 * the McKenna-Mitra quadratures, pfqn_nrl / pfqn_nrp).
14 *
15 * Nothing here mirrors a MATLAB file of its own: MATLAB gets log-gamma from
16 * gammaln, determinants from det, quadrature nodes from a .mat table and
17 * complex arithmetic from the language. Those four facilities have to be
18 * supplied explicitly in a templated port, and putting them in one header
19 * keeps each ported algorithm a 1:1 image of its MATLAB source.
20 *
21 * ARITHMETIC. Every function here is inherently inexact -- log-gamma, the
22 * Gauss node polynomials and the complex exponential all leave the rationals
23 * -- so each is gated on num_traits<T>::has_transcendental. The determinant
24 * is the exception: it is a finite sequence of field operations and is
25 * therefore left exact and ungated, so a bound or a Hessian determinant can
26 * still be evaluated in rational arithmetic.
27 *
28 * PRECISION CEILING of num_lgamma. For an integer argument the value is the
29 * exact sum log 1 + ... + log n, accumulated in T, so a Real<D> instantiation
30 * gains the full D digits. For a non-integer argument the Lanczos g=7
31 * coefficients are double constants, which caps the relative accuracy near
32 * 1e-15 whatever T is. Only pfqn_propfair evaluates log-gamma off the
33 * integers, and its own optimizer tolerance is far above that ceiling.
34 */
35
36#include <cmath>
37#include <complex>
38#include <cstddef>
39#include <limits>
40#include <vector>
41
42#include "line/num/number.h"
43#include "line/util/error.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace pfqn {
48namespace detail {
49
50// ---------------------------------------------------------------------------
51// Determinant (exact in a field, hence ungated)
52// ---------------------------------------------------------------------------
53
54/** Determinant by Gaussian elimination with partial pivoting; 0 if singular. */
55template <class T>
56T pfqn_det(Matrix<T> A) {
57 const std::size_t n = A.rows();
58 if (A.cols() != n) throw InputError("pfqn_det: matrix is not square");
59 const T zero = num_traits<T>::from_int(0);
60 T d = num_traits<T>::from_int(1);
61 for (std::size_t k = 0; k < n; ++k) {
62 std::size_t p = k;
63 T amax = num_abs(A(k, k));
64 for (std::size_t i = k + 1; i < n; ++i) {
65 const T a = num_abs(A(i, k));
66 if (a > amax) {
67 amax = a;
68 p = i;
69 }
70 }
71 if (amax == zero) return zero;
72 if (p != k) {
73 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(p, j));
74 d = -d;
75 }
76 d *= A(k, k);
77 for (std::size_t i = k + 1; i < n; ++i) {
78 const T f = A(i, k) / A(k, k);
79 for (std::size_t j = k; j < n; ++j) A(i, j) -= f * A(k, j);
80 }
81 }
82 return d;
83}
84
85/**
86 * Logarithm of |det A| accumulated over the pivots of the same elimination.
87 *
88 * det(A) of an R x R Hessian leaves double range well before its logarithm does
89 * (it overflowed at R = 64 in pfqn_kt, turning lG into -inf), so callers that
90 * only need log det must never form the determinant first.
91 */
92template <class T>
93T pfqn_logdet(Matrix<T> A) {
94 static_assert(num_traits<T>::has_transcendental,
95 "pfqn_logdet requires transcendental arithmetic (logarithm)");
96 using std::log;
97 const std::size_t n = A.rows();
98 if (A.cols() != n) throw InputError("pfqn_logdet: matrix is not square");
99 const T zero = num_traits<T>::from_int(0);
100 T acc = num_traits<T>::from_int(0);
101 for (std::size_t k = 0; k < n; ++k) {
102 std::size_t p = k;
103 T amax = num_abs(A(k, k));
104 for (std::size_t i = k + 1; i < n; ++i) {
105 const T a = num_abs(A(i, k));
106 if (a > amax) {
107 amax = a;
108 p = i;
109 }
110 }
111 if (amax == zero) throw InputError("pfqn_logdet: matrix is singular");
112 if (p != k) {
113 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(p, j));
114 }
115 acc += log(num_abs(A(k, k)));
116 for (std::size_t i = k + 1; i < n; ++i) {
117 const T f = A(i, k) / A(k, k);
118 for (std::size_t j = k; j < n; ++j) A(i, j) -= f * A(k, j);
119 }
120 }
121 return acc;
122}
123
124// ---------------------------------------------------------------------------
125// log-gamma / factln
126// ---------------------------------------------------------------------------
127
128/** log(n!) accumulated in T; exact up to the rounding of each log(k). */
129template <class T>
130T num_logfact_int(long n) {
131 static_assert(num_traits<T>::has_transcendental,
132 "num_logfact_int requires transcendental arithmetic (logarithm)");
133 if (n < 0) throw InputError("num_logfact_int: negative argument");
134 using std::log;
135 T s = num_traits<T>::from_int(0);
136 for (long k = 2; k <= n; ++k) s += log(num_traits<T>::from_int(k));
137 return s;
138}
139
140/** log Gamma(x) for x > 0, Lanczos g = 7 (see the precision note above). */
141template <class T>
142T num_lgamma(const T& x) {
143 static_assert(num_traits<T>::has_transcendental,
144 "num_lgamma requires transcendental arithmetic");
145 using std::log;
146 using std::sin;
147 using std::sqrt;
148 const T zero = num_traits<T>::from_int(0);
149 if (x <= zero) throw InputError("num_lgamma: argument must be positive");
150 // Integer arguments take the exact route, which is what every pfqn caller
151 // but pfqn_propfair needs and which carries the full precision of T.
152 const double xd = num_traits<T>::to_double(x);
153 const double rn = std::floor(xd + 0.5);
154 if (rn >= 1.0 && rn <= 1e6 && x == num_traits<T>::from_int(static_cast<long>(rn)))
155 return num_logfact_int<T>(static_cast<long>(rn) - 1);
156
157 static const double g[9] = {0.99999999999980993, 676.5203681218851, -1259.1392167224028,
158 771.32342877765313, -176.61502916214059, 12.507343278686905,
159 -0.13857109526572012, 9.9843695780195716e-6,
160 1.5056327351493116e-7};
161 const T one = num_traits<T>::from_int(1);
162 const T z = T(x - one);
163 T a = num_traits<T>::from_double(g[0]);
164 for (int i = 1; i < 9; ++i)
165 a += num_traits<T>::from_double(g[i]) / T(z + num_traits<T>::from_int(i));
166 const T t = T(z + num_traits<T>::from_double(7.5));
167 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
168 return T(num_traits<T>::from_rational(1, 2) * log(twopi) + T(z + num_traits<T>::from_rational(1, 2)) * log(t) -
169 t + log(a));
170}
171
172/** MATLAB factln(n) = gammaln(1+n). */
173template <class T>
174T num_factln(const T& n) {
175 return num_lgamma<T>(T(n + num_traits<T>::from_int(1)));
176}
177
178/** log(sum_i exp(v_i)), shifted by the maximum so no term overflows. */
179template <class T>
180T logsumexp(const std::vector<T>& v) {
181 static_assert(num_traits<T>::has_transcendental,
182 "logsumexp requires transcendental arithmetic");
183 using std::exp;
184 using std::log;
185 if (v.empty()) throw InputError("logsumexp: empty argument");
186 T m = v[0];
187 for (const T& x : v)
188 if (x > m) m = x;
189 // An all -inf input has no finite logarithm; return the maximum unchanged.
190 if (!(num_traits<T>::to_double(m) > -std::numeric_limits<double>::infinity())) return m;
191 T s = num_traits<T>::from_int(0);
192 for (const T& x : v) s += exp(T(x - m));
193 return T(m + log(s));
194}
195
196// ---------------------------------------------------------------------------
197// Gauss quadrature nodes, generated rather than tabulated
198// ---------------------------------------------------------------------------
199
200/**
201 * The first `count` nodes and weights of the n-point Gauss-Legendre rule on
202 * [a,b], by Newton iteration on the Legendre polynomial with the standard
203 * w = 2/((1-x^2) P'^2). `count` = 0 returns the whole rule.
204 *
205 * MATLAB's pfqn_mmint2_gausslegendre loads a table generated once in Julia by
206 * the Golub-Welsch tridiagonal eigenvalue method -- and then uses only its
207 * LEADING ENTRIES: the table is a 20000-point rule on [0, 1e6]
208 * (matlab/src/api/pfqn/gausslegendre-nodes.txt, first node 0.00361, last
209 * 999999.98), and the routine takes nodes 1..n with n = max(300, ...). Taking
210 * a prefix of a Gauss rule is not itself a Gauss rule; what makes it work is
211 * that the McKenna-Mitra integrand carries e^{-u}, so the first 300 nodes of
212 * the 20000-point rule already span [0.0036, 557.8] and everything beyond is
213 * below 1e-240. Regenerating a genuine 300-point rule on [0, 1e6] instead
214 * would put the FIRST node at u = 13.7 and miss the mass entirely -- it
215 * returns log G = -3.44 where the answer is 1.63 -- so the prefix is not an
216 * implementation detail of the reference but part of its definition.
217 *
218 * The table cannot be carried across arithmetics without pinning every
219 * instantiation to the precision it was generated at, so the rule is
220 * regenerated in T; only the requested prefix is computed, since each Newton
221 * iteration is independent of the other nodes.
222 */
223template <class T>
224void gauss_legendre(std::size_t n, const T& a, const T& b, std::vector<T>& x, std::vector<T>& w,
225 std::size_t count = 0) {
226 static_assert(num_traits<T>::has_transcendental,
227 "gauss_legendre requires transcendental arithmetic (cos, sqrt)");
228 using std::cos;
229 x.assign(n, num_traits<T>::from_int(0));
230 w.assign(n, num_traits<T>::from_int(0));
231 const T one = num_traits<T>::from_int(1);
232 const T two = num_traits<T>::from_int(2);
233 const T half = num_traits<T>::from_rational(1, 2);
234 const T pi = num_traits<T>::from_double(3.14159265358979323846264338328);
235 const T xm = T(half * T(b + a));
236 const T xl = T(half * T(b - a));
237 std::size_t m = (n + 1) / 2;
238 if (count > 0 && count < m) m = count; // only the requested prefix
239 for (std::size_t i = 0; i < m; ++i) {
240 // Tricomi's asymptotic start, then Newton to the precision of T.
241 T z = cos(T(pi * T(num_traits<T>::from_int(static_cast<long>(i) + 1) - num_traits<T>::from_rational(1, 4)) /
242 T(num_traits<T>::from_int(static_cast<long>(n)) + half)));
243 T pp = one;
244 for (int it = 0; it < 200; ++it) {
245 T p1 = one, p2 = num_traits<T>::from_int(0);
246 for (std::size_t j = 0; j < n; ++j) {
247 const T p3 = p2;
248 p2 = p1;
249 const T jj = num_traits<T>::from_int(static_cast<long>(j) + 1);
250 p1 = T(T(T(two * jj - one) * z * p2 - T(jj - one) * p3) / jj);
251 }
252 pp = T(num_traits<T>::from_int(static_cast<long>(n)) * T(z * p1 - p2) / T(z * z - one));
253 const T dz = T(p1 / pp);
254 z -= dz;
255 if (num_traits<T>::to_double(num_abs(T(dz))) < 1e-40) break;
256 }
257 x[i] = T(xm - xl * z);
258 x[n - 1 - i] = T(xm + xl * z);
259 const T wi = T(two * xl / T(T(one - z * z) * pp * pp));
260 w[i] = wi;
261 w[n - 1 - i] = wi;
262 }
263}
264
265/**
266 * n-point Gauss-Laguerre rule for weight exp(-x) on [0,inf), by Newton
267 * iteration on the Laguerre polynomial (Numerical Recipes gaulag with
268 * alpha = 0). The returned weights are the classical ones, i.e. they already
269 * carry the exp(-x) factor, exactly like MATLAB's tabulated pair.
270 */
271template <class T>
272void gauss_laguerre(std::size_t n, std::vector<T>& x, std::vector<T>& w) {
273 static_assert(num_traits<T>::has_transcendental,
274 "gauss_laguerre requires transcendental arithmetic");
275 using std::exp;
276 using std::log;
277 x.assign(n, num_traits<T>::from_int(0));
278 w.assign(n, num_traits<T>::from_int(0));
279 const T one = num_traits<T>::from_int(1);
280 const T nT = num_traits<T>::from_int(static_cast<long>(n));
281 T z = num_traits<T>::from_int(0);
282 for (std::size_t i = 0; i < n; ++i) {
283 if (i == 0) {
284 z = num_traits<T>::from_double(3.0) / T(one + num_traits<T>::from_double(2.4) * nT);
285 } else if (i == 1) {
286 z += num_traits<T>::from_double(15.0) / T(one + num_traits<T>::from_double(2.5) * nT);
287 } else {
288 const T ai = num_traits<T>::from_int(static_cast<long>(i) - 1);
289 z += T(one + num_traits<T>::from_double(2.55) * ai) /
290 T(num_traits<T>::from_double(1.9) * ai) * T(z - x[i - 2]);
291 }
292 T pp = one, p2 = num_traits<T>::from_int(0);
293 for (int it = 0; it < 300; ++it) {
294 T p1 = one;
295 p2 = num_traits<T>::from_int(0);
296 for (std::size_t j = 0; j < n; ++j) {
297 const T p3 = p2;
298 p2 = p1;
299 const T jj = num_traits<T>::from_int(static_cast<long>(j) + 1);
300 p1 = T(T(T(num_traits<T>::from_int(2 * static_cast<long>(j) + 1) - z) * p2 -
301 T(jj - one) * p3) /
302 jj);
303 }
304 // After the loop p1 = L_n(z) and p2 = L_{n-1}(z).
305 pp = T(T(nT * p1 - nT * p2) / z);
306 const T dz = T(p1 / pp);
307 z -= dz;
308 if (num_traits<T>::to_double(num_abs(T(dz))) < 1e-40) break;
309 }
310 x[i] = z;
311 // Numerical Recipes weight-form rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
312 w[i] = T(-one / T(nT * pp * p2));
313 }
314}
315
316// ---------------------------------------------------------------------------
317// The reference's Inf in a field that may not have one
318// ---------------------------------------------------------------------------
319
320/**
321 * MATLAB writes Inf for a rate or a balance value that is undefined (a zero
322 * denominator, a NaN, a magnitude beyond 1e15). An exact rational field has no
323 * infinity, so there the marker is 0 instead, which is unambiguous: a real
324 * load-dependent service rate is never zero, and a station whose rate is zero
325 * cannot serve, which is precisely what Inf encodes on the reciprocal side.
326 * Callers must test with is_inf_marker rather than with isfinite.
327 */
328template <class T>
329T num_inf_marker() {
330 if constexpr (num_traits<T>::is_exact) {
331 return num_traits<T>::from_int(0);
332 } else {
333 return num_traits<T>::from_double(std::numeric_limits<double>::infinity());
334 }
335}
336
337/** Companion test for num_inf_marker. */
338template <class T>
339bool is_inf_marker(const T& v) {
340 if constexpr (num_traits<T>::is_exact) {
341 return v == num_traits<T>::from_int(0);
342 } else {
343 return !std::isfinite(num_traits<T>::to_double(v));
344 }
345}
346
347// ---------------------------------------------------------------------------
348// Complex arithmetic over T, for the Norlund-Rice inversion integrands
349// ---------------------------------------------------------------------------
350
351/** Minimal complex over T. std::complex<T> is only defined for the built-ins. */
352template <class T>
353struct Cx {
354 T re, im;
355 Cx() : re(num_traits<T>::from_int(0)), im(num_traits<T>::from_int(0)) {}
356 Cx(const T& r, const T& i) : re(r), im(i) {}
357 explicit Cx(const T& r) : re(r), im(num_traits<T>::from_int(0)) {}
358};
359
360template <class T>
361Cx<T> cx_add(const Cx<T>& a, const Cx<T>& b) {
362 return Cx<T>(T(a.re + b.re), T(a.im + b.im));
363}
364
365template <class T>
366Cx<T> cx_mul(const Cx<T>& a, const Cx<T>& b) {
367 return Cx<T>(T(a.re * b.re - a.im * b.im), T(a.re * b.im + a.im * b.re));
368}
369
370template <class T>
371Cx<T> cx_scale(const Cx<T>& a, const T& s) {
372 return Cx<T>(T(a.re * s), T(a.im * s));
373}
374
375template <class T>
376Cx<T> cx_div(const Cx<T>& a, const Cx<T>& b) {
377 const T d = T(b.re * b.re + b.im * b.im);
378 if (d == num_traits<T>::from_int(0)) throw NumericError("cx_div: division by zero");
379 return Cx<T>(T(T(a.re * b.re + a.im * b.im) / d), T(T(a.im * b.re - a.re * b.im) / d));
380}
381
382/** exp(i theta). */
383template <class T>
384Cx<T> cx_expi(const T& theta) {
385 static_assert(num_traits<T>::has_transcendental,
386 "cx_expi requires transcendental arithmetic");
387 using std::cos;
388 using std::sin;
389 return Cx<T>(cos(theta), sin(theta));
390}
391
392} // namespace detail
393} // namespace pfqn
394} // namespace line
395
396#endif // LINE_API_PFQN_ASYMPT_COMMON_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.