LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_spm_size.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_CACHE_CACHE_SPM_SIZE_H
6#define LINE_API_CACHE_CACHE_SPM_SIZE_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Ray (WKB) asymptotic expansion of the cost-capped cache normalizing constant.
12 *
13 * Templated port of matlab/src/api/cache/cache_spm_size.m, cross-checked
14 * against jar/src/main/java/jline/api/cache/Cache_spm_size.java and
15 * python/line_solver/api/cache/spm_size.py.
16 *
17 * Approximates what cache_erec(gamma, m, sigma, k) computes exactly, in the SAME
18 * normalization, so the two are interchangeable. This is the item-size extension
19 * of retrieval_rayint, which carries the size-free expansion; call that one when
20 * there are no storage costs.
21 *
22 * Writing E = prod_j m_j! * H, the size-free recursion
23 *
24 * E(m,n) = E(m,n-1) + sum_j gamma_{n,j} m_j E(m-1_j,n-1), E(0,0)=1
25 *
26 * relaxes to H ~ exp(phi/eps) with n = y/eps, m_j = x_j/eps, whose eikonal
27 * e^{phi_y} = 1 + sum_j gamma_j(y) e^{-phi_j} carries the ray constants
28 * xi_j = e^{-phi_j}. With per-item storage costs sigma_i and per-list cost caps
29 * k_j the recursion gains the cost coordinate,
30 *
31 * E(m,k) = E_i(m,k) + sum_j m_j gamma_ij E_i(m-1_j, k-sigma_i 1_j),
32 *
33 * so the shift 1_j becomes e_j(y) = (1_j, s(y) 1_j) in the enlarged space
34 * X = (x,kappa) and the eikonal picks up the size tilt
35 *
36 * e^{phi_y} = 1 + sum_j gamma_j(y) e^{-phi_{x_j} - s(y) phi_{kappa_j}},
37 *
38 * with the second family of ray constants zeta_j = e^{-phi_{kappa_j}}. The rays
39 * integrate to the discrete saddle point of the product generating function
40 *
41 * sum_{m,k} H(m,k) prod_j z_j^{m_j} w_j^{k_j}
42 * = prod_i ( 1 + sum_j gamma_ij z_j w_j^{sigma_i} ),
43 *
44 * namely, with D_i = 1 + sum_j gamma_ij xi_j zeta_j^{sigma_i} and
45 * Psi = sum_i log D_i,
46 *
47 * m_j = sum_i gamma_ij xi_j zeta_j^{sigma_i} / D_i,
48 * k_j = sum_i sigma_i gamma_ij xi_j zeta_j^{sigma_i} / D_i,
49 * log H(m,k) ~ Psi - sum_j m_j log xi_j - sum_j k_j log zeta_j
50 * - (d/2) log(2 pi) - (1/2) log det grad^2 Psi,
51 *
52 * where d is the number of saddle coordinates and, with
53 * pi_ij = gamma_ij xi_j zeta_j^{sigma_i} / D_i and
54 * Q^i_{jl} = delta_{jl} pi_ij - pi_ij pi_il,
55 *
56 * grad^2 Psi = sum_i [1; sigma_i] [1; sigma_i]' (x) Q^i .
57 *
58 * Setting zeta_j = 1 recovers the size-free expansion exactly.
59 *
60 * CAPS ARE CUMULATIVE. cache_erec sums over the states of cost AT MOST k_j, so
61 * this function does the same by default (CacheCostMode::AtMost). The shadow
62 * price eta_j = log zeta_j <= 0 obeys complementary slackness: a list whose
63 * unconstrained mean cost already meets its cap is SLACK, keeps zeta_j = 1, and
64 * drops out of the saddle, which then degenerates continuously to the size-free
65 * expansion; a list whose cap BINDS sits at eta_j < 0, and the states below the
66 * boundary decay geometrically with ratio zeta_j, contributing the amplitude
67 * factor 1/(1-zeta_j). CacheCostMode::Exact gives instead the constant resolving
68 * the cost exactly at k_j, the raw Laplace formula above with no such factor.
69 *
70 * SIZE DIVERSITY IS REQUIRED. The Hessian integrand
71 * [1;sigma_i][1;sigma_i]' (x) Q^i has rank h, not 2h, so grad^2 Psi is
72 * nonsingular only if the sizes actually vary. This is not an artefact: with a
73 * single item size the cost of list j is sigma*m_j identically and the cap
74 * carries no information. That case is detected and answered exactly rather than
75 * passed to a singular saddle. If the sizes share a common divisor the cost
76 * lives on a sublattice; the sizes and caps are divided through by their gcd,
77 * which is an exact reduction and removes the corresponding lattice factor.
78 *
79 * OCCUPANCY. The result's pij is the saddle occupancy
80 * pi_il = gamma_il xi_l zeta_l^{sigma_i} / D_i and k_mean its per-list cost.
81 * These are EXACT-COST quantities: the saddle conditions are sum_i pi_ij = m_j
82 * and sum_i sigma_i pi_ij = k_j, so k_mean equals the cap exactly on every
83 * binding list. Under cumulative caps the true mean cost is strictly below the
84 * cap; use cache_cost and cache_prob_erec for that.
85 *
86 * ACCURACY. The expansion is O(1/n) at fixed occupancy. With a well-separated
87 * cap the observed error in log E is around 1e-2 at n = 200 and halves at each
88 * doubling of n. It degrades as a binding zeta_j approaches 1, i.e. in the
89 * transition between the binding and slack regimes, where the geometric
90 * resummation 1/(1-zeta_j) is no longer sharp; the result's zeta and binding
91 * report where the saddle sits, and relerr_est is the size-free O(1/n) baseline
92 * that does NOT cover that transition.
93 *
94 * ARITHMETIC: the expansion is a Laplace approximation built out of logs, exps
95 * and a square root, so it is meaningless at exact arithmetic and is gated on
96 * has_transcendental. It is also an APPROXIMATION whatever the arithmetic --
97 * widening the type sharpens the saddle solve, never the O(1/n) model error.
98 * Use cache_erec when the exact constant is wanted.
99 */
100
101#include <algorithm>
102#include <cmath>
103#include <cstddef>
104#include <limits>
105#include <string>
106#include <vector>
107
108#include "line/num/number.h"
109#include "line/util/error.h"
110#include "line/util/matrix.h"
111
112namespace line {
113namespace cache {
114
115/** Whether the caps bound the cost from above (matching cache_erec) or resolve it exactly. */
116enum class CacheCostMode { AtMost, Exact };
117
118/** Outcome of the expansion. */
119template <class T>
121 /** Normalizing constant, same normalization as cache_erec (may overflow; use log_e). */
122 T e;
123 /** Natural logarithm of e, safe for large n. */
125 /** Saddle point xi_j, one entry per list (0 for a list of zero capacity). */
126 std::vector<T> xi;
127 /** Cost tilt zeta_j on the original size lattice (1 for a slack or absent list). */
128 std::vector<T> zeta;
129 /** Whether each list's cost cap binds. */
130 std::vector<bool> binding;
131 /** Occupancy pi, n x (h+1), column 0 the miss probability. */
133 /** Mean storage cost held by each list. */
134 std::vector<T> k_mean;
135 /** The exponent Psi - m.log xi - k.log zeta. */
137 /** log det of the Hessian in (log xi, log zeta), restricted to the free coordinates. */
139 /** gcd of the item sizes, divided out as an exact lattice reduction. */
140 int span = 1;
141 /** "spm-size", "spm", "uniform-size", "lattice" or "boundary". */
142 std::string method;
143 /** Size-free error baseline 0.14*(1/min_j m_j + 1/(n - sum_j m_j)); see ACCURACY. */
144 double relerr_est = 0.0;
145 /** Newton iterations used. */
146 std::size_t iterations = 0;
147};
148
149namespace detail {
150
151/**
152 * pi_ij = gamma_ij xi_j zeta_j^{sigma_i} / D_i, D_i = 1 + sum_j (that numerator).
153 * `d_out`, when non-null, receives the D_i so the caller need not recompute them.
154 */
155template <class T>
156Matrix<T> spm_size_occupancy(const Matrix<T>& g, const std::vector<T>& sg,
157 const std::vector<T>& th, const std::vector<T>& et,
158 std::vector<T>* d_out = nullptr) {
159 using std::exp;
160 const std::size_t n = g.rows();
161 const std::size_t h = th.size();
162 const T one = num_traits<T>::from_int(1);
164 for (std::size_t a = 0; a < n; ++a) {
165 T s = one;
166 for (std::size_t b = 0; b < h; ++b) {
167 p(a, b) = g(a, b) * exp(th[b] + sg[a] * et[b]);
168 s += p(a, b);
169 }
170 if (d_out != nullptr) (*d_out)[a] = s;
171 for (std::size_t b = 0; b < h; ++b) p(a, b) = p(a, b) / s;
172 }
173 return p;
174}
175
176/**
177 * grad^2 Psi in (theta, eta), restricted to the free eta coordinates `ix`. Coordinate
178 * u < h is theta_u with weight 1; coordinate h+b is eta_{ix[b]} with weight sigma_i.
179 * The block is sum_i w_u(i) w_v(i) Q^i_{ju,jv} with Q^i_{jl} = delta_{jl} pi_ij - pi_ij pi_il.
180 */
181template <class T>
182Matrix<T> spm_size_hessian(const Matrix<T>& p, const std::vector<T>& sg,
183 const std::vector<std::size_t>& ix) {
184 const std::size_t n = p.rows();
185 const std::size_t h = p.cols();
186 const std::size_t nb = ix.size();
187 const std::size_t dim = h + nb;
188 std::vector<std::size_t> coord(dim);
189 for (std::size_t u = 0; u < h; ++u) coord[u] = u;
190 for (std::size_t b = 0; b < nb; ++b) coord[h + b] = ix[b];
191 Matrix<T> hess(dim, dim, num_traits<T>::from_int(0));
192 std::vector<T> w(dim, num_traits<T>::from_int(1));
193 const T zero = num_traits<T>::from_int(0);
194 for (std::size_t a = 0; a < n; ++a) {
195 for (std::size_t b = 0; b < nb; ++b) w[h + b] = sg[a];
196 for (std::size_t u = 0; u < dim; ++u) {
197 const std::size_t ju = coord[u];
198 for (std::size_t v = 0; v < dim; ++v) {
199 const std::size_t jv = coord[v];
200 const T q = (ju == jv ? p(a, ju) : zero) - p(a, ju) * p(a, jv);
201 hess(u, v) += w[u] * w[v] * q;
202 }
203 }
204 }
205 return hess;
206}
207
208/** Gaussian elimination with partial pivoting; the order is at most 2h, so tiny. */
209template <class T>
210std::vector<T> spm_size_solve(const Matrix<T>& a, const std::vector<T>& b) {
211 using std::abs;
212 const std::size_t dim = b.size();
213 Matrix<T> m(dim, dim + 1);
214 for (std::size_t i = 0; i < dim; ++i) {
215 for (std::size_t j = 0; j < dim; ++j) m(i, j) = a(i, j);
216 m(i, dim) = b[i];
217 }
218 for (std::size_t c = 0; c < dim; ++c) {
219 std::size_t piv = c;
220 for (std::size_t i = c + 1; i < dim; ++i)
221 if (abs(m(i, c)) > abs(m(piv, c))) piv = i;
222 if (num_traits<T>::to_double(abs(m(piv, c))) == 0.0)
223 throw NumericError("cache_spm_size: the saddle-point Newton step is not finite. With "
224 "item sizes this is the rank-h degeneracy of the size-tilted Hessian: "
225 "the sizes must genuinely vary for the cost coordinate to carry "
226 "information");
227 if (piv != c)
228 for (std::size_t j = 0; j <= dim; ++j) {
229 const T tmp = m(c, j);
230 m(c, j) = m(piv, j);
231 m(piv, j) = tmp;
232 }
233 for (std::size_t i = c + 1; i < dim; ++i) {
234 const T f = m(i, c) / m(c, c);
235 for (std::size_t j = c; j <= dim; ++j) m(i, j) -= f * m(c, j);
236 }
237 }
238 std::vector<T> x(dim);
239 for (std::size_t ii = dim; ii-- > 0;) {
240 T s = m(ii, dim);
241 for (std::size_t j = ii + 1; j < dim; ++j) s -= m(ii, j) * x[j];
242 x[ii] = s / m(ii, ii);
243 }
244 return x;
245}
246
247/** log det via Cholesky; the Hessian of a strictly convex objective is positive definite. */
248template <class T>
249T spm_size_logdet(const Matrix<T>& a) {
250 using std::log;
251 using std::sqrt;
252 const std::size_t dim = a.rows();
253 const T half = num_traits<T>::from_rational(1, 2);
254 const T two = num_traits<T>::from_int(2);
255 Matrix<T> l(dim, dim, num_traits<T>::from_int(0));
256 T ld = num_traits<T>::from_int(0);
257 for (std::size_t i = 0; i < dim; ++i) {
258 for (std::size_t j = 0; j <= i; ++j) {
259 T s = half * (a(i, j) + a(j, i));
260 for (std::size_t c = 0; c < j; ++c) s -= l(i, c) * l(j, c);
261 if (i == j) {
262 if (num_traits<T>::to_double(s) <= 0.0)
263 throw NumericError("cache_spm_size: the saddle-point Hessian is not positive "
264 "definite; the ray map is singular here. With item sizes this "
265 "happens when the sizes do not vary over the items the cache "
266 "can hold, in which case the cost cap carries no information");
267 l(i, j) = sqrt(s);
268 ld += two * log(l(i, j));
269 } else {
270 l(i, j) = s / l(j, j);
271 }
272 }
273 }
274 return ld;
275}
276
277/** Keeps the tilts within a factor e^2 per iteration. */
278template <class T>
279double spm_size_damp(const std::vector<T>& d) {
280 double dmax = 0.0;
281 for (std::size_t i = 0; i < d.size(); ++i)
282 dmax = std::max(dmax, std::abs(num_traits<T>::to_double(d[i])));
283 double step = 1.0;
284 while (step * dmax > 2.0) step /= 2.0;
285 return step;
286}
287
288template <class T>
289std::vector<T> spm_size_theta0(const Matrix<T>& g, const std::vector<T>& tgt) {
290 using std::log;
291 const std::size_t n = g.rows();
292 const std::size_t h = tgt.size();
293 double tsum = 0.0;
294 for (std::size_t b = 0; b < h; ++b) tsum += num_traits<T>::to_double(tgt[b]);
295 const double slack = std::max(1.0 - tsum / static_cast<double>(n), 1e-9);
296 const T slackT = num_traits<T>::from_double(slack);
297 const T tiny = num_traits<T>::from_double(1e-12);
298 std::vector<T> th(h);
299 for (std::size_t b = 0; b < h; ++b) {
300 T gb = num_traits<T>::from_int(0);
301 for (std::size_t a = 0; a < n; ++a) gb += g(a, b);
302 T den = gb * slackT;
303 if (num_traits<T>::to_double(den) < 1e-12) den = tiny;
304 T num = tgt[b];
305 if (num_traits<T>::to_double(num) < 1e-12) num = tiny;
306 th[b] = log(num / den);
307 }
308 return th;
309}
310
311/** The convex dual f = sum_i log D_i - m.theta - k.eta, over the binding eta only. */
312template <class T>
313T spm_size_obj(const Matrix<T>& g, const std::vector<T>& sg, const std::vector<T>& th,
314 const std::vector<T>& et, const std::vector<T>& tgtm, const std::vector<T>& tgtk,
315 const std::vector<bool>& bind) {
316 using std::log;
317 std::vector<T> d(g.rows());
318 spm_size_occupancy(g, sg, th, et, &d);
319 T f = num_traits<T>::from_int(0);
320 for (std::size_t a = 0; a < g.rows(); ++a) f += log(d[a]);
321 for (std::size_t b = 0; b < th.size(); ++b) {
322 f -= tgtm[b] * th[b];
323 if (bind[b]) f -= tgtk[b] * et[b];
324 }
325 return f;
326}
327
328/** Size-free saddle: Newton on theta = log xi for sum_i gamma_ij xi_j / D_i = m_j. */
329template <class T>
330std::vector<T> spm_size_saddle_free(const Matrix<T>& g, const std::vector<T>& tgt,
331 std::size_t& iterations) {
332 const std::size_t n = g.rows();
333 const std::size_t h = tgt.size();
334 const std::vector<T> zeros_n(n, num_traits<T>::from_int(0));
335 const std::vector<T> zeros_h(h, num_traits<T>::from_int(0));
336 const std::vector<std::size_t> noix;
337 std::vector<T> th = spm_size_theta0(g, tgt);
338 double tmax = 1.0;
339 for (std::size_t b = 0; b < h; ++b)
340 tmax = std::max(tmax, std::abs(num_traits<T>::to_double(tgt[b])));
341 std::size_t it = 0;
342 for (it = 1; it <= 200; ++it) {
343 const Matrix<T> p = spm_size_occupancy(g, zeros_n, th, zeros_h);
344 std::vector<T> grad(h);
345 double gmax = 0.0;
346 for (std::size_t b = 0; b < h; ++b) {
347 T s = num_traits<T>::from_int(0);
348 for (std::size_t a = 0; a < n; ++a) s += p(a, b);
349 grad[b] = s - tgt[b];
350 gmax = std::max(gmax, std::abs(num_traits<T>::to_double(grad[b])));
351 }
352 if (gmax <= 1e-12 * tmax) break;
353 std::vector<T> d = spm_size_solve(spm_size_hessian(p, zeros_n, noix), grad);
354 for (std::size_t b = 0; b < h; ++b) d[b] = -d[b];
355 const T step = num_traits<T>::from_double(spm_size_damp(d));
356 for (std::size_t b = 0; b < h; ++b) th[b] += step * d[b];
357 }
358 iterations = it;
359 return th;
360}
361
362/**
363 * Cost-constrained saddle. Minimises the convex dual
364 * f(theta,eta) = sum_i log D_i - m.theta - k.eta over eta <= 0 when the caps are
365 * cumulative, so that complementary slackness selects the binding lists; over all
366 * of R^{2h} when the cost is resolved exactly.
367 */
368template <class T>
369void spm_size_saddle(const Matrix<T>& g, const std::vector<T>& tgtm, const std::vector<T>& sg,
370 const std::vector<T>& tgtk, bool cumulative, std::vector<T>& th,
371 std::vector<T>& et, std::vector<bool>& bind, std::size_t& iterations) {
372 const std::size_t n = g.rows();
373 const std::size_t h = tgtm.size();
374 const T zero = num_traits<T>::from_int(0);
375 th = spm_size_theta0(g, tgtm);
376 et.assign(h, zero);
377 bind.assign(h, true);
378 double tol = 1.0;
379 for (std::size_t b = 0; b < h; ++b) {
380 tol = std::max(tol, std::abs(num_traits<T>::to_double(tgtm[b])));
381 tol = std::max(tol, std::abs(num_traits<T>::to_double(tgtk[b])));
382 }
383 tol *= 1e-12;
384 std::vector<T> thn(h), etn(h);
385 std::size_t it = 0;
386 for (it = 1; it <= 200; ++it) {
387 const Matrix<T> p = spm_size_occupancy(g, sg, th, et);
388 std::vector<T> gth(h), get(h);
389 for (std::size_t b = 0; b < h; ++b) {
390 T s1 = zero;
391 T s2 = zero;
392 for (std::size_t a = 0; a < n; ++a) {
393 s1 += p(a, b);
394 s2 += sg[a] * p(a, b);
395 }
396 gth[b] = s1 - tgtm[b];
397 get[b] = s2 - tgtk[b];
398 }
399 if (cumulative)
400 for (std::size_t b = 0; b < h; ++b) // at eta_j = 0 the cap binds when the cost exceeds it
401 bind[b] = num_traits<T>::to_double(et[b]) < 0.0 ||
402 num_traits<T>::to_double(get[b]) > 0.0;
403 std::vector<std::size_t> ix;
404 for (std::size_t b = 0; b < h; ++b)
405 if (bind[b]) ix.push_back(b);
406 const std::size_t nb = ix.size();
407 std::vector<T> grad(h + nb);
408 double gmax = 0.0;
409 for (std::size_t b = 0; b < h; ++b) {
410 grad[b] = gth[b];
411 gmax = std::max(gmax, std::abs(num_traits<T>::to_double(grad[b])));
412 }
413 for (std::size_t b = 0; b < nb; ++b) {
414 grad[h + b] = get[ix[b]];
415 gmax = std::max(gmax, std::abs(num_traits<T>::to_double(grad[h + b])));
416 }
417 if (gmax <= tol) break;
418 std::vector<T> d = spm_size_solve(spm_size_hessian(p, sg, ix), grad);
419 for (std::size_t b = 0; b < d.size(); ++b) d[b] = -d[b];
420 double step = spm_size_damp(d);
421 const T fcur = spm_size_obj(g, sg, th, et, tgtm, tgtk, bind);
422 // Backtrack until the dual decreases. The slack is essential, not cosmetic:
423 // Newton reaches the floating-point floor of f in a handful of steps, and a
424 // strict test then rejects every step and halves to zero without converging.
425 const double ftol = 1e-12 * (1.0 + std::abs(num_traits<T>::to_double(fcur)));
426 for (int ls = 0; ls < 40; ++ls) {
427 const T stepT = num_traits<T>::from_double(step);
428 for (std::size_t b = 0; b < h; ++b) {
429 thn[b] = th[b] + stepT * d[b];
430 etn[b] = et[b];
431 }
432 for (std::size_t b = 0; b < nb; ++b) etn[ix[b]] = et[ix[b]] + stepT * d[h + b];
433 if (cumulative)
434 for (std::size_t b = 0; b < h; ++b)
435 if (num_traits<T>::to_double(etn[b]) > 0.0) etn[b] = zero;
436 const T fn = spm_size_obj(g, sg, thn, etn, tgtm, tgtk, bind);
437 if (num_traits<T>::to_double(fn) <= num_traits<T>::to_double(fcur) + ftol) break;
438 step /= 2.0;
439 }
440 double moved = 0.0;
441 for (std::size_t b = 0; b < h; ++b) {
442 moved = std::max(moved, std::abs(num_traits<T>::to_double(thn[b] - th[b])));
443 moved = std::max(moved, std::abs(num_traits<T>::to_double(etn[b] - et[b])));
444 }
445 th = thn;
446 et = etn;
447 if (moved <= 1e-13) break; // the iterate can no longer move: at the floor
448 }
449 iterations = it;
450 if (cumulative)
451 for (std::size_t b = 0; b < h; ++b) bind[b] = num_traits<T>::to_double(et[b]) < 0.0;
452}
453
454/** log(x!) for a non-negative integer x, by summing logs; the counts here are small. */
455template <class T>
456T spm_size_logfact(int x) {
457 using std::log;
458 T s = num_traits<T>::from_int(0);
459 for (int i = 2; i <= x; ++i) s += log(num_traits<T>::from_int(i));
460 return s;
461}
462
463inline long spm_size_gcd(long a, long b) {
464 long x = a < 0 ? -a : a;
465 long y = b < 0 ? -b : b;
466 while (y != 0) {
467 const long r = x % y;
468 x = y;
469 y = r;
470 }
471 return x;
472}
473
474} // namespace detail
475
476/**
477 * @brief Ray (WKB) asymptotic expansion of the cost-capped cache normalizing
478 * constant.
479 *
480 * @param gamma (n x h) access factors
481 * @param m (h) list capacities, non-negative
482 * @param sigma (n) per-item storage costs, positive
483 * @param k (h) per-list storage cost caps
484 * @param mode AtMost (matches cache_erec) or Exact
485 */
486template <class T>
487CacheSpmSizeResult<T> cache_spm_size(const Matrix<T>& gamma, const std::vector<int>& m,
488 const std::vector<int>& sigma, const std::vector<int>& k,
491 "cache_spm_size is a Laplace approximation built out of logs, exps and a "
492 "square root: it is meaningless at exact arithmetic, and widening the type "
493 "sharpens the saddle solve but never the O(1/n) model error. Use cache_erec "
494 "for the exact constant");
495 using std::exp;
496 using std::log;
497
498 if (gamma.rows() == 0 || gamma.cols() == 0)
499 throw InputError("cache_spm_size: gamma must be a non-empty n x h matrix");
500 const std::size_t n0 = gamma.rows();
501 const std::size_t h0 = gamma.cols();
502 if (m.size() != h0)
503 throw InputError("cache_spm_size: the capacity vector must have one entry per cache list");
504 for (std::size_t j = 0; j < h0; ++j)
505 if (m[j] < 0) throw InputError("cache_spm_size: list capacities must be non-negative");
506 if (sigma.empty() || k.empty())
507 throw InputError("cache_spm_size: the item sizes and the cost caps are both required; use "
508 "retrieval_rayint for the size-free expansion");
509 if (sigma.size() != n0)
510 throw InputError("cache_spm_size: the item size vector must have one entry per item");
511 if (k.size() != h0)
512 throw InputError("cache_spm_size: the cost cap vector must have one entry per cache list");
513 for (std::size_t i = 0; i < n0; ++i)
514 if (sigma[i] <= 0) throw InputError("cache_spm_size: item sizes must be positive integers");
515 const bool exact_mode = (mode == CacheCostMode::Exact);
516 bool capped = true; // cleared below when a single item size makes the cap uninformative
517
518 const T zero = num_traits<T>::from_int(0);
519 const T one = num_traits<T>::from_int(1);
521 out.e = zero;
522 out.log_e = num_traits<T>::from_double(-std::numeric_limits<double>::infinity());
523 out.xi.assign(h0, zero);
524 out.zeta.assign(h0, one);
525 out.binding.assign(h0, false);
526 out.k_mean.assign(h0, zero);
527 out.phi = zero;
528 out.logdet_sigma = zero;
529 out.pij = Matrix<T>(n0, h0 + 1, zero);
530 for (std::size_t i = 0; i < n0; ++i) out.pij(i, 0) = one;
531
532 // --- boundaries, matching cache_erec ---
533 long msum = 0;
534 for (std::size_t j = 0; j < h0; ++j) msum += m[j];
535 bool negcap = false;
536 for (std::size_t j = 0; j < h0; ++j)
537 if (k[j] < 0) negcap = true;
538 if (msum > static_cast<long>(n0) || negcap) {
539 out.method = "boundary";
540 return out;
541 }
542 if (msum == 0) {
543 bool poscap = false;
544 for (std::size_t j = 0; j < h0; ++j)
545 if (k[j] > 0) poscap = true;
546 out.method = "boundary";
547 if (!(exact_mode && poscap)) {
548 out.e = one;
549 out.log_e = zero;
550 }
551 return out;
552 }
553
554 // --- items that can never be cached and lists of zero capacity drop out ---
555 std::vector<std::size_t> alive;
556 for (std::size_t i = 0; i < n0; ++i) {
557 T s = zero;
558 for (std::size_t j = 0; j < h0; ++j) s += gamma(i, j);
559 if (num_traits<T>::to_double(s) > 0.0) alive.push_back(i);
560 }
561 std::vector<std::size_t> keep;
562 for (std::size_t j = 0; j < h0; ++j)
563 if (m[j] > 0) keep.push_back(j);
564 const std::size_t n = alive.size();
565 const std::size_t hk = keep.size();
566 long mksum = 0;
567 for (std::size_t b = 0; b < hk; ++b) mksum += m[keep[b]];
568 if (mksum > static_cast<long>(n)) {
569 out.method = "boundary";
570 return out;
571 }
572 if (mksum == static_cast<long>(n))
573 throw InputError("cache_spm_size: the expansion requires sum(m) < n; at sum(m) = n the "
574 "saddle point escapes to infinity, use cache_erec for a full cache");
575
576 Matrix<T> g(n, hk);
577 std::vector<long> sgi(n);
578 for (std::size_t a = 0; a < n; ++a) {
579 sgi[a] = sigma[alive[a]];
580 for (std::size_t b = 0; b < hk; ++b) g(a, b) = gamma(alive[a], keep[b]);
581 }
582 std::vector<long> kki(hk);
583 std::vector<long> mki(hk);
584 for (std::size_t b = 0; b < hk; ++b) {
585 mki[b] = m[keep[b]];
586 kki[b] = k[keep[b]];
587 }
588
589 // --- exact reductions on the cost lattice ---
590 long span = 0;
591 for (std::size_t a = 0; a < n; ++a) span = detail::spm_size_gcd(span, sgi[a]);
592 out.span = static_cast<int>(span);
593 if (exact_mode)
594 for (std::size_t b = 0; b < hk; ++b)
595 if (kki[b] % span != 0) { // unreachable off the sublattice
596 out.method = "lattice";
597 return out;
598 }
599 for (std::size_t a = 0; a < n; ++a) sgi[a] /= span;
600 for (std::size_t b = 0; b < hk; ++b) kki[b] = kki[b] / span;
601 // per-list feasibility: the m_j cheapest (dearest) reachable items bound the cost
602 for (std::size_t b = 0; b < hk; ++b) {
603 std::vector<long> srt;
604 for (std::size_t a = 0; a < n; ++a)
605 if (num_traits<T>::to_double(g(a, b)) > 0.0) srt.push_back(sgi[a]);
606 if (static_cast<long>(srt.size()) < mki[b]) {
607 out.method = "boundary";
608 return out;
609 }
610 std::sort(srt.begin(), srt.end());
611 long lo = 0;
612 for (long a = 0; a < mki[b]; ++a) lo += srt[static_cast<std::size_t>(a)];
613 if (lo > kki[b]) {
614 out.method = "boundary";
615 return out;
616 }
617 if (exact_mode) {
618 long hi = 0;
619 for (std::size_t a = srt.size() - static_cast<std::size_t>(mki[b]); a < srt.size(); ++a)
620 hi += srt[a];
621 if (hi < kki[b]) {
622 out.method = "boundary";
623 return out;
624 }
625 }
626 }
627 // a single item size makes the cost of list j equal to sigma*m_j identically,
628 // so the cap carries no information and the 2h saddle is singular (rank h)
629 bool uniform = true;
630 for (std::size_t a = 1; a < n; ++a)
631 if (sgi[a] != sgi[0]) uniform = false;
632 if (uniform) {
633 bool feasible = true;
634 for (std::size_t b = 0; b < hk; ++b) {
635 const long cost = sgi[0] * mki[b];
636 if (exact_mode ? (cost != kki[b]) : (cost > kki[b])) feasible = false;
637 }
638 if (!feasible) {
639 out.method = "uniform-size";
640 return out;
641 }
642 capped = false; // fall through to the size-free expansion
643 out.method = "uniform-size";
644 }
645
646 std::vector<T> sg(n);
647 std::vector<T> mk(hk), kk(hk);
648 for (std::size_t a = 0; a < n; ++a) sg[a] = num_traits<T>::from_int(sgi[a]);
649 for (std::size_t b = 0; b < hk; ++b) {
650 mk[b] = num_traits<T>::from_int(mki[b]);
651 kk[b] = num_traits<T>::from_int(kki[b]);
652 }
653
654 // --- saddle point ---
655 std::vector<T> th, et;
656 std::vector<bool> bind;
657 std::size_t iters = 0;
658 std::vector<T> dvec(n);
659 Matrix<T> p;
660 T phi = zero;
661 T logdet = zero;
662 T log_h = zero;
663 const T half = num_traits<T>::from_rational(1, 2);
664 const T log2pi = num_traits<T>::from_double(std::log(2.0 * M_PI));
665 if (capped) {
666 detail::spm_size_saddle(g, mk, sg, kk, !exact_mode, th, et, bind, iters);
667 p = detail::spm_size_occupancy(g, sg, th, et, &dvec);
668 std::vector<std::size_t> ix;
669 for (std::size_t b = 0; b < hk; ++b)
670 if (bind[b]) ix.push_back(b);
671 for (std::size_t a = 0; a < n; ++a) phi += log(dvec[a]);
672 for (std::size_t b = 0; b < hk; ++b) phi -= mk[b] * th[b];
673 for (std::size_t b = 0; b < ix.size(); ++b) phi -= kk[ix[b]] * et[ix[b]];
674 logdet = detail::spm_size_logdet(detail::spm_size_hessian(p, sg, ix));
675 const T dof = num_traits<T>::from_int(static_cast<long>(hk + ix.size()));
676 log_h = phi - half * dof * log2pi - half * logdet;
677 if (!exact_mode)
678 for (std::size_t b = 0; b < ix.size(); ++b) // geometric resummation below the cap
679 log_h -= log(one - exp(et[ix[b]]));
680 if (out.method.empty()) out.method = "spm-size";
681 } else {
682 const std::vector<T> zeros_n(n, zero);
683 th = detail::spm_size_saddle_free(g, mk, iters);
684 et.assign(hk, zero);
685 bind.assign(hk, false);
686 p = detail::spm_size_occupancy(g, zeros_n, th, et, &dvec);
687 for (std::size_t a = 0; a < n; ++a) phi += log(dvec[a]);
688 for (std::size_t b = 0; b < hk; ++b) phi -= mk[b] * th[b];
689 const std::vector<std::size_t> noix;
690 logdet = detail::spm_size_logdet(detail::spm_size_hessian(p, zeros_n, noix));
691 const T dof = num_traits<T>::from_int(static_cast<long>(hk));
692 log_h = phi - half * dof * log2pi - half * logdet;
693 if (out.method.empty()) out.method = "spm";
694 }
695
696 T logfact = zero;
697 for (std::size_t j = 0; j < h0; ++j) logfact += detail::spm_size_logfact<T>(m[j]);
698 out.log_e = log_h + logfact; // back to the cache_erec normalization
699 out.e = exp(out.log_e);
700
701 // --- ray quantities, reported on the original item and list indexing ---
702 const T spanT = num_traits<T>::from_int(span);
703 for (std::size_t b = 0; b < hk; ++b) {
704 out.xi[keep[b]] = exp(th[b]);
705 out.zeta[keep[b]] = exp(et[b] / spanT);
706 out.binding[keep[b]] = bind[b];
707 }
708 for (std::size_t a = 0; a < n; ++a) {
709 T hit = zero;
710 for (std::size_t b = 0; b < hk; ++b) {
711 out.pij(alive[a], 1 + keep[b]) = p(a, b);
712 hit += p(a, b);
713 }
714 out.pij(alive[a], 0) = one - hit;
715 }
716 for (std::size_t j = 0; j < h0; ++j) {
717 T s = zero;
718 for (std::size_t i = 0; i < n0; ++i) s += num_traits<T>::from_int(sigma[i]) * out.pij(i, 1 + j);
719 out.k_mean[j] = s;
720 }
721 out.phi = phi;
722 out.logdet_sigma = logdet;
723 out.iterations = iters;
724 long mmin = mki[0];
725 for (std::size_t b = 1; b < hk; ++b) mmin = std::min(mmin, mki[b]);
726 out.relerr_est = 0.14 * (1.0 / static_cast<double>(mmin) +
727 1.0 / static_cast<double>(n - static_cast<std::size_t>(mksum)));
728 return out;
729}
730
731} // namespace cache
732} // namespace line
733
734#endif // LINE_API_CACHE_CACHE_SPM_SIZE_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
CacheCostMode
Whether the caps bound the cost from above (matching cache_erec) or resolve it exactly.
CacheSpmSizeResult< T > cache_spm_size(const Matrix< T > &gamma, const std::vector< int > &m, const std::vector< int > &sigma, const std::vector< int > &k, CacheCostMode mode=CacheCostMode::AtMost)
Ray (WKB) asymptotic expansion of the cost-capped cache normalizing constant.
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
Number-type abstraction for the templated API port.
Outcome of the expansion.
int span
gcd of the item sizes, divided out as an exact lattice reduction.
T phi
The exponent Psi - m.log xi - k.log zeta.
std::vector< T > xi
Saddle point xi_j, one entry per list (0 for a list of zero capacity).
std::vector< T > zeta
Cost tilt zeta_j on the original size lattice (1 for a slack or absent list).
Matrix< T > pij
Occupancy pi, n x (h+1), column 0 the miss probability.
std::vector< T > k_mean
Mean storage cost held by each list.
std::string method
"spm-size", "spm", "uniform-size", "lattice" or "boundary".
T e
Normalizing constant, same normalization as cache_erec (may overflow; use log_e).
std::size_t iterations
Newton iterations used.
double relerr_est
Size-free error baseline 0.14*(1/min_j m_j + 1/(n - sum_j m_j)); see ACCURACY.
T log_e
Natural logarithm of e, safe for large n.
T logdet_sigma
log det of the Hessian in (log xi, log zeta), restricted to the free coordinates.
std::vector< bool > binding
Whether each list's cost cap binds.