LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_erec.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_EREC_H
6#define LINE_API_CACHE_EREC_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Exact recursive normalizing constant of a multi-list cache model.
12 *
13 * Templated port of matlab/src/api/cache/cache_erec.m, cross-checked against
14 * jar/src/main/java/jline/api/cache/Cache_erec.java.
15 *
16 * The cache holds h lists of capacities m(1..h) filled from n items; the
17 * steady-state distribution of the list-based replacement model is
18 * proportional to prod over placed items of gamma(item,list), and E(gamma,m)
19 * is the sum of that product over every admissible placement, weighted by the
20 * list multiplicities. The recursion peels off item k:
21 *
22 * E_k(m) = E_{k-1}(m) + sum_j gamma(k,j) m(j) E_{k-1}(m - e_j),
23 *
24 * with E(0) = 1, E(m) = 0 whenever sum(m) exceeds the number of remaining
25 * items or any capacity is negative, and E_1(e_j) = gamma(1,j).
26 *
27 * Every step is a multiplication and an addition, so the exact instantiation
28 * returns E as a rational with no rounding at all: for rational access factors
29 * that is the true normalizing constant, which is what makes it usable as the
30 * oracle for the approximate members of the family (cache_spm, cache_xi_fp).
31 *
32 * DIVERGENCE, MATLAB vs JAR: the MATLAB entry point recurses from
33 * k = length(gamma), and MATLAB's length() on an (n x h) matrix is max(n,h).
34 * That is the item count n only while n >= h; for a cache with more lists than
35 * items MATLAB starts the recursion at h and reads gamma rows that do not
36 * exist. The JAR uses gamma.getNumRows() and is right. This port follows the
37 * JAR (n = number of rows).
38 */
39
40#include <cstddef>
41#include <vector>
42
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace cache {
49
50namespace detail {
51
52/**
53 * gamma with row i deleted, i.e. MATLAB's gamma(setdiff(1:n,i),:). Shared by
54 * every member of the family that conditions on "item i is absent".
55 */
56template <class T>
57Matrix<T> gamma_without_row(const Matrix<T>& gamma, std::size_t i) {
58 if (gamma.rows() == 0) return gamma;
59 Matrix<T> g(gamma.rows() - 1, gamma.cols());
60 std::size_t r = 0;
61 for (std::size_t a = 0; a < gamma.rows(); ++a) {
62 if (a == i) continue;
63 for (std::size_t b = 0; b < gamma.cols(); ++b) g(r, b) = gamma(a, b);
64 ++r;
65 }
66 return g;
67}
68
69/** E over items 1..k of the (n x h) access-factor matrix. */
70template <class T>
71T cache_erec_aux(const Matrix<T>& gamma, const std::vector<int>& m, int k) {
72 long mt = 0;
73 int mmin = 0;
74 bool first = true;
75 for (int v : m) {
76 mt += v;
77 if (first || v < mmin) mmin = v;
78 first = false;
79 }
80 if (mt == 0) return num_traits<T>::from_int(1);
81 if (mt > k || mmin < 0) return num_traits<T>::from_int(0);
82
83 const std::size_t h = m.size();
84 if (k == 1 && mt == 1) {
85 for (std::size_t j = 0; j < h; ++j)
86 if (m[j] != 0) return gamma(0, j);
87 return num_traits<T>::from_int(0);
88 }
89
90 T E = cache_erec_aux(gamma, m, k - 1);
91 for (std::size_t j = 0; j < h; ++j) {
92 if (m[j] > 0) {
93 std::vector<int> mj = m;
94 mj[j] -= 1;
95 E += gamma(static_cast<std::size_t>(k - 1), j) *
96 num_traits<T>::from_int(static_cast<long>(m[j])) *
97 cache_erec_aux(gamma, mj, k - 1);
98 }
99 }
100 return E;
101}
102
103/**
104 * E(m,k) over the (residual capacity, residual cost cap) lattice, the
105 * cost-capped recursion of Casale-Gast, IEEE/ACM Trans. Networking 29(2),
106 * 2021, Sec. IX:
107 *
108 * E(m,k) = E_i(m,k) + sum_j m_j gamma(i,j) E_i(m - e_j, k - sigma_i e_j),
109 *
110 * with the extra boundary E(m,k) = 0 whenever a residual cap is negative. The
111 * plain recursion cannot carry the cap, because the k - sigma_i e_j argument
112 * couples item sizes into the recursion graph, so this is evaluated as a
113 * dynamic program over the product lattice of residual capacities and residual
114 * caps: O(n prod_j (m_j+1)(k_j+1)) time and space.
115 */
116template <class T>
117T cache_erec_cost(const Matrix<T>& gamma, const std::vector<int>& m,
118 const std::vector<int>& sigma, const std::vector<int>& k) {
119 const std::size_t n = gamma.rows();
120 const std::size_t h = m.size();
121 if (sigma.size() != n)
122 throw InputError("cache_erec: the item size vector must have one entry per item");
123 if (k.size() != h)
124 throw InputError("cache_erec: the cost cap vector must have one entry per cache list");
125 long mt = 0;
126 for (std::size_t j = 0; j < h; ++j) {
127 if (m[j] < 0 || k[j] < 0) return num_traits<T>::from_int(0);
128 mt += m[j];
129 }
130 for (std::size_t i = 0; i < n; ++i)
131 if (sigma[i] <= 0)
132 throw InputError("cache_erec: item sizes must be positive integers");
133 if (mt > static_cast<long>(n)) return num_traits<T>::from_int(0);
134 if (mt == 0) return num_traits<T>::from_int(1);
135
136 std::vector<std::size_t> dims(2 * h), stride(2 * h);
137 for (std::size_t j = 0; j < h; ++j) {
138 dims[j] = static_cast<std::size_t>(m[j]) + 1;
139 dims[h + j] = static_cast<std::size_t>(k[j]) + 1;
140 }
141 std::size_t size = 1;
142 for (std::size_t d = 0; d < 2 * h; ++d) {
143 stride[d] = size;
144 size *= dims[d];
145 if (size > 10000000u)
146 throw InputError(
147 "cache_erec: the cost-constrained lattice exceeds the exact method "
148 "limit; use the sampling method");
149 }
150 std::vector<T> F(size, num_traits<T>::from_int(0));
151 std::vector<T> Fprev(size, num_traits<T>::from_int(0));
152 std::vector<std::size_t> sub(2 * h, 0);
153 for (std::size_t idx = 0; idx < size; ++idx) {
154 std::size_t rem = idx, mc = 0;
155 for (std::size_t d = 2 * h; d-- > 0;) {
156 sub[d] = rem / stride[d];
157 rem -= sub[d] * stride[d];
158 }
159 for (std::size_t j = 0; j < h; ++j) mc += sub[j];
160 if (mc == 0) F[idx] = num_traits<T>::from_int(1);
161 }
162 for (std::size_t t = 0; t < n; ++t) {
163 Fprev = F;
164 for (std::size_t idx = 0; idx < size; ++idx) {
165 std::size_t rem = idx, mc = 0;
166 for (std::size_t d = 2 * h; d-- > 0;) {
167 sub[d] = rem / stride[d];
168 rem -= sub[d] * stride[d];
169 }
170 for (std::size_t j = 0; j < h; ++j) mc += sub[j];
171 if (mc > t + 1) {
172 F[idx] = num_traits<T>::from_int(0);
173 continue;
174 }
175 T val = Fprev[idx];
176 for (std::size_t j = 0; j < h; ++j) {
177 const std::size_t mj = sub[j];
178 const std::size_t kj = sub[h + j];
179 const std::size_t si = static_cast<std::size_t>(sigma[t]);
180 if (mj > 0 && kj >= si && !(gamma(t, j) == num_traits<T>::from_int(0))) {
181 val += gamma(t, j) * num_traits<T>::from_int(static_cast<long>(mj)) *
182 Fprev[idx - stride[j] - si * stride[h + j]];
183 }
184 }
185 F[idx] = val;
186 }
187 }
188 return F[size - 1];
189}
190
191} // namespace detail
192
193/**
194 * @brief Exact recursive normalizing constant of a multi-list cache model.
195 *
196 * @param gamma (n x h) access factors, item by list
197 * @param m (h) list capacities
198 * @return the normalizing constant E
199 */
200template <class T>
201T cache_erec(const Matrix<T>& gamma, const std::vector<int>& m) {
202 if (gamma.empty()) {
203 // empty-cache base case rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
204 return detail::cache_erec_aux(gamma, m, 0);
205 }
206 if (gamma.cols() != m.size())
207 throw InputError("cache_erec: gamma and m disagree on the number of lists");
208 return detail::cache_erec_aux(gamma, m, static_cast<int>(gamma.rows()));
209}
210
211/**
212 * The normalizing constant under per-list storage cost caps. An EMPTY sigma or
213 * k selects the unconstrained recursion, so a caller can pass the model's
214 * (possibly absent) sizes through unconditionally.
215 *
216 * @param gamma (n x h) access factors, item by list
217 * @param m (h) list capacities
218 * @param sigma (n) per-item storage costs, positive integers
219 * @param k (h) per-list storage cost caps, non-negative integers
220 * @return the normalizing constant E(m,k)
221 */
222template <class T>
223T cache_erec(const Matrix<T>& gamma, const std::vector<int>& m,
224 const std::vector<int>& sigma, const std::vector<int>& k) {
225 if (sigma.empty() || k.empty()) return cache_erec(gamma, m);
226 if (gamma.cols() != m.size())
227 throw InputError("cache_erec: gamma and m disagree on the number of lists");
228 return detail::cache_erec_cost(gamma, m, sigma, k);
229}
230
231} // namespace cache
232} // namespace line
233
234#endif // LINE_API_CACHE_EREC_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
bool empty() const
Definition matrix.h:92
The exception types the port throws.
Dense matrix and non-owning view.
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
Number-type abstraction for the templated API port.