LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_spm.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_SPM_H
6#define LINE_API_CACHE_SPM_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Saddle-point approximation of the cache normalizing constant.
12 *
13 * Templated port of matlab/src/api/cache/cache_spm.m, cross-checked against
14 * jar/src/main/java/jline/api/cache/Cache_spm.java.
15 *
16 * The constant E(gamma,m) of cache_erec is the coefficient extraction
17 *
18 * E = [prod_l z_l^{m_l}] prod_k (1 + sum_l gamma(k,l) z_l) * prod_l m_l!,
19 *
20 * evaluated here by a multidimensional saddle point at the multipliers xi from
21 * cache_xi_iter. With S(k) = sum_l gamma(k,l) xi(l) and
22 *
23 * phi = sum_k log(1 + S(k)) - sum_l m(l) log(xi(l)),
24 * C(j,l) = delta(j,l) sum_k gamma(k,j)/(1+S(k))
25 * - xi(j) sum_k gamma(k,j) gamma(k,l)/(1+S(k))^2,
26 *
27 * the Gaussian integral around the saddle gives
28 *
29 * log E ~ phi + sum_l log(m_l!) - (h/2) log(2 pi) - (1/2) sum_l log xi(l)
30 * - (1/2) log det C.
31 *
32 * TWO BOUNDARIES, both fixed in all four codebases on 2026-08-29; the notes
33 * below say what the behaviour used to be, because saved results predating the
34 * fix carry it.
35 *
36 * n == sum(m): every item is cached, so the capacity equations force every
37 * multiplier to infinity and there is no interior saddle. Z comes from the
38 * exact cache_erec and xi is reported as +infinity, its limit, WITHOUT running
39 * the iteration. Before the fix MATLAB computed Z exactly but then called
40 * cache_xi_iter for the third return value and hung (`cache_spm([.5 .25;.4
41 * .2;.3 .15],[2 1])` did not return in two minutes); the JAR had no fallback at
42 * all and ran the same non-terminating loop for every value; this port raised
43 * NumericError from cache_xi_iter's sweep cap, losing the exact Z with it.
44 *
45 * m_l == 0: list l has xi(l)=0, which is a boundary of the Laplace integral
46 * rather than a direction of it, so list l is dropped before the saddle solve
47 * and h shrinks with it. Dropping is exact: setting z_l=0 in the generating
48 * function removes list l from E(m), and prod_l m_l! is unchanged because
49 * 0!=1. Before the fix no codebase dropped it -- only all-zero ROWS of gamma
50 * were filtered, never zero-capacity COLUMNS -- so the bisection floored xi(l)
51 * at ~2^-50 and the -(1/2) sum_l log xi(l) prefactor gained ~+17 per empty
52 * list, silently: n=12 items at gamma=(0.8,0.6,0.4) and m=(0,3,3) returned
53 * lZ=24.814 against an exact 9.127, a factor of 6.5e8. cache_prob_spm reaches
54 * this on any list with m_l==1, since it evaluates E at oner(m,l).
55 * All m_l zero is the empty cache: Z=1, lZ=0, xi=0.
56 *
57 * ARITHMETIC: log, exp and sqrt throughout, plus the tolerance-stopped
58 * cache_xi_iter, so transcendental arithmetic is required.
59 *
60 * MATLAB's `lZ=real(lZ)` discards the imaginary part that appears when det C
61 * or some xi(l) is negative; that is implemented here as taking the modulus
62 * inside the logarithm, which is the same real branch. Z is returned on the
63 * same branch (MATLAB would return a complex Z there).
64 *
65 * The item count is gamma's ROW count in every codebase. MATLAB used to read
66 * `n = length(gamma)`, which is max(n,h), and used it both for the n == mt
67 * degenerate test and as the loop bound over gamma's rows, reading past the end
68 * whenever h > n; that was corrected to size(gamma,1) alongside the two fixes
69 * above. cache_erec still carries the same length() misuse.
70 */
71
72#include <cmath>
73#include <cstddef>
74#include <limits>
75#include <vector>
76
79#include "line/num/number.h"
80#include "line/util/error.h"
81#include "line/util/matrix.h"
82
83namespace line {
84namespace cache {
85
86/** Return value of cache_spm, mirroring [Z,lZ,xi]. */
87template <class T>
89 T Z; ///< normalizing constant
90 T lZ; ///< its logarithm
91 std::vector<T> xi; ///< (h) saddle-point multipliers
92};
93
94namespace detail {
95
96/** Determinant by Gaussian elimination with partial pivoting; 0 if singular. */
97template <class T>
98T cache_det(Matrix<T> A) {
99 const std::size_t n = A.rows();
100 if (A.cols() != n) throw InputError("cache_spm: determinant of a non-square matrix");
101 const T zero = num_traits<T>::from_int(0);
102 T det = num_traits<T>::from_int(1);
103 for (std::size_t k = 0; k < n; ++k) {
104 std::size_t p = k;
105 T amax = num_abs(A(k, k));
106 for (std::size_t i = k + 1; i < n; ++i) {
107 const T a = num_abs(A(i, k));
108 if (a > amax) {
109 amax = a;
110 p = i;
111 }
112 }
113 if (amax == zero) return zero;
114 if (p != k) {
115 for (std::size_t j = 0; j < n; ++j) std::swap(A(k, j), A(p, j));
116 det = -det;
117 }
118 det *= A(k, k);
119 for (std::size_t i = k + 1; i < n; ++i) {
120 const T fct = A(i, k) / A(k, k);
121 for (std::size_t j = k; j < n; ++j) A(i, j) -= fct * A(k, j);
122 }
123 }
124 return det;
125}
126
127/** gamma restricted to the rows with a strictly positive row sum. */
128template <class T>
129Matrix<T> gamma_nonzero_rows(const Matrix<T>& gamma) {
130 const T zero = num_traits<T>::from_int(0);
131 std::vector<std::size_t> keep;
132 for (std::size_t i = 0; i < gamma.rows(); ++i) {
133 T s = zero;
134 for (std::size_t j = 0; j < gamma.cols(); ++j) s += gamma(i, j);
135 if (s > zero) keep.push_back(i);
136 }
137 Matrix<T> g(keep.size(), gamma.cols());
138 for (std::size_t a = 0; a < keep.size(); ++a)
139 for (std::size_t j = 0; j < gamma.cols(); ++j) g(a, j) = gamma(keep[a], j);
140 return g;
141}
142
143} // namespace detail
144
145/**
146 * @brief Saddle-point approximation of the cache normalizing constant.
147 *
148 * @param gamma_in (n x h) access factors; all-zero rows are dropped first
149 * @param m (h) list capacities
150 */
151template <class T>
152CacheSpmResult<T> cache_spm(const Matrix<T>& gamma_in, const std::vector<int>& m) {
154 "cache_spm requires transcendental arithmetic");
155 using std::acos;
156 using std::log;
157 using std::exp;
158 using std::sqrt;
159
160 const Matrix<T> gamma = detail::gamma_nonzero_rows(gamma_in);
161 const std::size_t h = m.size();
162 const std::size_t n = gamma.rows();
163 if (gamma.cols() != h) throw InputError("cache_spm: gamma and m disagree on the number of lists");
164
165 long mt = 0;
166 for (int v : m) mt += v;
167
168 const T zero = num_traits<T>::from_int(0);
169 const T one = num_traits<T>::from_int(1);
170 const T two = num_traits<T>::from_int(2);
171 const T half = one / two;
172
174 if (static_cast<long>(n) == mt) {
175 // Degenerate saddle: every item is cached, so the capacity equations force
176 // every multiplier to infinity and cache_xi_iter cannot converge. Take Z
177 // from the exact recursion and report the limit, rather than iterating.
178 r.Z = cache_erec(gamma, m);
179 r.lZ = log(r.Z);
180 r.xi.assign(h, num_traits<T>::from_double(std::numeric_limits<double>::infinity()));
181 return r;
182 }
183
184 // A list with no capacity has xi=0, which is a boundary of the Laplace integral
185 // rather than a direction of it, so it must leave the expansion: kept, its
186 // -sum_l log(sqrt(xi_l)) prefactor diverges and Z comes out far too large.
187 // Dropping it is exact, since setting z_l=0 in the generating function removes
188 // list l from E(m) and prod_l m_l! is unchanged because 0!=1.
189 std::vector<std::size_t> keep;
190 for (std::size_t l = 0; l < h; ++l)
191 if (m[l] > 0) keep.push_back(l);
192 const std::size_t hk = keep.size();
193 if (hk == 0) {
194 // Empty cache: E(0)=1 and prod_l m_l!=1.
195 r.Z = one;
196 r.lZ = zero;
197 r.xi.assign(h, zero);
198 return r;
199 }
200
201 Matrix<T> gk(n, hk);
202 std::vector<int> mk(hk);
203 for (std::size_t a = 0; a < hk; ++a) {
204 mk[a] = m[keep[a]];
205 for (std::size_t k = 0; k < n; ++k) gk(k, a) = gamma(k, keep[a]);
206 }
207
208 const std::vector<T> xik = cache_xi_iter(gk, mk);
209 std::vector<T> xi(h, zero); // dropped lists keep xi = 0
210 for (std::size_t a = 0; a < hk; ++a) xi[keep[a]] = xik[a];
211
212 std::vector<T> S(n, zero);
213 for (std::size_t k = 0; k < n; ++k)
214 for (std::size_t l = 0; l < hk; ++l) S[k] += gk(k, l) * xik[l];
215
216 T phi = zero;
217 for (std::size_t k = 0; k < n; ++k) phi += log(one + S[k]);
218 for (std::size_t l = 0; l < hk; ++l)
219 phi -= log(xik[l]) * num_traits<T>::from_int(static_cast<long>(mk[l]));
220
221 Matrix<T> C(hk, hk, zero);
222 for (std::size_t j = 0; j < hk; ++j) {
223 for (std::size_t l = 0; l < hk; ++l) {
224 T C1 = zero, C2 = zero;
225 for (std::size_t k = 0; k < n; ++k) {
226 C1 += gk(k, j) / (one + S[k]);
227 C2 += gk(k, j) * gk(k, l) / ((one + S[k]) * (one + S[k]));
228 }
229 C(j, l) = (j == l ? C1 : zero) - xik[j] * C2;
230 }
231 }
232
233 const T detC = detail::cache_det(C);
234 if (detC == zero) throw NumericError("cache_spm: the saddle-point Hessian is singular");
235
236 const T pi = acos(num_traits<T>::from_int(-1));
237 const T s2pi = sqrt(two * pi);
238
239 // log(prod_l m_l!), computed from the exact factorial rather than lgamma.
240 T lfact = zero;
241 for (std::size_t l = 0; l < hk; ++l)
242 lfact += log(num_factorial<T>(static_cast<unsigned>(mk[l])));
243
244 // real() branch: modulus inside every logarithm, as MATLAB's real(lZ) does.
245 T lsqrtxi = zero;
246 for (std::size_t l = 0; l < hk; ++l) lsqrtxi += half * log(num_abs(xik[l]));
247
248 r.lZ = -num_traits<T>::from_int(static_cast<long>(hk)) * log(s2pi) + phi + lfact - lsqrtxi -
249 half * log(num_abs(detC));
250
251 T prodfact = one, prodsqrtxi = one;
252 for (std::size_t l = 0; l < hk; ++l) {
253 prodfact *= num_factorial<T>(static_cast<unsigned>(mk[l]));
254 prodsqrtxi *= sqrt(num_abs(xik[l]));
255 }
256 r.Z = exp(phi) * num_pow_int(one / s2pi, static_cast<unsigned>(hk)) * prodfact / prodsqrtxi /
257 sqrt(num_abs(detC));
258 r.xi = xi;
259 return r;
260}
261
262} // namespace cache
263} // namespace line
264
265#endif // LINE_API_CACHE_SPM_H
Exact recursive normalizing constant of a multi-list cache model.
Lagrange multipliers of a multi-list cache by the Gast-Van Houdt iteration.
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.
std::vector< T > cache_xi_iter(const Matrix< T > &gamma, const std::vector< int > &m)
Lagrange multipliers of a multi-list cache by the Gast-Van Houdt iteration.
CacheSpmResult< T > cache_spm(const Matrix< T > &gamma_in, const std::vector< int > &m)
Saddle-point approximation of the cache normalizing constant.
Definition cache_spm.h:152
T cache_erec(const Matrix< T > &gamma, const std::vector< int > &m)
Exact recursive normalizing constant of a multi-list cache model.
Definition cache_erec.h:201
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
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.
Return value of cache_spm, mirroring [Z,lZ,xi].
Definition cache_spm.h:88
T Z
normalizing constant
Definition cache_spm.h:89
std::vector< T > xi
(h) saddle-point multipliers
Definition cache_spm.h:91