LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
laplaceapprox.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_LAPLACEAPPROX_H
6#define LINE_API_PFQN_LAPLACEAPPROX_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Laplace approximation of a multidimensional integral around a given point.
12 *
13 * Templated port of matlab/src/api/pfqn/laplaceapprox.m together with the two
14 * thirdparty helpers it calls, matlab/lib/thirdparty/num_hess.m and num_grad.m
15 * (central differences at step h, the Hessian built as a difference of
16 * gradients). Those helpers have no pfqn counterpart of their own, so they are
17 * ported here as num_grad / num_hess in the pfqn::detail namespace rather than
18 * given a header of their own.
19 *
20 * I = h(x0) sqrt((2 pi)^d / det(-H)), H = Hessian of log h at x0.
21 *
22 * MATLAB's retry ladder on a negative det(-H) -- widen the differentiation
23 * step from 1e-5 to 1e-4 to 1e-3, then warn and carry on -- is reproduced
24 * exactly, including the fact that the final value is still returned when the
25 * determinant stays negative. Callers (pfqn_nrl, pfqn_nrp) take the real part
26 * of the logarithm afterwards, which is what makes that path survivable.
27 *
28 * logI IS the logarithm of I. laplaceapprox.m returns
29 * logI = log(h(x0)) + (d/2) log(2 pi) - (1/2) log(det(-H)), i.e. exactly
30 * log(I): the curvature term carries HALF the log-determinant, because I
31 * carries sqrt(det(-H)) in its denominator. That is worth stating explicitly
32 * because the two are easy to desynchronize -- the reference itself carried a
33 * full log(detnH) at one point, which shifted every pfqn_nrl and pfqn_nrp
34 * value by (1/2) log det(-H), and pfqn_nrl / pfqn_nrp read logI, not I.
35 *
36 * ARITHMETIC. Both the (2 pi)^{d/2} factor and the finite-difference Hessian
37 * are inexact, so the routine is gated on num_traits<T>::has_transcendental.
38 */
39
40#include <cmath>
41#include <cstddef>
42#include <functional>
43#include <vector>
44
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace pfqn {
52
53namespace detail {
54
55/** Central-difference gradient at step h (matlab/lib/thirdparty/num_grad.m). */
56template <class T>
57std::vector<T> num_grad(const std::function<T(const std::vector<T>&)>& f, const std::vector<T>& X,
58 const T& h) {
59 std::vector<T> df(X.size());
60 for (std::size_t i = 0; i < X.size(); ++i) {
61 std::vector<T> x1 = X, x2 = X;
62 x1[i] = T(X[i] - h);
63 x2[i] = T(X[i] + h);
64 df[i] = T(T(f(x2) - f(x1)) / T(num_traits<T>::from_int(2) * h));
65 }
66 return df;
67}
68
69/** Central-difference Hessian at step h (matlab/lib/thirdparty/num_hess.m). */
70template <class T>
71Matrix<T> num_hess(const std::function<T(const std::vector<T>&)>& f, const std::vector<T>& X,
72 const T& h) {
73 const std::size_t n = X.size();
74 Matrix<T> H(n, n, num_traits<T>::from_int(0));
75 for (std::size_t i = 0; i < n; ++i) {
76 std::vector<T> x1 = X, x2 = X;
77 x1[i] = T(X[i] - h);
78 x2[i] = T(X[i] + h);
79 const std::vector<T> df1 = num_grad<T>(f, x1, h);
80 const std::vector<T> df2 = num_grad<T>(f, x2, h);
81 for (std::size_t j = 0; j < n; ++j)
82 H(i, j) = T(T(df2[j] - df1[j]) / T(num_traits<T>::from_int(2) * h));
83 }
84 return H;
85}
86
87} // namespace detail
88
89/** Return value of laplaceapprox, mirroring [I, H, logI]. */
90template <class T>
92 T I;
95 bool detNegative; ///< true when det(-H) stayed negative (MATLAB warns)
96};
97
98/**
99 * @brief Laplace approximation of a multidimensional integral around a given
100 * point.
101 *
102 * @param h integrand, evaluated as a callable on a d-vector
103 * @param x0 expansion point
104 */
105template <class T>
106LaplaceResult<T> laplaceapprox(const std::function<T(const std::vector<T>&)>& h,
107 const std::vector<T>& x0) {
109 "laplaceapprox requires transcendental arithmetic (log, sqrt, (2 pi)^{d/2})");
110 using std::log;
111 using std::sqrt;
112 const std::size_t d = x0.size();
113 if (d == 0) throw InputError("laplaceapprox: empty expansion point");
114 const std::function<T(const std::vector<T>&)> logh = [&h](const std::vector<T>& x) {
115 using std::log;
116 return T(log(h(x)));
117 };
118
119 const double steps[3] = {1e-5, 1e-4, 1e-3};
120 Matrix<T> H;
121 T detnH = num_traits<T>::from_int(0);
122 bool neg = true;
123 for (int k = 0; k < 3; ++k) {
124 H = detail::num_hess<T>(logh, x0, num_traits<T>::from_double(steps[k]));
125 Matrix<T> nH(d, d);
126 for (std::size_t i = 0; i < d; ++i)
127 for (std::size_t j = 0; j < d; ++j) nH(i, j) = T(-H(i, j));
128 detnH = detail::pfqn_det(nH);
129 if (detnH >= num_traits<T>::from_int(0)) {
130 neg = false;
131 break;
132 }
133 }
134
136 r.H = H;
137 r.detNegative = neg;
138 const T twopi = num_traits<T>::from_double(6.283185307179586476925286766559);
139 const T h0 = h(x0);
140 // negative det(-H) rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
141 const T adet = num_abs(detnH);
142 if (adet == num_traits<T>::from_int(0)) throw NumericError("laplaceapprox: singular Hessian");
143 r.I = neg ? num_traits<T>::from_int(0)
144 : T(h0 * sqrt(T(num_pow_int(twopi, static_cast<unsigned>(d)) / detnH)));
145 r.logI = T(log(h0) + T(num_traits<T>::from_rational(static_cast<long>(d), 2) * log(twopi)) -
146 T(num_traits<T>::from_rational(1, 2) * log(adet)));
147 return r;
148}
149
150} // namespace pfqn
151} // namespace line
152
153#endif // LINE_API_PFQN_LAPLACEAPPROX_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.
LaplaceResult< T > laplaceapprox(const std::function< T(const std::vector< T > &)> &h, const std::vector< T > &x0)
Laplace approximation of a multidimensional integral around a given point.
T num_abs(const T &v)
Definition number.h:172
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Return value of laplaceapprox, mirroring [I, H, logI].
bool detNegative
true when det(-H) stayed negative (MATLAB warns)