1% Copyright (c) 2012-2026, Imperial College London
5 % CacheRMF Multi-list cache with RANDOM(m) replacement as a DDPP.
7 % Models a cache with h lists of capacities m = [m_1, ..., m_h] and n
8 % items with popularity distribution p = [p_1, ..., p_n]. The state
9 % space tracks in which list each item resides (or
if it
is outside the
12 % State vector layout (1-based MATLAB indexing):
13 % x(i + k * n) = density of item i in list k
14 % where i = 1..n, k = 0..h (k=0 means outside cache, k=1..h are
17 % Transition dynamics (RANDOM(m) replacement):
18 % When item i
is requested (rate p_i) and item i
is in list k:
19 % - A uniformly random item j from list k+1
is displaced to list k
20 % - Item i moves from list k to list k+1
23 % N. Gast,
"Expected Values Estimated via Mean-Field Approximation
24 % are 1/N-Accurate", Proc. ACM Meas. Anal. Comput. Syst., 2017.
27 p % double
array (1 x n), item popularity distribution
28 m % int
array (1 x h), list capacities
29 numberOfItems % int, number of items (n)
30 numberOfLists % int, number of cache lists (h)
31 modelDimension % int, total state dimension n*(h+1)
32 x0 % double
array (1 x modelDimension), initial state
36 function obj = CacheRMF(p, m)
37 % CacheRMF Constructor.
40 % p - double
array (1 x n), item request probabilities
41 % m - int
array (1 x h), capacity of each cache list
43 % Builds initial state: first m(1) items in list 1, next m(2)
44 % in list 2, etc. Remaining items outside cache (list 0).
46 obj.p = p(:)
'; % ensure row vector
47 obj.m = m(:)'; % ensure row vector
48 obj.numberOfItems = length(p);
49 obj.numberOfLists = length(m);
50 obj.modelDimension = obj.numberOfItems * (obj.numberOfLists + 1);
52 n = obj.numberOfItems;
53 h = obj.numberOfLists;
56 obj.x0 = zeros(1, obj.modelDimension);
57 objIdx = 1; % 1-based item counter
61 obj.x0(obj.index(objIdx, k)) = 1.0;
67 obj.x0(obj.index(i, 0)) = 1.0;
71 function idx = index(obj, i, k)
72 % index Map (item i, list k) to flat state vector index.
75 % i - item index (1-based, 1..n), scalar or vector
76 % k - list index (0-based, 0..h), scalar
79 % idx - 1-based flat index into state vector
81 idx = i + k * obj.numberOfItems;
84 function hr = hitRate(obj, x, listNumber)
85 % hitRate Compute hit rate contribution from a specific list.
88 % x - state vector (1 x modelDimension) or column vector
89 % listNumber - list index (0 = outside cache, 1..h = cache lists)
92 % hr - sum of p(i) * x(index(i, listNumber)) over all items
94 indices = obj.index(1:obj.numberOfItems, listNumber);
95 hr = dot(obj.p(:), x(indices(:)));
98 function dX = drift(obj, x)
99 % drift Compute the mean field drift F(x).
101 % The drift
for RANDOM(m) replacement
is:
102 % dx(i,k)/dt = -p(i)*x(i,k) + hitRate(k)*x(i,k+1)/m(k)
103 % dx(i,k+1)/dt = p(i)*x(i,k) - hitRate(k)*x(i,k+1)/m(k)
106 % x - state vector (modelDimension x 1) or (1 x modelDimension)
109 % dX - drift vector, same shape as x
111 x = x(:)
'; % ensure row vector for internal computation
112 n = obj.numberOfItems;
113 h = obj.numberOfLists;
115 hitRates = zeros(1, h + 1);
117 hitRates(k + 1) = obj.hitRate(x, k);
120 dX = zeros(1, obj.modelDimension);
123 ik = obj.index(i, k);
124 ik1 = obj.index(i, k + 1);
125 flow = obj.p(i) * x(ik) - hitRates(k + 1) * x(ik1) / obj.m(k + 1);
126 dX(ik) = dX(ik) - flow;
127 dX(ik1) = dX(ik1) + flow;
130 dX = dX(:); % return column vector (for ODE solvers)
133 function Fp = jacobian(obj, x)
134 % jacobian Compute Jacobian dF/dx at state x.
137 % x - state vector (1 x modelDimension)
140 % Fp - Jacobian matrix (modelDimension x modelDimension)
143 n = obj.numberOfItems;
144 h = obj.numberOfLists;
145 dim = obj.modelDimension;
147 hitRates = zeros(1, h + 1);
149 hitRates(k + 1) = obj.hitRate(x, k);
152 Fp = zeros(dim, dim);
155 ik = obj.index(i, k);
156 ik1 = obj.index(i, k + 1);
157 mk = obj.m(k + 1); % m
is 1-indexed, k
is 0-based list
160 Fp(ik, ik) = Fp(ik, ik) - obj.p(i);
161 Fp(ik1, ik) = Fp(ik1, ik) + obj.p(i);
162 Fp(ik, ik1) = Fp(ik, ik1) + hitRates(k + 1) / mk;
163 Fp(ik1, ik1) = Fp(ik1, ik1) - hitRates(k + 1) / mk;
165 % Indirect terms via hit rate dependence
167 jk = obj.index(j, k);
168 jk1 = obj.index(j, k + 1);
169 % d(hitRate[k])/d(x[j,k+1]) = p[j]
for list k+1
170 % but here hitRates(k+1)
is hitRate for list k
171 Fp(ik, jk1) = Fp(ik, jk1) - obj.p(i) * x(ik) / mk;
172 Fp(ik1, jk1) = Fp(ik1, jk1) + obj.p(i) * x(ik) / mk;
173 Fp(ik, jk) = Fp(ik, jk) + obj.p(j) * x(ik1) / mk;
174 Fp(ik1, jk) = Fp(ik1, jk) - obj.p(j) * x(ik1) / mk;
180 function Fpp = hessian(obj, x) %
#ok<INUSD>
181 % hessian Compute Hessian d^2F/dx^2.
183 % The Hessian
is constant (drift
is quadratic in x), so x
is
184 % unused but kept
for interface consistency.
187 % x - state vector (unused)
190 % Fpp - Hessian tensor (modelDimension x modelDimension x modelDimension)
192 n = obj.numberOfItems;
193 h = obj.numberOfLists;
194 dim = obj.modelDimension;
195 Fpp = zeros(dim, dim, dim);
199 ik = obj.index(i, k);
200 ik1 = obj.index(i, k + 1);
204 jk = obj.index(j, k);
205 jk1 = obj.index(j, k + 1);
206 % d^2 F[ik] / (d x[jk] d x[ik1]) = p[j]/mk
207 Fpp(ik, jk, ik1) = Fpp(ik, jk, ik1) + obj.p(j) / mk;
208 Fpp(ik, ik1, jk) = Fpp(ik, ik1, jk) + obj.p(j) / mk;
209 % d^2 F[ik] / (d x[jk1] d x[ik]) = -p[i]/mk
210 Fpp(ik, jk1, ik) = Fpp(ik, jk1, ik) - obj.p(i) / mk;
211 Fpp(ik, ik, jk1) = Fpp(ik, ik, jk1) - obj.p(i) / mk;
213 Fpp(ik1, jk, ik1) = Fpp(ik1, jk, ik1) - obj.p(j) / mk;
214 Fpp(ik1, ik1, jk) = Fpp(ik1, ik1, jk) - obj.p(j) / mk;
215 Fpp(ik1, jk1, ik) = Fpp(ik1, jk1, ik) + obj.p(i) / mk;
216 Fpp(ik1, ik, jk1) = Fpp(ik1, ik, jk1) + obj.p(i) / mk;
223 function Q = noiseMatrix(obj, x)
224 % noiseMatrix Compute noise intensity matrix Q(x)
for the DDPP.
226 % Q(a,b) = sum_ell ell(a) * ell(b) * beta_ell(x)
227 % where each transition ell
is a swap between items i and j
228 % across lists k and k+1, with rate p(i)*x(i,k)*x(j,k+1)/m(k).
231 % x - state vector (1 x modelDimension)
234 % Q - noise matrix (modelDimension x modelDimension)
237 n = obj.numberOfItems;
238 h = obj.numberOfLists;
239 dim = obj.modelDimension;
242 signs = [-1, 1, 1, -1];
246 ik = obj.index(i, k);
247 ik1 = obj.index(i, k + 1);
249 jk = obj.index(j, k);
250 jk1 = obj.index(j, k + 1);
251 rate = obj.p(i) * x(ik) * x(jk1) / mk;
252 indices = [ik, jk, ik1, jk1];
255 Q(indices(ia), indices(ib)) = Q(indices(ia), indices(ib)) + rate * signs(ia) * signs(ib);
263 function pi = fixedPoint(obj, tmax)
264 % fixedPoint Compute mean field fixed point by ODE integration.
266 % Integrates dx/dt = F(x) until steady state using ode15s.
269 % tmax - maximum integration time (default: 10000)
272 % pi - fixed point state vector (1 x modelDimension)
277 opts = odeset('RelTol
', 1e-8, 'AbsTol
', 1e-10);
278 %[~, Y] = ode15s(@(t, x) obj.drift(x), [0, tmax], obj.x0', opts);
279 [~, Y] = lsoda_solve(@(t, x) obj.drift(x), [0, tmax], obj.x0
', opts);
283 function [P, Pinv, r] = dimensionReduction(obj, Fp)
284 % dimensionReduction SVD-based dimension reduction for singular Jacobian.
286 % The Jacobian is singular because item populations are conserved
287 % (sum over lists for each item = 1). This method finds a change
288 % of basis that separates the rank-deficient directions.
291 % Fp - Jacobian matrix (modelDimension x modelDimension)
294 % P - change-of-basis matrix
295 % Pinv - inverse of P
298 dim = obj.modelDimension;
299 n = obj.numberOfItems;
300 h = obj.numberOfLists;
304 % Build change-of-basis: first r rows are independent coordinates,
305 % remaining rows span the null space of Fp
310 C(d, obj.index(i, lIdx)) = 1.0;
316 C((r + 1):end, :) = U(:, (r + 1):end)';
317 Pinv = inv(C); %#ok<MINV>
321 function [Fp_r, Fpp_r, Q_r,
P, Pinv, r] = reduceFpFppQ(obj, Fp, Fpp, Q)
322 % reduceFpFppQ Apply dimension reduction to Fp, Fpp, Q.
324 % Projects the Jacobian, Hessian, and noise matrix onto the
325 % non-singular subspace identified by dimension reduction.
328 % Fp - Jacobian matrix (dim x dim)
329 % Fpp - Hessian tensor (dim x dim x dim)
330 % Q - noise matrix (dim x dim)
333 % Fp_r - reduced Jacobian (r x r)
334 % Fpp_r - reduced Hessian (r x r x r)
335 % Q_r - reduced noise matrix (r x r)
336 %
P - change-of-basis matrix
337 % Pinv - inverse of
P
340 [
P, Pinv, r] = obj.dimensionReduction(Fp);
343 Fp_full =
P * Fp * Pinv;
344 Fp_r = Fp_full(1:r, 1:r);
346 % Reduced Hessian via tensor contraction
347 % Fpp_r(a,b,c) = sum_{i,j,k}
P(a,i) * Fpp(i,j,k) * Pinv(j,b) * Pinv(k,c)
348 dim = obj.modelDimension;
349 Fpp_r = zeros(r, r, r);
350 % First contraction: T1(a,j,k) = sum_i
P(a,i) * Fpp(i,j,k)
351 T1 = zeros(r, dim, dim);
357 val = val +
P(a, i) * Fpp(i, j, k);
363 % Second contraction: T2(a,b,k) = sum_j T1(a,j,k) * Pinv(j,b)
364 T2 = zeros(r, r, dim);
370 val = val + T1(a, j, k) * Pinv(j, b);
376 % Third contraction: Fpp_r(a,b,c) = sum_k T2(a,b,k) * Pinv(k,c)
382 val = val + T2(a, b, k) * Pinv(k, c);
384 Fpp_r(a, b, c) = val;
389 % Reduced noise matrix
391 Q_r = Q_full(1:r, 1:r);
394 function [V, W] = expandVW(obj, V_r, W_r, Pinv, r) %#ok<INUSL>
395 % expandVW Expand reduced V, W back to full dimension.
398 % V_r - reduced correction vector (r x 1)
399 % W_r - reduced covariance matrix (r x r)
400 % Pinv - inverse change-of-basis matrix
404 % V - full correction vector (modelDimension x 1)
405 % W - full covariance matrix (modelDimension x modelDimension)
407 V = Pinv(:, 1:r) * V_r;
408 W = Pinv(:, 1:r) * W_r * Pinv(:, 1:r)';
411 function [pi, V, VW] = meanFieldExpansionSteadyState(obj, order)
412 % meanFieldExpansionSteadyState Compute refined mean field steady-state.
414 % Computes the mean field fixed point pi and the 1/N correction V
415 %
using the Lyapunov equation approach with dimension reduction.
417 % The refined approximation
for a system of N items
is:
418 % E[X] ~ pi + V/N + O(1/N^2)
421 % order - expansion order (0 = plain MF, 1 = with 1/N correction)
425 % pi - mean field fixed point (1 x modelDimension)
426 % V - first-order correction (modelDimension x 1)
427 % VW - cell {V, W} with correction and covariance
433 pi = obj.fixedPoint();
436 V = zeros(obj.modelDimension, 1);
437 W = zeros(obj.modelDimension);
442 Fp = obj.jacobian(pi);
443 Fpp = obj.hessian(pi);
444 Q = obj.noiseMatrix(pi);
446 % Dimension reduction: project onto non-singular subspace
447 [Fp_r, Fpp_r, Q_r, ~, Pinv, r] = obj.reduceFpFppQ(Fp, Fpp, Q);
449 % Solve Lyapunov equation in reduced space:
450 % Fp_r * W_r + W_r * Fp_r
' + Q_r = 0
451 % MATLAB lyap(A, Q) solves A*X + X*A' + Q = 0
452 W_r = lyap(Fp_r, Q_r);
454 % First-order correction: Hessian contraction
455 % C_r(a) = sum_{b,c} Fpp_r(a,b,c) * W_r(b,c)
458 C_r(a) = sum(sum(squeeze(Fpp_r(a, :, :)) .* W_r));
460 V_r = -Fp_r \ (C_r / 2);
462 % Expand back to full dimension
463 [V, W] = obj.expandVW(V_r, W_r, Pinv, r);
467 function [T, X, Vt, Wt] = meanFieldExpansionTransient(obj, time, n_points, order)
468 % meanFieldExpansionTransient Compute refined mean field transient expansion.
470 % Integrates the coupled ODE system
for (X, V, W) where:
471 % X(t): mean field trajectory
472 % V(t): 1/N correction trajectory
473 % W(t): covariance trajectory
476 % time - maximum integration time (default: 50)
477 % n_points - number of output time points (default: 200)
478 % order - expansion order (0 or 1, default: 1)
481 % T - time points (n_points x 1)
482 % X - mean field trajectory (n_points x modelDimension)
483 % Vt - correction trajectory (n_points x modelDimension)
484 % Wt - covariance trajectory (n_points x modelDimension x modelDimension)
486 if nargin < 2 || isempty(time), time = 50; end
487 if nargin < 3 || isempty(n_points), n_points = 200; end
488 if nargin < 4 || isempty(order), order = 1; end
490 dim = obj.modelDimension;
491 T = linspace(0, time, n_points)
';
494 odeopt = odeset('RelTol
', 1e-6, 'AbsTol
', 1e-10);
495 %[~, X] = ode15s(@(t, x) obj.drift(x), T, obj.x0', odeopt);
496 [~, X] = lsoda_solve(@(t, x) obj.drift(x), T, obj.x0
', odeopt);
497 Vt = zeros(n_points, dim);
498 Wt = zeros(n_points, dim, dim);
502 % Coupled ODE: state = [X (dim), V (dim), W_flat (dim^2)]
503 total_dim = dim + dim + dim * dim;
504 y0 = zeros(total_dim, 1);
505 y0(1:dim) = obj.x0(:);
507 function dy = coupled_rhs(~, y)
509 v = y((dim+1):(2*dim));
510 w = reshape(y((2*dim+1):end), dim, dim);
513 Fp = obj.jacobian(x(:)');
514 Fpp = obj.hessian(x(:)
');
515 Qmat = obj.noiseMatrix(x(:)');
519 % Add Hessian contraction: 0.5 * tensordot(Fpp, w, axes=([1,2],[0,1]))
521 dv(a) = dv(a) + 0.5 * sum(sum(squeeze(Fpp(a, :, :)) .* w));
523 dw = Fp * w + w * Fp' + Qmat;
525 dy = [dx; dv; dw(:)];
528 odeopt = odeset('RelTol', 1e-6, 'AbsTol', 1e-10);
529 %[~, Y] = ode15s(@coupled_rhs, T, y0, odeopt);
530 [~, Y] = lsoda_solve(@coupled_rhs, T, y0, odeopt);
533 Vt = Y(:, (dim+1):(2*dim));
534 W_flat = Y(:, (2*dim+1):end);
535 Wt = reshape(W_flat', dim, dim, []);
536 Wt = permute(Wt, [3, 1, 2]);
539 function hr = hitRatesAll(obj, x)
540 % hitRatesAll Compute hit rates for all lists.
543 % x - state vector (1 x modelDimension)
546 % hr -
array (1 x numberOfLists+1) with hit rate per list
548 h = obj.numberOfLists;
549 hr = zeros(1, h + 1);
551 hr(k + 1) = obj.hitRate(x, k);
555 function out = convertTo2D(obj, x)
556 % convertTo2D Convert flat state vector to (n x h+1) matrix.
559 % x - state vector (1 x modelDimension) or matrix
560 % (T x modelDimension) for trajectories
563 % out -
array (n x h+1) or (T x n x h+1)
565 n = obj.numberOfItems;
566 h = obj.numberOfLists;
569 out = zeros(n, h + 1);
572 out(i, k + 1) = x(obj.index(i, k));
577 out = zeros(T, n, h + 1);
581 out(t, i, k + 1) = x(t, obj.index(i, k));