LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
retrieval_rayint.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_RETRIEVAL_RETRIEVAL_RAYINT_H
6#define LINE_API_RETRIEVAL_RETRIEVAL_RAYINT_H
7
8/**
9 * @file
10 * @ingroup api_retrieval
11 * Ray (WKB) asymptotic expansion of the list-based cache normalizing constant.
12 *
13 * Templated port of matlab/src/api/retrieval/retrieval_rayint.m, cross-checked
14 * against jar/src/main/java/jline/api/retrieval/Retrieval_rayint.java.
15 *
16 * Approximates the constant cache_erec computes exactly, in the SAME
17 * normalization, so the two are interchangeable:
18 *
19 * E(m,n) = E(m,n-1) + sum_j gamma_{n,j} m_j E(m-1_j,n-1), E(0,0)=1
20 *
21 * Writing E = prod_j m_j! * Et, the relaxation Et ~ H exp(phi/eps) with
22 * n = y/eps and m_j = x_j/eps gives the eikonal
23 * e^{phi_y} = 1 + sum_j gamma_j(y) e^{-phi_j}, whose rays carry the constants
24 * xi_j = e^{-phi_j}. With S(v) = 1 + sum_j gamma_j(v) xi_j,
25 *
26 * x_j = int_0^y gamma_j(v) xi_j / S(v) dv (the saddle conditions)
27 * phi = int_0^y log S(v) dv - sum_j x_j log xi_j
28 * H = (2 pi)^{-h/2} sqrt(S(y)/S(0)) / sqrt(prod_j xi_j * det A)
29 * A_{ik} = d x_i / d xi_k
30 *
31 * and E ~ prod_j m_j! * eps^{h/2} H exp(phi/eps).
32 *
33 * DISCRETE (gamma an n x h matrix). The ray integrals are the sums they
34 * discretize and the expansion collapses to the Laplace form
35 *
36 * Et ~ (2 pi)^{-h/2} exp(sum_k log D_k - sum_j m_j log xi_j) / sqrt(det Sigma)
37 *
38 * with D_k = 1 + sum_j gamma_{k,j} xi_j, sum_k gamma_{k,j} xi_j / D_k = m_j and
39 * Sigma = A * diag(xi) the Hessian in log xi. This is the more accurate of the
40 * two forms; the sqrt(S(y)/S(0)) factor is exactly the Euler-Maclaurin term
41 * relating sum_k to int dv and is already accounted for.
42 *
43 * CONTINUUM (gamma a callable profile on v in [0,1]). Composite Simpson
44 * quadrature on the profile itself, the form written in the note. Costs roughly
45 * a factor two in accuracy but does not need the n rows.
46 *
47 * ACCURACY. The relative error is O(1/n) at fixed occupancy but is governed by
48 * the smallest occupancy rather than by n, tracking
49 * 0.14 * (1/min_j m_j + 1/(n - sum_j m_j)), so a per cent needs every m_j and
50 * n - sum_j m_j above about 15 and a part in a thousand needs them above about
51 * 150. Returned in the result's relerr_est. Lists with m_j = 0 contribute
52 * nothing and are dropped before the saddle is solved.
53 *
54 * ARITHMETIC: the expansion is a Laplace approximation built out of logs, exps
55 * and a square root, so it is meaningless at exact arithmetic and is gated on
56 * has_transcendental. It is also an APPROXIMATION whatever the arithmetic --
57 * widening the type sharpens the saddle solve, never the O(1/n) model error.
58 * Use cache_erec or retrieval_nc when the exact constant is wanted.
59 *
60 * This is the no-fetch (q=0) case, i.e. the same quantity as cache_erec. The
61 * delayed-hit extension carrying the fetch coordinates is NOT implemented: its
62 * eikonal is known but its amplitude has not been derived.
63 */
64
65#include <cmath>
66#include <cstddef>
67#include <functional>
68#include <string>
69#include <vector>
70
71#include "line/num/number.h"
72#include "line/util/error.h"
73#include "line/util/matrix.h"
74
75namespace line {
76namespace retrieval {
77
78/** Outcome of the expansion. */
79template <class T>
81 /** Normalizing constant, same normalization as cache_erec (may overflow; use log_e). */
82 T e;
83 /** Natural logarithm of e, safe for large n. */
85 /** Saddle point xi_j, one entry per list (0 for a list of zero capacity). */
86 std::vector<T> xi;
87 /** log det of the Hessian in log xi. */
89 /** Estimated relative error, 0.14*(1/min_j m_j + 1/(n - sum_j m_j)). */
90 double relerr_est = 0.0;
91 /** "saddle", "rayint" or "boundary". */
92 std::string method;
93 /** Newton iterations used. */
94 std::size_t iterations = 0;
95};
96
97namespace detail {
98
99/** Hessian in theta = log xi; equals A*diag(xi) with A_{ik} = d x_i / d xi_k. */
100template <class T>
101Matrix<T> rayint_hessian(const Matrix<T>& g, const std::vector<T>& w, const std::vector<T>& xi) {
102 const std::size_t K = g.rows();
103 const std::size_t h = xi.size();
104 const T one = num_traits<T>::from_int(1);
105 Matrix<T> hess(h, h, num_traits<T>::from_int(0));
106 std::vector<T> a(h);
107 for (std::size_t k = 0; k < K; ++k) {
108 T s = one;
109 for (std::size_t j = 0; j < h; ++j) {
110 a[j] = g(k, j) * xi[j];
111 s += a[j];
112 }
113 for (std::size_t j = 0; j < h; ++j) {
114 const T aj = a[j] / s;
115 hess(j, j) += w[k] * aj;
116 for (std::size_t l = 0; l < h; ++l) hess(j, l) -= w[k] * aj * (a[l] / s);
117 }
118 }
119 return hess;
120}
121
122/** Gaussian elimination with partial pivoting; h is the list count, so tiny. */
123template <class T>
124std::vector<T> rayint_solve(const Matrix<T>& a, const std::vector<T>& b) {
125 using std::abs;
126 const std::size_t h = b.size();
127 Matrix<T> m(h, h + 1);
128 for (std::size_t i = 0; i < h; ++i) {
129 for (std::size_t j = 0; j < h; ++j) m(i, j) = a(i, j);
130 m(i, h) = b[i];
131 }
132 for (std::size_t c = 0; c < h; ++c) {
133 std::size_t piv = c;
134 for (std::size_t i = c + 1; i < h; ++i)
135 if (abs(m(i, c)) > abs(m(piv, c))) piv = i;
136 if (num_traits<T>::to_double(abs(m(piv, c))) == 0.0)
137 throw NumericError("retrieval_rayint: the saddle-point Hessian is singular; the ray map "
138 "is degenerate here");
139 if (piv != c)
140 for (std::size_t j = 0; j <= h; ++j) {
141 const T tmp = m(c, j);
142 m(c, j) = m(piv, j);
143 m(piv, j) = tmp;
144 }
145 for (std::size_t i = c + 1; i < h; ++i) {
146 const T f = m(i, c) / m(c, c);
147 for (std::size_t j = c; j <= h; ++j) m(i, j) -= f * m(c, j);
148 }
149 }
150 std::vector<T> x(h);
151 for (std::size_t ii = h; ii-- > 0;) {
152 T s = m(ii, h);
153 for (std::size_t j = ii + 1; j < h; ++j) s -= m(ii, j) * x[j];
154 x[ii] = s / m(ii, ii);
155 }
156 return x;
157}
158
159/** log det via Cholesky; the Hessian of a strictly convex objective is positive definite. */
160template <class T>
161T rayint_logdet(const Matrix<T>& a) {
162 using std::log;
163 using std::sqrt;
164 const std::size_t h = a.rows();
165 const T half = num_traits<T>::from_rational(1, 2);
166 const T two = num_traits<T>::from_int(2);
167 Matrix<T> l(h, h, num_traits<T>::from_int(0));
168 T ld = num_traits<T>::from_int(0);
169 for (std::size_t i = 0; i < h; ++i) {
170 for (std::size_t j = 0; j <= i; ++j) {
171 T s = half * (a(i, j) + a(j, i));
172 for (std::size_t k = 0; k < j; ++k) s -= l(i, k) * l(j, k);
173 if (i == j) {
174 if (num_traits<T>::to_double(s) <= 0.0)
175 throw NumericError("retrieval_rayint: the saddle-point Hessian is not positive "
176 "definite; the ray map is singular here");
177 l(i, j) = sqrt(s);
178 ld += two * log(l(i, j));
179 } else {
180 l(i, j) = s / l(j, j);
181 }
182 }
183 }
184 return ld;
185}
186
187/**
188 * Newton on theta = log xi for sum_k w_k g_{k,j} xi_j / (1 + sum_l g_{k,l} xi_l) = tgt_j.
189 * The objective sum_k w_k log(1 + sum_l g_{k,l} e^{theta_l}) - tgt.theta is strictly
190 * convex, so the root is unique and damped Newton converges globally.
191 */
192template <class T>
193std::vector<T> rayint_saddle(const Matrix<T>& g, const std::vector<T>& w,
194 const std::vector<T>& tgt, std::size_t& iterations) {
195 using std::abs;
196 using std::exp;
197 using std::log;
198 const std::size_t K = g.rows();
199 const std::size_t h = tgt.size();
200 const T one = num_traits<T>::from_int(1);
201
202 T wsum = num_traits<T>::from_int(0);
203 for (std::size_t k = 0; k < K; ++k) wsum += w[k];
204 T tsum = num_traits<T>::from_int(0);
205 for (std::size_t j = 0; j < h; ++j) tsum += tgt[j];
206 T slack = one - tsum / wsum;
207 const T tiny = num_traits<T>::from_double(1e-9);
208 if (num_traits<T>::to_double(slack) < 1e-9) slack = tiny;
209
210 std::vector<T> th(h);
211 for (std::size_t j = 0; j < h; ++j) {
212 T gb = num_traits<T>::from_int(0);
213 for (std::size_t k = 0; k < K; ++k) gb += w[k] * g(k, j);
214 th[j] = log(tgt[j] / (gb * slack));
215 }
216
217 double tmax = 1.0;
218 for (std::size_t j = 0; j < h; ++j)
219 tmax = std::max(tmax, std::abs(num_traits<T>::to_double(tgt[j])));
220
221 std::vector<T> xi(h);
222 std::size_t it = 0;
223 for (it = 1; it <= 200; ++it) {
224 for (std::size_t j = 0; j < h; ++j) xi[j] = exp(th[j]);
225 std::vector<T> grad(h, num_traits<T>::from_int(0));
226 for (std::size_t k = 0; k < K; ++k) {
227 T s = one;
228 for (std::size_t j = 0; j < h; ++j) s += g(k, j) * xi[j];
229 for (std::size_t j = 0; j < h; ++j) grad[j] += w[k] * g(k, j) * xi[j] / s;
230 }
231 double gmax = 0.0;
232 for (std::size_t j = 0; j < h; ++j) {
233 grad[j] -= tgt[j];
234 gmax = std::max(gmax, std::abs(num_traits<T>::to_double(grad[j])));
235 }
236 if (gmax <= 1e-12 * tmax) break;
237 const Matrix<T> hess = rayint_hessian(g, w, xi);
238 std::vector<T> d = rayint_solve(hess, grad);
239 double dmax = 0.0;
240 for (std::size_t j = 0; j < h; ++j) {
241 d[j] = -d[j];
242 dmax = std::max(dmax, std::abs(num_traits<T>::to_double(d[j])));
243 }
244 T step = one;
245 double stepd = 1.0;
246 while (stepd * dmax > 2.0) {
247 stepd /= 2.0;
248 step = step / num_traits<T>::from_int(2);
249 }
250 for (std::size_t j = 0; j < h; ++j) th[j] += step * d[j];
251 }
252 iterations = it;
253 for (std::size_t j = 0; j < h; ++j) xi[j] = exp(th[j]);
254 return xi;
255}
256
257/** log(x!) for a non-negative integer x, by summing logs; the counts here are small. */
258template <class T>
259T rayint_logfact(int x) {
260 using std::log;
261 T s = num_traits<T>::from_int(0);
262 for (int i = 2; i <= x; ++i) s += log(num_traits<T>::from_int(i));
263 return s;
264}
265
266/** Shared core: `gmat` non-null selects the discrete form, otherwise the quadrature one. */
267template <class T>
268RetrievalRayintResult<T> rayint_run(const Matrix<T>* gmat,
269 const std::function<Matrix<T>(const std::vector<T>&)>* gfun,
270 int n, const std::vector<int>& m, std::size_t nquad) {
271 using std::log;
272 using std::sqrt;
273 const std::size_t h = m.size();
274 if (h == 0) throw InputError("retrieval_rayint: the capacity vector must not be empty");
275
276 long msum = 0;
277 for (std::size_t j = 0; j < h; ++j) {
278 if (m[j] < 0) throw InputError("retrieval_rayint: list capacities must be non-negative");
279 msum += m[j];
280 }
281 if (gmat != nullptr && gmat->cols() != h)
282 throw InputError("retrieval_rayint: gamma and m disagree on the number of lists");
283
284 RetrievalRayintResult<T> out;
285 out.xi.assign(h, num_traits<T>::from_int(0));
286 out.logdet_sigma = num_traits<T>::from_int(0);
287
288 if (msum > n) {
289 out.e = num_traits<T>::from_int(0);
290 out.log_e = num_traits<T>::from_double(-std::numeric_limits<double>::infinity());
291 out.method = "boundary";
292 return out;
293 }
294 if (msum == 0) {
295 out.e = num_traits<T>::from_int(1);
296 out.log_e = num_traits<T>::from_int(0);
297 out.method = "boundary";
298 return out;
299 }
300 if (msum == n)
301 throw InputError("retrieval_rayint: the expansion requires sum(m) < n; at sum(m) = n the "
302 "saddle point escapes to infinity, use cache_erec for a full cache");
303
304 // lists of zero capacity contribute nothing and would make the saddle singular
305 std::vector<std::size_t> keep;
306 for (std::size_t j = 0; j < h; ++j)
307 if (m[j] > 0) keep.push_back(j);
308 const std::size_t hk = keep.size();
309
310 Matrix<T> g;
311 std::vector<T> w;
312 std::vector<T> tgt(hk);
313 const bool discrete = (gmat != nullptr);
314 if (discrete) {
315 const std::size_t K = gmat->rows();
316 g = Matrix<T>(K, hk);
317 for (std::size_t k = 0; k < K; ++k)
318 for (std::size_t a = 0; a < hk; ++a) g(k, a) = (*gmat)(k, keep[a]);
319 w.assign(K, num_traits<T>::from_int(1));
320 for (std::size_t a = 0; a < hk; ++a) tgt[a] = num_traits<T>::from_int(m[keep[a]]);
321 } else {
322 const std::size_t K = nquad;
323 std::vector<T> v(K);
324 for (std::size_t k = 0; k < K; ++k)
325 v[k] = num_traits<T>::from_rational(static_cast<long>(k), static_cast<long>(K - 1));
326 const Matrix<T> gfull = (*gfun)(v);
327 if (gfull.rows() != K)
328 throw InputError("retrieval_rayint: the profile must return one row per evaluation point");
329 if (gfull.cols() != h)
330 throw InputError("retrieval_rayint: the profile must return one column per cache list");
331 g = Matrix<T>(K, hk);
332 for (std::size_t k = 0; k < K; ++k)
333 for (std::size_t a = 0; a < hk; ++a) g(k, a) = gfull(k, keep[a]);
334 w.assign(K, num_traits<T>::from_int(0));
335 for (std::size_t k = 0; k < K; ++k) {
336 const long c = (k == 0 || k == K - 1) ? 1 : ((k % 2 == 1) ? 4 : 2);
337 w[k] = num_traits<T>::from_rational(c, static_cast<long>(3 * (K - 1)));
338 }
339 for (std::size_t a = 0; a < hk; ++a)
340 tgt[a] = num_traits<T>::from_rational(m[keep[a]], n);
341 }
342
343 std::size_t iters = 0;
344 const std::vector<T> xi = detail::rayint_saddle(g, w, tgt, iters);
345
346 const std::size_t K = g.rows();
347 const T one = num_traits<T>::from_int(1);
348 std::vector<T> s(K);
349 for (std::size_t k = 0; k < K; ++k) {
350 T acc = one;
351 for (std::size_t a = 0; a < hk; ++a) acc += g(k, a) * xi[a];
352 s[k] = acc;
353 }
354 const Matrix<T> hess = detail::rayint_hessian(g, w, xi);
355 const T logdet = detail::rayint_logdet(hess);
356
357 const T half = num_traits<T>::from_rational(1, 2);
358 const T log2pi = num_traits<T>::from_double(std::log(2.0 * M_PI));
359 const T hkT = num_traits<T>::from_int(static_cast<long>(hk));
360
361 T log_et;
362 if (discrete) {
363 T phi = num_traits<T>::from_int(0);
364 for (std::size_t k = 0; k < K; ++k) phi += log(s[k]);
365 for (std::size_t a = 0; a < hk; ++a) phi -= tgt[a] * log(xi[a]);
366 log_et = -half * hkT * log2pi + phi - half * logdet;
367 out.method = "saddle";
368 } else {
369 T phi = num_traits<T>::from_int(0);
370 for (std::size_t k = 0; k < K; ++k) phi += w[k] * log(s[k]);
371 for (std::size_t a = 0; a < hk; ++a) phi -= tgt[a] * log(xi[a]);
372 const T nT = num_traits<T>::from_int(n);
373 log_et = -half * hkT * log(nT) - half * hkT * log2pi + nT * phi - half * logdet +
374 half * log(s[K - 1] / s[0]);
375 out.method = "rayint";
376 }
377
378 T logfact = num_traits<T>::from_int(0);
379 for (std::size_t j = 0; j < h; ++j) logfact += detail::rayint_logfact<T>(m[j]);
380 out.log_e = log_et + logfact;
381 {
382 using std::exp;
383 out.e = exp(out.log_e);
384 }
385 for (std::size_t a = 0; a < hk; ++a) out.xi[keep[a]] = xi[a];
386 out.logdet_sigma = logdet;
387 out.iterations = iters;
388 int mmin = m[keep[0]];
389 for (std::size_t a = 1; a < hk; ++a) mmin = std::min(mmin, m[keep[a]]);
390 out.relerr_est = 0.14 * (1.0 / static_cast<double>(mmin) +
391 1.0 / static_cast<double>(n - msum));
392 return out;
393}
394
395} // namespace detail
396
397/**
398 * Discrete (saddle) form.
399 *
400 * @param gamma access factors gamma(k,j), n x h
401 * @param m cache list capacities, length h
402 */
403template <class T>
404RetrievalRayintResult<T> retrieval_rayint(const Matrix<T>& gamma, const std::vector<int>& m) {
406 "retrieval_rayint is a Laplace approximation built out of logs, exps and a "
407 "square root: it is meaningless at exact arithmetic, and widening the type "
408 "sharpens the saddle solve but never the O(1/n) model error. Use cache_erec "
409 "or retrieval_nc for the exact constant");
410 if (gamma.rows() == 0) throw InputError("retrieval_rayint: gamma must be a non-empty n x h matrix");
411 return detail::rayint_run<T>(&gamma, nullptr, static_cast<int>(gamma.rows()), m, 0);
412}
413
414/**
415 * Continuum (ray-integral) form.
416 *
417 * @param gfun access-factor profile on v in [0,1], returning v.size() x h
418 * @param m cache list capacities, length h
419 * @param n number of items
420 * @param nquad composite Simpson nodes (forced odd, at least 5)
421 */
422template <class T>
423RetrievalRayintResult<T> retrieval_rayint(const std::function<Matrix<T>(const std::vector<T>&)>& gfun,
424 const std::vector<int>& m, int n,
425 std::size_t nquad = 4097) {
427 "retrieval_rayint is a Laplace approximation built out of logs, exps and a "
428 "square root: it is meaningless at exact arithmetic. Use cache_erec or "
429 "retrieval_nc for the exact constant");
430 const std::size_t nq = std::max<std::size_t>(5, 2 * (nquad / 2) + 1);
431 return detail::rayint_run<T>(nullptr, &gfun, n, m, nq);
432}
433
434} // namespace retrieval
435} // namespace line
436
437#endif // LINE_API_RETRIEVAL_RETRIEVAL_RAYINT_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
RetrievalRayintResult< T > retrieval_rayint(const Matrix< T > &gamma, const std::vector< int > &m)
Discrete (saddle) form.
Number-type abstraction for the templated API port.
std::size_t iterations
Newton iterations used.
T logdet_sigma
log det of the Hessian in log xi.
double relerr_est
Estimated relative error, 0.14*(1/min_j m_j + 1/(n - sum_j m_j)).
T e
Normalizing constant, same normalization as cache_erec (may overflow; use log_e).
T log_e
Natural logarithm of e, safe for large n.
std::vector< T > xi
Saddle point xi_j, one entry per list (0 for a list of zero capacity).
std::string method
"saddle", "rayint" or "boundary".