LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_miss_rmf.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_MISS_RMF_H
6#define LINE_API_CACHE_CACHE_MISS_RMF_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Refined mean field (RMF) miss rates of a multi-list RANDOM(m) cache.
12 *
13 * Templated port of matlab/src/api/cache/cache_miss_rmf.m, including its
14 * nested rmf_drift, rmf_jacobian, rmf_hessian, rmf_noise_matrix,
15 * rmf_fixed_point, rmf_dimension_reduction and rmf_expansion_steady_state.
16 *
17 * The model is a density dependent population process (DDPP) whose state
18 * x(i,k) is the probability that item i sits in list k, k = 0 meaning "not
19 * cached". A request for item i in list k promotes it to list k+1 and demotes
20 * a uniformly chosen occupant of list k+1, so the drift is
21 *
22 * flow(i,k) = p(i) x(i,k) - hit(k) x(i,k+1) / m(k+1),
23 * dx(i,k)/dt -= flow(i,k), dx(i,k+1)/dt += flow(i,k),
24 * hit(k) = sum_j p(j) x(j,k),
25 *
26 * for k = 0..h-1. The mean-field fixed point pi is the t -> infinity limit of
27 * that drift, and Gast's refinement adds the 1/N correction
28 *
29 * E[X] = pi + V/N + O(1/N^2), V = -(F')^-1 (1/2) sum_{b,c} F''_{bc} W_bc,
30 *
31 * with W the solution of the Lyapunov equation F' W + W F'^T + Q = 0 and Q the
32 * noise intensity of the DDPP. Reference: N. Gast, "Expected Values Estimated
33 * via Mean-Field Approximation are 1/N-Accurate", POMACS 2017.
34 *
35 * WHAT UNBLOCKED THIS. The fixed point is reached by integrating the drift to
36 * t = 1e4, which MATLAB does with ode15s. The drift is stiff: the per-item
37 * request rates p(i) of a Zipf-like popularity profile span several orders of
38 * magnitude, so the fast items equilibrate in O(1/p_max) while the slow ones
39 * need O(1/p_min), and an explicit integrator would be pinned to the fast
40 * scale for the whole horizon. The port now has line/util/ode.h, an adaptive
41 * Rosenbrock-4 with an embedded lower-order estimate, and this file uses it.
42 * The Jacobian is supplied analytically (rmf_jacobian is needed for the
43 * refinement anyway), so the integrator never forms a numeric one here.
44 *
45 * ALIGNMENT WITH THE REFERENCE. The fixed point is the single ode15s-style
46 * integration at RelTol 1e-8 / AbsTol 1e-10 over [0, tmax] that
47 * rmf_fixed_point performs (an earlier edition of this port polished it with a
48 * second pass at 1e-13 / 1e-16, which is no longer done), and the rank of the
49 * Jacobian is decided by a threshold of 1e-8 relative to sigma_1, the rule
50 * MATLAB, the JAR and python also use since BUG-102 (2026-10-06): rank()'s
51 * max(size) * eps(sigma_1) flipped on ULP-level changes of lambda, because the
52 * 1e-10 fixed point lifts one exact null direction to a singular value of that
53 * order, while the nonzero ones are O(0.1). Both choices are what makes
54 * the port return the reference's answer, including on the cases where the
55 * reference's refinement comes out non-finite and cache_miss_rmf.m silently
56 * keeps the plain mean-field fixed point: the port now takes the same branch
57 * there, which the result's `refined` flag reports. The one remaining
58 * difference is in the basis of the null space: LAPACK's SVD at T = double
59 * (the same quantity MATLAB reads out of svd(Fp)) and a pivoted elimination
60 * plus Gram-Schmidt at any other T. Only the SUBSPACE affects the result:
61 * writing C = [C1; C2], the block C1 Fp D1 that the reduction keeps depends on
62 * C2 only through ker(C2), and the expansion V = D1 V_r likewise, so any basis
63 * of the same subspace gives the same V.
64 * The LNA stationary covariance of the occupancy (rmf_lna_covariance) is
65 * returned as `Wcov` at T = double, empty when it is not computable or not
66 * finite, exactly as the reference's try/catch leaves it.
67 * MEASURED AGREEMENT (MATLAB R2025a, T = double), global miss rate M, as
68 * recorded for the earlier edition of this port:
69 * lambda = [0.5 0.3 0.15 0.05; 0.1 0.2 0.3 0.4] (2 users, 4 items)
70 * m = [2] MATLAB 0.992377021789716
71 * m = [1 2] MATLAB 0.490863272756674
72 * lambda = [49 49 49 49 7 1 1]/205 (1 user, 7 items)
73 * m = [1 1 3] MATLAB 0.024017720168154
74 * m = [3] MATLAB 0.348362022568998 (refinement rejected as non-finite)
75 * When the reduced Jacobian is singular -- a small or non-hyperbolic fixed
76 * point -- MATLAB's linear solves emit a warning and return non-finite
77 * entries, which cache_miss_rmf.m detects and discards, keeping the plain
78 * mean-field fixed point. The port raises NumericError from the same solves
79 * and catches it in the same place, with the same outcome; the result carries
80 * a `refined` flag saying which of the two was used, which the reference does
81 * not expose.
82 *
83 * ARITHMETIC. Gated: the fixed point is reached by a tolerance-driven
84 * integration, so it is an approximation in any arithmetic.
85 *
86 * COST. The Hessian is a dense model_dim^3 tensor and the Lyapunov solve is a
87 * dense rk^2 by rk^2 system, exactly as in the reference. With n items and h
88 * lists model_dim = n(h+1), so the refinement is practical for tens of items
89 * and quickly stops being so; the plain mean-field path has no such limit.
90 */
91
92#include <algorithm>
93#include <cmath>
94#include <complex>
95#include <cstddef>
96#include <limits>
97#include <type_traits>
98#include <vector>
99
100#include "line/num/number.h"
101#include "line/util/error.h"
102#include "line/util/eig.h"
103#include "line/util/linalg.h"
104#include "line/util/lu.h"
105#include "line/util/matrix.h"
106#include "line/util/lsoda.h"
107#include "line/util/ode.h"
108#include "line/util/sylvester.h"
110
111namespace line {
112namespace cache {
113
114/** Flat index of (item i, list k), k = 0 meaning "not cached" (rmf_index.m). */
115inline std::size_t cache_miss_rmf_index(std::size_t i, std::size_t k, std::size_t n_items) {
116 return i + k * n_items;
117}
118
119/** Return value of cache_miss_rmf, mirroring [M,MU,MI,pi0,tout,pi0_t,MU_t,xtraj]. */
120template <class T>
122 T M; ///< global miss rate
123 std::vector<T> MU; ///< (u) per-user miss rate
124 std::vector<T> MI; ///< (n_items) per-item miss rate
125 std::vector<T> pi0; ///< (n_items) per-item miss probability, clipped to [0,1]
126 std::vector<T> xss; ///< the occupancy the metrics were read from
127 bool refined = false; ///< true when the 1/N correction was accepted
128 /** LNA stationary covariance of the occupancy (model_dim x model_dim); empty when unavailable or not finite, and at T != double. */
130 std::vector<T> tout; ///< transient time grid, empty unless tspan was given
131 Matrix<T> pi0_t; ///< (n_items x nt) transient miss probability
132 Matrix<T> MU_t; ///< (u x nt) transient per-user miss rate
133 Matrix<T> xtraj; ///< (model_dim x nt) transient occupancy
134 /** Grid-independent average of the transient against a clock; empty unless one was given. */
136};
137
138namespace rmf_detail {
139
140/** hit rate of list `level`: sum_i p(i) x(i,level) (rmf_hit_rate.m). */
141template <class T>
142T hit_rate(const std::vector<T>& x, const std::vector<T>& p, std::size_t level,
143 std::size_t n_items) {
144 T hr = num_traits<T>::from_int(0);
145 for (std::size_t i = 0; i < n_items; ++i) hr += p[i] * x[cache_miss_rmf_index(i, level, n_items)];
146 return hr;
147}
148
149/** Mean-field drift F(x) (rmf_drift.m). */
150template <class T>
151std::vector<T> drift(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
152 std::size_t n_items, std::size_t h) {
153 const T zero = num_traits<T>::from_int(0);
154 const std::size_t model_dim = n_items * (h + 1);
155 std::vector<T> hr(h + 1, zero);
156 for (std::size_t k = 0; k <= h; ++k) hr[k] = hit_rate(x, p, k, n_items);
157 std::vector<T> dX(model_dim, zero);
158 for (std::size_t i = 0; i < n_items; ++i) {
159 for (std::size_t k = 0; k + 1 <= h; ++k) {
160 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
161 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
162 const T flow = p[i] * x[ik] - hr[k] * x[ik1] / m[k];
163 dX[ik] -= flow;
164 dX[ik1] += flow;
165 }
166 }
167 return dX;
168}
169
170/**
171 * dF/dx at x (rmf_jacobian.m).
172 *
173 * REFERENCE DEFECT, reproduced deliberately. This is a transcription of
174 * rmf_jacobian in cache_miss_rmf.m, and that function is NOT the derivative of
175 * the rmf_drift it accompanies. Differentiating the drift
176 *
177 * flow(i,k) = p(i) x(i,k) - hit(k) x(i,k+1) / m(k+1), hit(k) = sum_j p(j) x(j,k)
178 *
179 * gives d flow / d x(j,k+1) = -hit(k)/m(k+1) for j = i and ZERO for j != i,
180 * whereas rmf_jacobian adds a further -p(i) x(i,k)/m(k+1) for EVERY j.
181 * Those extra entries belong to the pairwise form of the drift, in which the
182 * demoted item is chosen explicitly, not to the aggregated form that rmf_drift
183 * implements. The discrepancy is real and reproducible: a central difference of
184 * rmf_drift disagrees with rmf_jacobian at those entries by about 10 percent
185 * (see the test, which pins the reference's values rather than the derivative).
186 *
187 * The port keeps the reference's formula because the refined mean-field
188 * correction is DEFINED by it in the reference and changing it would change the
189 * ported answer. It is not used as an integration Jacobian anywhere here: the
190 * fixed point is integrated with a numeric Jacobian, which is what the
191 * reference's own ode15s call does.
192 */
193template <class T>
194Matrix<T> jacobian(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
195 std::size_t n_items, std::size_t h) {
196 const T zero = num_traits<T>::from_int(0);
197 const std::size_t model_dim = n_items * (h + 1);
198 std::vector<T> hr(h + 1, zero);
199 for (std::size_t k = 0; k <= h; ++k) hr[k] = hit_rate(x, p, k, n_items);
200 Matrix<T> Fp(model_dim, model_dim, zero);
201 for (std::size_t i = 0; i < n_items; ++i) {
202 for (std::size_t k = 0; k + 1 <= h; ++k) {
203 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
204 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
205 Fp(ik, ik) -= p[i];
206 Fp(ik1, ik) += p[i];
207 Fp(ik, ik1) += hr[k] / m[k];
208 Fp(ik1, ik1) -= hr[k] / m[k];
209 for (std::size_t j = 0; j < n_items; ++j) {
210 const std::size_t jk = cache_miss_rmf_index(j, k, n_items);
211 const std::size_t jk1 = cache_miss_rmf_index(j, k + 1, n_items);
212 Fp(ik, jk1) -= p[i] * x[ik] / m[k];
213 Fp(ik1, jk1) += p[i] * x[ik] / m[k];
214 Fp(ik, jk) += p[j] * x[ik1] / m[k];
215 Fp(ik1, jk) -= p[j] * x[ik1] / m[k];
216 }
217 }
218 }
219 return Fp;
220}
221
222/**
223 * d^2F/dx^2 (rmf_hessian.m), stored flat: H[(a*model_dim + b)*model_dim + c] is
224 * d^2 F_a / (dx_b dx_c). The drift is quadratic, so the Hessian does not
225 * depend on x, exactly as in the reference.
226 */
227template <class T>
228std::vector<T> hessian(const std::vector<T>& p, const std::vector<T>& m, std::size_t n_items,
229 std::size_t h) {
230 const T zero = num_traits<T>::from_int(0);
231 const std::size_t md = n_items * (h + 1);
232 std::vector<T> H(md * md * md, zero);
233 const auto at = [md](std::size_t a, std::size_t b, std::size_t c) {
234 return (a * md + b) * md + c;
235 };
236 for (std::size_t i = 0; i < n_items; ++i) {
237 for (std::size_t k = 0; k + 1 <= h; ++k) {
238 const std::size_t ik = cache_miss_rmf_index(i, k, n_items);
239 const std::size_t ik1 = cache_miss_rmf_index(i, k + 1, n_items);
240 for (std::size_t j = 0; j < n_items; ++j) {
241 if (j == i) continue;
242 const std::size_t jk = cache_miss_rmf_index(j, k, n_items);
243 const std::size_t jk1 = cache_miss_rmf_index(j, k + 1, n_items);
244 H[at(ik, jk, ik1)] += p[j] / m[k];
245 H[at(ik, ik1, jk)] += p[j] / m[k];
246 H[at(ik, jk1, ik)] -= p[i] / m[k];
247 H[at(ik, ik, jk1)] -= p[i] / m[k];
248 H[at(ik1, jk, ik1)] -= p[j] / m[k];
249 H[at(ik1, ik1, jk)] -= p[j] / m[k];
250 H[at(ik1, jk1, ik)] += p[i] / m[k];
251 H[at(ik1, ik, jk1)] += p[i] / m[k];
252 }
253 }
254 }
255 return H;
256}
257
258/** Noise intensity Q(x) of the DDPP (rmf_noise_matrix.m). */
259template <class T>
260Matrix<T> noise_matrix(const std::vector<T>& x, const std::vector<T>& p, const std::vector<T>& m,
261 std::size_t n_items, std::size_t h) {
262 const T zero = num_traits<T>::from_int(0);
263 const std::size_t md = n_items * (h + 1);
264 Matrix<T> Q(md, md, zero);
265 const int signs[4] = {-1, 1, 1, -1};
266 for (std::size_t i = 0; i < n_items; ++i) {
267 for (std::size_t k = 0; k + 1 <= h; ++k) {
268 for (std::size_t j = 0; j < n_items; ++j) {
269 const T rate = p[i] * x[cache_miss_rmf_index(i, k, n_items)] *
270 x[cache_miss_rmf_index(j, k + 1, n_items)] / m[k];
271 const std::size_t idx[4] = {cache_miss_rmf_index(i, k, n_items),
272 cache_miss_rmf_index(j, k, n_items),
273 cache_miss_rmf_index(i, k + 1, n_items),
274 cache_miss_rmf_index(j, k + 1, n_items)};
275 for (int ia = 0; ia < 4; ++ia)
276 for (int ib = 0; ib < 4; ++ib)
277 Q(idx[ia], idx[ib]) +=
278 rate * num_traits<T>::from_int(signs[ia] * signs[ib]);
279 }
280 }
281 }
282 return Q;
283}
284
285/**
286 * Mean-field fixed point by integrating the drift to tmax (rmf_fixed_point.m).
287 * The reference uses ode15s with RelTol 1e-8 and AbsTol 1e-10 over [0,10000];
288 * the port uses the same horizon and the same tolerances with ode_rosenbrock4
289 * and the analytic Jacobian.
290 */
291template <class T>
292std::vector<T> fixed_point(const std::vector<T>& x0, const std::vector<T>& p,
293 const std::vector<T>& m, std::size_t n_items, std::size_t h,
294 const T& tmax, const T& rtol, const T& atol) {
295 OdeOptions<T> opt;
296 opt.rtol = rtol;
297 opt.atol = atol;
298 opt.store_trajectory = false;
299 const auto f = [&](const T& t, const std::vector<T>& x) {
300 (void)t;
301 return drift(x, p, m, n_items, h);
302 };
303 // numeric-Jacobian rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
304 return ode_rosenbrock4(f, T(num_traits<T>::from_int(0)), tmax, x0, opt).final_state();
305}
306
307/** fixed_point with the reference tolerances, RelTol 1e-8 and AbsTol 1e-10. */
308template <class T>
309std::vector<T> fixed_point(const std::vector<T>& x0, const std::vector<T>& p,
310 const std::vector<T>& m, std::size_t n_items, std::size_t h,
311 const T& tmax) {
312 return fixed_point(x0, p, m, n_items, h, tmax, T(num_traits<T>::from_double(1e-8)),
313 T(num_traits<T>::from_double(1e-10)));
314}
315
316/**
317 * Solve the Lyapunov equation F W + W F^T + Q = 0 by vectorization.
318 *
319 * The vectorized operator is (I (x) F + F (x) I) acting on vec(W); with W
320 * stored row-major, row (a,b) of the system reads
321 * sum_c F(a,c) W(c,b) + sum_c F(b,c) W(a,c) = -Q(a,b).
322 * The system is r^2 by r^2 and is solved with the port's LU. MATLAB calls
323 * lyap(), which uses a Bartels-Stewart Schur factorization; the two compute
324 * the same W, and the Schur route is the faster one, not a different answer.
325 * The vectorized route is used here because it needs nothing beyond the LU
326 * that the port already has, and because it fails loudly (a singular matrix
327 * throws) when F has a zero or a symmetric pair of eigenvalues, which is the
328 * case the caller must detect and reject.
329 */
330template <class T>
331Matrix<T> lyapunov(const Matrix<T>& F, const Matrix<T>& Q) {
332 const std::size_t r = F.rows();
333 const T zero = num_traits<T>::from_int(0);
334 Matrix<T> A(r * r, r * r, zero);
335 std::vector<T> rhs(r * r, zero);
336 for (std::size_t a = 0; a < r; ++a)
337 for (std::size_t b = 0; b < r; ++b) {
338 const std::size_t row = a * r + b;
339 for (std::size_t c = 0; c < r; ++c) {
340 A(row, c * r + b) += F(a, c);
341 A(row, a * r + c) += F(b, c);
342 }
343 rhs[row] = -Q(a, b);
344 }
345 const std::vector<T> w = line::solve(A, rhs);
346 Matrix<T> W(r, r, zero);
347 for (std::size_t a = 0; a < r; ++a)
348 for (std::size_t b = 0; b < r; ++b) W(a, b) = w[a * r + b];
349 return W;
350}
351
352/**
353 * Numeric rank and an orthonormal basis of the LEFT null space of Fp, by
354 * Gaussian elimination with full pivoting on Fp^T followed by Gram-Schmidt.
355 *
356 * MATLAB gets both from svd(Fp): rk = rank(Fp) and the trailing left singular
357 * vectors. The port cannot, because the port's SVD entry point is LAPACK-backed
358 * and double-only while this header is templated; and it does not need to,
359 * because only the SUBSPACE matters, not the basis of it (see the note at the
360 * top of this file). What does matter is the rank DECISION, which is a
361 * tolerance comparison in both codes: MATLAB thresholds the singular values at
362 * 1e-8*sigma_max, this thresholds the elimination pivots at 1e-8*max|Fp|. The
363 * null singular values sit near the 1e-10 accuracy of the fixed point and the
364 * nonzero ones are O(0.1), so both land in the same gap.
365 *
366 * The left null space is where the mean-field conservation laws live: each
367 * item's occupancies sum to one, so the n item-indicator vectors are always in
368 * it, and a symmetric popularity profile (items with equal request rates) adds
369 * more. That is why the rank cannot simply be taken as model_dim - n_items.
370 */
371template <class T>
372struct NullSpace {
373 std::size_t rank = 0;
374 std::vector<std::vector<T>> basis; ///< orthonormal rows spanning {w : w^T Fp = 0}
375};
376
377template <class T>
378NullSpace<T> left_null_space(const Matrix<T>& Fp) {
379 const std::size_t n = Fp.rows();
380 const T zero = num_traits<T>::from_int(0);
381 // Work on M = Fp^T so that its null vectors are the left null vectors of Fp.
382 Matrix<T> M(n, n, zero);
383 T scale = zero;
384 for (std::size_t i = 0; i < n; ++i)
385 for (std::size_t j = 0; j < n; ++j) {
386 M(i, j) = Fp(j, i);
387 const T a = num_abs(Fp(j, i));
388 if (a > scale) scale = a;
389 }
390 if (scale == zero) scale = num_traits<T>::from_int(1);
391 // Rank threshold: 1e-8 relative to sigma_max, as in the MATLAB reference
392 // (BUG-102), applied to the pivots with max|Fp| standing in for sigma_max.
393 const T tol = scale * num_traits<T>::from_double(1e-8);
394
395 std::vector<std::size_t> col_of_pivot;
396 std::vector<std::size_t> perm(n);
397 for (std::size_t i = 0; i < n; ++i) perm[i] = i;
398 std::size_t row = 0;
399 for (std::size_t col = 0; col < n && row < n; ++col) {
400 std::size_t p = row;
401 T best = num_abs(M(row, col));
402 for (std::size_t i = row + 1; i < n; ++i) {
403 const T a = num_abs(M(i, col));
404 if (a > best) {
405 best = a;
406 p = i;
407 }
408 }
409 if (best <= tol) continue; // no pivot in this column: it is free
410 if (p != row)
411 for (std::size_t j = 0; j < n; ++j) std::swap(M(row, j), M(p, j));
412 const T d = M(row, col);
413 for (std::size_t j = 0; j < n; ++j) M(row, j) = M(row, j) / d;
414 for (std::size_t i = 0; i < n; ++i) {
415 if (i == row) continue;
416 const T f = M(i, col);
417 if (f == zero) continue;
418 for (std::size_t j = 0; j < n; ++j) M(i, j) -= f * M(row, j);
419 }
420 col_of_pivot.push_back(col);
421 ++row;
422 }
423
424 NullSpace<T> ns;
425 ns.rank = col_of_pivot.size();
426 std::vector<bool> is_pivot(n, false);
427 for (std::size_t c : col_of_pivot) is_pivot[c] = true;
428 // One basis vector per free column, from the reduced row echelon form.
429 for (std::size_t free_col = 0; free_col < n; ++free_col) {
430 if (is_pivot[free_col]) continue;
431 std::vector<T> v(n, zero);
432 v[free_col] = num_traits<T>::from_int(1);
433 for (std::size_t r = 0; r < col_of_pivot.size(); ++r)
434 v[col_of_pivot[r]] = -M(r, free_col);
435 ns.basis.push_back(v);
436 }
437 // Gram-Schmidt, so that the basis is orthonormal like MATLAB's.
438 using std::sqrt;
439 for (std::size_t i = 0; i < ns.basis.size(); ++i) {
440 for (std::size_t k = 0; k < i; ++k) {
441 T dot = zero;
442 for (std::size_t j = 0; j < n; ++j) dot += ns.basis[i][j] * ns.basis[k][j];
443 for (std::size_t j = 0; j < n; ++j) ns.basis[i][j] -= dot * ns.basis[k][j];
444 }
445 T nrm2 = zero;
446 for (std::size_t j = 0; j < n; ++j) nrm2 += ns.basis[i][j] * ns.basis[i][j];
447 const T nrm = sqrt(nrm2);
448 if (nrm <= tol) throw NumericError(ErrorKind{"Degenerate"}, "cache_miss_rmf: the null-space basis degenerated");
449 for (std::size_t j = 0; j < n; ++j) ns.basis[i][j] = ns.basis[i][j] / nrm;
450 }
451 return ns;
452}
453
454/**
455 * The same thing from the SVD, which is what the reference uses.
456 *
457 * This matters more than a change of basis normally would. The Jacobian at the
458 * mean-field fixed point is very ill conditioned: on the four-item two-list
459 * case below its singular values are 7.5e-1 ... 1.7e-1, then 5.4e-10, then
460 * five at 1e-17. The 5.4e-10 one is a null direction that the finite accuracy
461 * of the fixed point has lifted off zero, so it counts as a nonzero singular
462 * value under both MATLAB's rank tolerance and any other, and the reduced
463 * Jacobian inherits it. Everything downstream then divides by it, and the 1/N
464 * correction depends on which basis of the (numerically ambiguous) null space
465 * was chosen. Reproducing the reference's numbers therefore requires
466 * reproducing its basis, not merely its subspace. This routine is used when
467 * LAPACK is available and T is double; the templated elimination above is the
468 * fallback, and the two agree whenever the fixed point is hyperbolic and the
469 * rank decision is unambiguous.
470 */
471inline NullSpace<double> left_null_space_svd(const Matrix<double>& Fp) {
472#ifndef LINE_MP_HAVE_LAPACK
473 return left_null_space(Fp);
474#else
475 const std::size_t n = Fp.rows();
476 std::vector<double> a(n * n);
477 for (std::size_t i = 0; i < n; ++i)
478 for (std::size_t j = 0; j < n; ++j) a[j * n + i] = Fp(i, j);
479 const int ni = static_cast<int>(n);
480 std::vector<double> s(n), u(n * n), vt(1);
481 int info = 0, lwork = -1;
482 double wopt = 0.0;
483 const int one = 1;
484 dgesvd_("A", "N", &ni, &ni, a.data(), &ni, s.data(), u.data(), &ni, vt.data(), &one, &wopt,
485 &lwork, &info);
486 if (info != 0) throw NumericError(ErrorKind{"Lapack"}, "cache_miss_rmf: LAPACK workspace query failed");
487 lwork = static_cast<int>(wopt);
488 std::vector<double> work(static_cast<std::size_t>(lwork));
489 dgesvd_("A", "N", &ni, &ni, a.data(), &ni, s.data(), u.data(), &ni, vt.data(), &one,
490 work.data(), &lwork, &info);
491 if (info != 0) throw NumericError(ErrorKind{"NonConvergence"}, "cache_miss_rmf: LAPACK dgesvd failed to converge");
492 // 1e-8 relative to sigma_1, as in the MATLAB reference (BUG-102).
493 const double tol = 1e-8 * s[0];
494 NullSpace<double> ns;
495 ns.rank = 0;
496 for (double v : s)
497 if (v > tol) ++ns.rank;
498 for (std::size_t c = ns.rank; c < n; ++c) {
499 std::vector<double> row(n, 0.0);
500 for (std::size_t i = 0; i < n; ++i) row[i] = u[c * n + i];
501 ns.basis.push_back(row);
502 }
503 return ns;
504#endif
505}
506
507/**
508 * Orthonormal basis of `{u in R^n : sum(u) = 0}`, as an n-by-(n-1) matrix
509 * (local_centered_basis in the reference, QR of I - ones/n).
510 */
511inline Matrix<double> lna_centered_basis(std::size_t n) {
512 if (n <= 1) return Matrix<double>(n, 0, 0.0);
513 std::vector<std::vector<double>> cols;
514 for (std::size_t c = 0; c < n && cols.size() + 1 < n; ++c) {
515 std::vector<double> v(n, 0.0);
516 for (std::size_t i = 0; i < n; ++i)
517 v[i] = (i == c ? 1.0 : 0.0) - 1.0 / static_cast<double>(n);
518 for (std::size_t b = 0; b < cols.size(); ++b) {
519 double d = 0.0;
520 for (std::size_t i = 0; i < n; ++i) d += v[i] * cols[b][i];
521 for (std::size_t i = 0; i < n; ++i) v[i] -= d * cols[b][i];
522 }
523 double nv = 0.0;
524 for (std::size_t i = 0; i < n; ++i) nv += v[i] * v[i];
525 nv = std::sqrt(nv);
526 if (nv <= 1e-12) continue;
527 for (std::size_t i = 0; i < n; ++i) v[i] /= nv;
528 cols.push_back(v);
529 }
530 Matrix<double> U(n, cols.size(), 0.0);
531 for (std::size_t c = 0; c < cols.size(); ++c)
532 for (std::size_t i = 0; i < n; ++i) U(i, c) = cols[c][i];
533 return U;
534}
535
536/** `kron(A, B)` in MATLAB's ordering. */
537inline Matrix<double> lna_kron(const Matrix<double>& A, const Matrix<double>& B) {
538 Matrix<double> K(A.rows() * B.rows(), A.cols() * B.cols(), 0.0);
539 for (std::size_t i = 0; i < A.rows(); ++i)
540 for (std::size_t j = 0; j < A.cols(); ++j)
541 for (std::size_t k = 0; k < B.rows(); ++k)
542 for (std::size_t l = 0; l < B.cols(); ++l)
543 K(i * B.rows() + k, j * B.cols() + l) = A(i, j) * B(k, l);
544 return K;
545}
546
547/**
548 * Stationary covariance of the occupancy under the LNA (rmf_lna_covariance):
549 * F' W + W F'^T + Q = 0 restricted to the double-centred subspace, the tensor
550 * product of the zero-sum item space with the zero-sum list space. Refuses a
551 * fixed point that is not exponentially stable on that subspace. Double only:
552 * the eigenvalue test and the Bartels-Stewart solve are LAPACK-backed.
553 */
554inline Matrix<double> lna_covariance(const std::vector<double>& x, const std::vector<double>& p,
555 const std::vector<double>& m, std::size_t n, std::size_t h,
556 std::size_t dim) {
557 if (x.size() != dim) throw InputError("cache_rmf_lna: the fixed point has the wrong length");
558
559 const Matrix<double> Fp = jacobian<double>(x, p, m, n, h);
560 Matrix<double> Q = noise_matrix<double>(x, p, m, n, h);
561 for (std::size_t i = 0; i < Q.rows(); ++i)
562 for (std::size_t j = 0; j < i; ++j) {
563 const double v = 0.5 * (Q(i, j) + Q(j, i));
564 Q(i, j) = v;
565 Q(j, i) = v;
566 }
567
568 const Matrix<double> Ui = lna_centered_basis(n);
569 const Matrix<double> Ul = lna_centered_basis(h + 1);
570 const Matrix<double> V = lna_kron(Ul, Ui); // item-major, i + k*n
571 if (V.cols() == 0) return Matrix<double>(dim, dim, 0.0);
572 if (V.rows() != dim)
573 throw InputError(
574 "cache_rmf_lna: the centred basis does not span the state dimension; n, h and dim "
575 "disagree");
576
577 const std::size_t d = V.cols();
578 Matrix<double> Ar(d, d, 0.0), Qr(d, d, 0.0);
579 for (std::size_t a = 0; a < d; ++a)
580 for (std::size_t b = 0; b < d; ++b) {
581 double sa = 0.0, sq = 0.0;
582 for (std::size_t i = 0; i < dim; ++i)
583 for (std::size_t j = 0; j < dim; ++j) {
584 sa += V(i, a) * Fp(i, j) * V(j, b);
585 sq += V(i, a) * Q(i, j) * V(j, b);
586 }
587 Ar(a, b) = sa;
588 Qr(a, b) = sq;
589 }
590 for (std::size_t i = 0; i < d; ++i)
591 for (std::size_t j = 0; j < i; ++j) {
592 const double v = 0.5 * (Qr(i, j) + Qr(j, i));
593 Qr(i, j) = v;
594 Qr(j, i) = v;
595 }
596
597 const std::vector<std::complex<double>> ev = eig_values(Ar);
598 double worst = -std::numeric_limits<double>::infinity();
599 for (std::size_t i = 0; i < ev.size(); ++i) worst = std::max(worst, ev[i].real());
600 if (worst >= -std::sqrt(2.220446049250313e-16))
601 throw InputError(
602 "cache_rmf_lna: the cache fluid fixed point is not exponentially stable on the "
603 "reachable subspace, so the occupancy process has no stationary covariance");
604
605 Matrix<double> ArT(d, d, 0.0);
606 for (std::size_t i = 0; i < d; ++i)
607 for (std::size_t j = 0; j < d; ++j) ArT(i, j) = Ar(j, i);
608 Matrix<double> Wr = lyap_schur(Ar, ArT, Qr);
609 for (std::size_t i = 0; i < d; ++i)
610 for (std::size_t j = 0; j < i; ++j) {
611 const double v = 0.5 * (Wr(i, j) + Wr(j, i));
612 Wr(i, j) = v;
613 Wr(j, i) = v;
614 }
615
616 Matrix<double> W(dim, dim, 0.0);
617 for (std::size_t i = 0; i < dim; ++i)
618 for (std::size_t j = 0; j < dim; ++j) {
619 double s = 0.0;
620 for (std::size_t a = 0; a < d; ++a)
621 for (std::size_t b = 0; b < d; ++b) s += V(i, a) * Wr(a, b) * V(j, b);
622 W(i, j) = s;
623 }
624 for (std::size_t i = 0; i < dim; ++i)
625 for (std::size_t j = 0; j < i; ++j) {
626 const double v = 0.5 * (W(i, j) + W(j, i));
627 W(i, j) = v;
628 W(j, i) = v;
629 }
630 return W;
631}
632
633/** Fills `W` with the LNA covariance when it is computable and finite; T != double leaves it empty. */
634template <class T>
635void lna_covariance_into(Matrix<T>& W, const std::vector<T>& x, const std::vector<T>& p,
636 const std::vector<T>& m, std::size_t n, std::size_t h, std::size_t dim) {
637 (void)W; (void)x; (void)p; (void)m; (void)n; (void)h; (void)dim;
638}
639
640template <>
641inline void lna_covariance_into<double>(Matrix<double>& W, const std::vector<double>& x,
642 const std::vector<double>& p, const std::vector<double>& m,
643 std::size_t n, std::size_t h, std::size_t dim) {
644 try {
645 Matrix<double> C = lna_covariance(x, p, m, n, h, dim);
646 for (std::size_t i = 0; i < C.rows(); ++i)
647 for (std::size_t j = 0; j < C.cols(); ++j)
648 if (!std::isfinite(C(i, j))) return;
649 W = C;
650 } catch (const Error&) {
651 // the reference's try/catch: Wcov stays empty
652 }
653}
654
655/** Dispatch: the SVD basis at double, the elimination basis otherwise. */
656template <class T>
657NullSpace<T> left_null_space_for(const Matrix<T>& Fp) {
658 return left_null_space(Fp);
659}
660
661template <>
662inline NullSpace<double> left_null_space_for<double>(const Matrix<double>& Fp) {
663 return left_null_space_svd(Fp);
664}
665
666// ---------------------------------------------------------------------------
667// The general access graph (`accost`).
668// ---------------------------------------------------------------------------
669/**
670 * `rmf_linear_graph`: the standard chain. Row 0 is miss admission (column 0 is
671 * reject, column 1+l admit to list l), row 1+i is a hit in list i, and the top
672 * list self-loops because a hit there moves nothing.
673 */
674template <class T>
675Matrix<T> linear_graph(std::size_t h) {
676 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
677 Matrix<T> g(h + 1, h + 1, zero);
678 g(0, 1) = one;
679 for (std::size_t a = 1; a + 1 <= h; ++a) g(a, a + 1) = one;
680 g(h, h) = one;
681 return g;
682}
683
684/**
685 * `rmf_build_item_graphs`: one (h+1)x(h+1) graph per item, the per-user graphs
686 * averaged by request rate and row-normalized.
687 *
688 * RETURNS EMPTY WHEN THE RESULT IS THE LINEAR CHAIN, and that is not an
689 * optimization. The linear chain is the only case for which the 1/N REFINEMENT
690 * is defined here -- its Jacobian, Hessian and noise matrix are all written for
691 * the chain drift -- so the caller must be able to tell "no graph was declared,
692 * take the refined path" from "a graph was declared, take the plain mean-field
693 * fixed point of the general drift". An empty return is that signal.
694 */
695template <class T>
696std::vector<Matrix<T> > build_item_graphs(const std::vector<std::vector<Matrix<T> > >& accost,
697 const Matrix<T>& lambda, std::size_t n, std::size_t h) {
698 std::vector<Matrix<T> > G;
699 if (accost.empty()) return G;
700 const T zero = num_traits<T>::from_int(0);
701 const Matrix<T> lin = linear_graph<T>(h);
702 const std::size_t u = accost.size();
703 std::vector<Matrix<T> > Gc(n, lin);
704 bool is_linear = true;
705 for (std::size_t k = 0; k < n; ++k) {
706 Matrix<T> num(h + 1, h + 1, zero);
707 T den = zero;
708 for (std::size_t v = 0; v < u; ++v) {
709 if (k >= accost[v].size()) continue;
710 const Matrix<T>& gvk = accost[v][k];
711 if (gvk.rows() == 0) continue;
712 if (gvk.rows() != h + 1 || gvk.cols() != h + 1)
713 throw InputError("cache_miss_rmf: an access graph is not (h+1)x(h+1)");
714 double w = (v < lambda.rows() && k < lambda.cols())
715 ? num_traits<T>::to_double(lambda(v, k))
716 : 0.0;
717 if (!std::isfinite(w)) w = 0.0;
718 const T wv = num_traits<T>::from_double(w);
719 for (std::size_t a = 0; a <= h; ++a)
720 for (std::size_t b = 0; b <= h; ++b) num(a, b) += wv * gvk(a, b);
721 den += wv;
722 }
723 Matrix<T> gk = lin;
724 if (den > zero) {
725 for (std::size_t a = 0; a <= h; ++a)
726 for (std::size_t b = 0; b <= h; ++b) gk(a, b) = num(a, b) / den;
727 } else if (!accost.empty() && k < accost[0].size() && accost[0][k].rows() == h + 1) {
728 gk = accost[0][k];
729 }
730 for (std::size_t a = 0; a <= h; ++a) {
731 T srow = zero;
732 for (std::size_t b = 0; b <= h; ++b) srow += gk(a, b);
733 if (srow > zero)
734 for (std::size_t b = 0; b <= h; ++b) gk(a, b) = gk(a, b) / srow;
735 }
736 Gc[k] = gk;
737 for (std::size_t a = 0; a <= h && is_linear; ++a)
738 for (std::size_t b = 0; b <= h && is_linear; ++b)
739 if (std::fabs(num_traits<T>::to_double(gk(a, b)) -
740 num_traits<T>::to_double(lin(a, b))) >= 1e-9)
741 is_linear = false;
742 }
743 if (!is_linear) G = Gc;
744 return G;
745}
746
747/**
748 * `rmf_drift_graph`: the general RANDOM(m) drift under a per-item access graph.
749 *
750 * A miss is admitted to list i with probability G[k](0,1+i) and a hit in list s
751 * promotes to list i with probability G[k](1+s,1+i); the occupant it displaces
752 * is drawn UNIFORMLY from the target list, which is the RR sample path
753 * (`State.afterEventCache`) and is why every displacement term carries the
754 * 1/m(i) factor. It reduces to the chain drift above when G is the linear
755 * graph, which is what makes `build_item_graphs`'s emptiness test sound.
756 */
757template <class T>
758std::vector<T> drift_graph(const std::vector<T>& x_in, const std::vector<T>& p,
759 const std::vector<Matrix<T> >& G, const std::vector<T>& m,
760 std::size_t n, std::size_t h) {
761 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
762 const std::size_t model_dim = n * (h + 1);
763 std::vector<T> x = x_in;
764 for (std::size_t a = 0; a < x.size(); ++a) {
765 if (x[a] < zero) x[a] = zero;
766 if (x[a] > one) x[a] = one;
767 }
768 // A(s,i): total rate of insertion or promotion into list i out of list s.
769 Matrix<T> A(h + 1, h + 1, zero);
770 for (std::size_t s = 0; s <= h; ++s)
771 for (std::size_t j = 0; j < n; ++j) {
772 const T xjs = x[cache_miss_rmf_index(j, s, n)];
773 if (xjs == zero) continue;
774 for (std::size_t i = 1; i <= h; ++i) A(s, i) += p[j] * xjs * G[j](s, i);
775 }
776 std::vector<T> dX(model_dim, zero);
777 for (std::size_t k = 0; k < n; ++k) {
778 const T outk = x[cache_miss_rmf_index(k, 0, n)];
779 for (std::size_t i = 1; i <= h; ++i) {
780 const T xki = x[cache_miss_rmf_index(k, i, n)];
781 T infl = p[k] * outk * G[k](0, i);
782 for (std::size_t s = 1; s + 1 <= i; ++s)
783 infl += p[k] * x[cache_miss_rmf_index(k, s, n)] * G[k](s, i);
784 for (std::size_t b = i + 1; b <= h; ++b)
785 infl += A(i, b) * x[cache_miss_rmf_index(k, b, n)] / m[b - 1];
786 T outfl = p[k] * xki * T(one - G[k](i, i));
787 T disp = zero;
788 for (std::size_t s = 0; s + 1 <= i; ++s) disp += A(s, i);
789 outfl += disp * xki / m[i - 1];
790 dX[cache_miss_rmf_index(k, i, n)] += infl - outfl;
791 }
792 T acc = zero;
793 for (std::size_t i = 1; i <= h; ++i) acc += dX[cache_miss_rmf_index(k, i, n)];
794 dX[cache_miss_rmf_index(k, 0, n)] = -acc;
795 }
796 return dX;
797}
798
799/**
800 * `rmf_fixed_point_graph`: the plain mean-field fixed point of the general
801 * drift, at the reference's own horizon of 20000 (twice the chain's, because
802 * the general drift has no refinement to fall back on if it stops short).
803 */
804template <class T>
805std::vector<T> fixed_point_graph(const std::vector<T>& x0, const std::vector<T>& p,
806 const std::vector<Matrix<T> >& G, const std::vector<T>& m,
807 std::size_t n, std::size_t h) {
808 OdeOptions<T> opt;
809 opt.rtol = num_traits<T>::from_double(1e-8);
810 opt.atol = num_traits<T>::from_double(1e-10);
811 opt.store_trajectory = false;
812 const auto f = [&](const T& t, const std::vector<T>& x) {
813 (void)t;
814 return drift_graph(x, p, G, m, n, h);
815 };
816 return ode_rosenbrock4(f, T(num_traits<T>::from_int(0)),
817 T(num_traits<T>::from_int(20000)), x0, opt)
818 .final_state();
819}
820
821} // namespace rmf_detail
822
823/**
824 * Refined mean-field miss rates of a RANDOM(m) multi-list cache.
825 *
826 * @param gamma item access factors. Present for signature compatibility with
827 * cache_miss_rmf.m, which marks it unused and reads
828 * only its size; nothing here depends on it either.
829 * @param m_in (h) list capacities
830 * @param lambda (u x n_items) per-user per-item request rates. The MATLAB
831 * argument is a three-dimensional array and the function reads
832 * only its first page, lambda(v,:,1); this is that page.
833 * @param tmax integration horizon for the fixed point (reference: 1e4)
834 * @param accost per-(user,item) access graph, each an (h+1)x(h+1) matrix; empty
835 * is the linear chain. A NON-LINEAR graph switches the solve to
836 * the general drift AND drops the 1/N refinement, exactly as the
837 * reference does: the refinement's Jacobian, Hessian and noise
838 * matrix are written for the chain drift, so applying it to
839 * another drift would correct the wrong system.
840 */
841template <class T>
842CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m_in,
843 const Matrix<T>& lambda, const T& tmax,
844 const std::vector<std::vector<Matrix<T> > >& accost) {
846 "cache_miss_rmf requires transcendental arithmetic: its fixed point is reached "
847 "by a tolerance-driven integration of the mean-field drift");
848 (void)gamma;
849 const T zero = num_traits<T>::from_int(0);
850 const T one = num_traits<T>::from_int(1);
851 const std::size_t u = lambda.rows();
852 const std::size_t n_items = lambda.cols();
853 const std::size_t h = m_in.size();
854 if (u == 0 || n_items == 0) throw InputError("cache_miss_rmf: empty request-rate matrix");
855 if (h == 0) throw InputError("cache_miss_rmf: at least one cache list is required");
856
857 std::vector<T> m(h, zero);
858 for (std::size_t k = 0; k < h; ++k) {
859 if (m_in[k] <= 0) throw InputError("cache_miss_rmf: a list has non-positive capacity");
860 m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
861 }
862
863 // non-finite rate rejection rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
864 std::vector<T> lam_i(n_items, zero);
865 T lam_tot = zero;
866 for (std::size_t v = 0; v < u; ++v)
867 for (std::size_t i = 0; i < n_items; ++i) {
868 lam_i[i] += lambda(v, i);
869 lam_tot += lambda(v, i);
870 }
871 if (lam_tot == zero) throw InputError("cache_miss_rmf: all request rates are zero");
872 std::vector<T> p(n_items, zero);
873 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
874
875 const std::size_t model_dim = n_items * (h + 1);
876
877 // Initial occupancy: the first m(1) items in list 1, the next m(2) in list
878 // 2, and so on; every remaining item outside the cache.
879 std::vector<T> x0(model_dim, zero);
880 std::size_t obj = 0;
881 for (std::size_t k = 1; k <= h; ++k)
882 for (int jj = 0; jj < m_in[k - 1]; ++jj) {
883 ++obj;
884 if (obj <= n_items) x0[cache_miss_rmf_index(obj - 1, k, n_items)] = one;
885 }
886 for (std::size_t i = obj; i < n_items; ++i) x0[cache_miss_rmf_index(i, 0, n_items)] = one;
887
889 const std::vector<Matrix<T> > G = rmf_detail::build_item_graphs(accost, lambda, n_items, h);
890 if (!G.empty()) {
891 // A declared access graph takes the general drift and the plain fixed
892 // point; `refined` stays false, which is the honest report.
893 res.xss = rmf_detail::fixed_point_graph(x0, p, G, m, n_items, h);
894 res.pi0.assign(n_items, zero);
895 for (std::size_t i = 0; i < n_items; ++i) {
896 T v = res.xss[cache_miss_rmf_index(i, 0, n_items)];
897 if (v < zero) v = zero;
898 if (v > one) v = one;
899 res.pi0[i] = v;
900 }
901 res.MI.assign(n_items, zero);
902 res.M = zero;
903 for (std::size_t i = 0; i < n_items; ++i) {
904 res.MI[i] = lam_i[i] * res.pi0[i];
905 res.M += res.MI[i];
906 }
907 res.MU.assign(u, zero);
908 for (std::size_t v = 0; v < u; ++v) {
909 T s = zero;
910 for (std::size_t i = 0; i < n_items; ++i) s += lambda(v, i) * res.pi0[i];
911 res.MU[v] = s;
912 }
913 return res;
914 }
915 // One integration pass at the reference's tolerances (RelTol 1e-8,
916 // AbsTol 1e-10), as rmf_fixed_point does; no second polishing pass, so the
917 // rank decision below sees the same fixed point the reference sees.
918 std::vector<T> xss = rmf_detail::fixed_point(x0, p, m, n_items, h, tmax);
919
920 // 1/N refinement failure rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
921 try {
922 const Matrix<T> Fp = rmf_detail::jacobian(xss, p, m, n_items, h);
923 const rmf_detail::NullSpace<T> ns = rmf_detail::left_null_space_for(Fp);
924 const std::size_t rk = ns.rank;
925 if (rk == 0 || rk >= model_dim)
926 throw NumericError(ErrorKind{"Degenerate"}, "cache_miss_rmf: the reduction is degenerate");
927 const std::vector<T> Fpp = rmf_detail::hessian(p, m, n_items, h);
928 const Matrix<T> Q = rmf_detail::noise_matrix(xss, p, m, n_items, h);
929
930 // change-of-basis rationale: see _kb/09-ldes-and-cache.md (cpp port notes)
931 Matrix<T> C(model_dim, model_dim, zero);
932 {
933 std::size_t d = 0;
934 for (std::size_t l = 0; l <= h && d < rk; ++l)
935 for (std::size_t i = 0; i + 1 < n_items && d < rk; ++i, ++d)
936 C(d, cache_miss_rmf_index(i, l, n_items)) = one;
937 if (d != rk) throw NumericError(ErrorKind{"Degenerate"}, "cache_miss_rmf: the reduction basis is too small");
938 }
939 if (ns.basis.size() != model_dim - rk)
940 throw NumericError(ErrorKind{"Degenerate"}, "cache_miss_rmf: the null-space basis has the wrong size");
941 for (std::size_t i = 0; i < ns.basis.size(); ++i)
942 for (std::size_t j = 0; j < model_dim; ++j) C(rk + i, j) = ns.basis[i][j];
943 const Matrix<T> Cinv = inverse(C);
944
945 Matrix<T> Fp_r(rk, rk, zero);
946 {
947 const Matrix<T> tmp = matmul(C, matmul(Fp, Cinv));
948 for (std::size_t a = 0; a < rk; ++a)
949 for (std::size_t b = 0; b < rk; ++b) Fp_r(a, b) = tmp(a, b);
950 }
951 Matrix<T> Q_r(rk, rk, zero);
952 {
953 Matrix<T> Ct(model_dim, model_dim, zero);
954 for (std::size_t a = 0; a < model_dim; ++a)
955 for (std::size_t b = 0; b < model_dim; ++b) Ct(a, b) = C(b, a);
956 const Matrix<T> tmp = matmul(C, matmul(Q, Ct));
957 for (std::size_t a = 0; a < rk; ++a)
958 for (std::size_t b = 0; b < rk; ++b) Q_r(a, b) = tmp(a, b);
959 }
960
961 // Reduced Hessian, by the same three contractions the reference does.
962 const auto Hat = [model_dim](std::size_t a, std::size_t b, std::size_t c) {
963 return (a * model_dim + b) * model_dim + c;
964 };
965 std::vector<T> tmp1(rk * model_dim * model_dim, zero);
966 for (std::size_t a = 0; a < rk; ++a)
967 for (std::size_t j = 0; j < model_dim; ++j)
968 for (std::size_t k = 0; k < model_dim; ++k) {
969 T s = zero;
970 for (std::size_t i = 0; i < model_dim; ++i) s += C(a, i) * Fpp[Hat(i, j, k)];
971 tmp1[(a * model_dim + j) * model_dim + k] = s;
972 }
973 std::vector<T> tmp2(rk * rk * model_dim, zero);
974 for (std::size_t a = 0; a < rk; ++a)
975 for (std::size_t b = 0; b < rk; ++b)
976 for (std::size_t k = 0; k < model_dim; ++k) {
977 T s = zero;
978 for (std::size_t j = 0; j < model_dim; ++j)
979 s += tmp1[(a * model_dim + j) * model_dim + k] * Cinv(j, b);
980 tmp2[(a * rk + b) * model_dim + k] = s;
981 }
982 std::vector<T> Fpp_r(rk * rk * rk, zero);
983 for (std::size_t a = 0; a < rk; ++a)
984 for (std::size_t b = 0; b < rk; ++b)
985 for (std::size_t c = 0; c < rk; ++c) {
986 T s = zero;
987 for (std::size_t k = 0; k < model_dim; ++k)
988 s += tmp2[(a * rk + b) * model_dim + k] * Cinv(k, c);
989 Fpp_r[(a * rk + b) * rk + c] = s;
990 }
991
992 const Matrix<T> W_r = rmf_detail::lyapunov(Fp_r, Q_r);
993
994 std::vector<T> C_r(rk, zero);
995 for (std::size_t a = 0; a < rk; ++a) {
996 T s = zero;
997 for (std::size_t b = 0; b < rk; ++b)
998 for (std::size_t c = 0; c < rk; ++c) s += Fpp_r[(a * rk + b) * rk + c] * W_r(b, c);
999 C_r[a] = s;
1000 }
1001 std::vector<T> rhs(rk, zero);
1002 for (std::size_t a = 0; a < rk; ++a)
1003 rhs[a] = -C_r[a] / num_traits<T>::from_int(2);
1004 const std::vector<T> V_r = line::solve(Fp_r, rhs);
1005
1006 std::vector<T> xref(model_dim, zero);
1007 bool finite = true;
1008 for (std::size_t i = 0; i < model_dim; ++i) {
1009 T s = zero;
1010 for (std::size_t a = 0; a < rk; ++a) s += Cinv(i, a) * V_r[a];
1011 xref[i] = xss[i] + s / num_traits<T>::from_int(static_cast<long>(n_items));
1012 if (!std::isfinite(num_traits<T>::to_double(xref[i]))) finite = false;
1013 }
1014 // the reference accepts the refinement only when all(isfinite(xref))
1015 if (finite) {
1016 xss = xref;
1017 res.refined = true;
1018 }
1019 } catch (const NumericError&) {
1020 // keep the plain mean-field fixed point, as the reference does
1021 } catch (const InputError&) {
1022 // ditto: a degenerate reduction is not an error of the caller's making
1023 }
1024
1025 // Wcov: the LNA stationary covariance at the occupancy the metrics are
1026 // read from (rmf_lna_covariance), kept only when finite, as the reference
1027 // does inside its try/catch. The eigenvalue test and the Lyapunov solve
1028 // run at double, so the field is filled at T = double only.
1029 rmf_detail::lna_covariance_into(res.Wcov, xss, p, m, n_items, h, model_dim);
1030
1031 res.xss = xss;
1032 res.pi0.assign(n_items, zero);
1033 for (std::size_t i = 0; i < n_items; ++i) {
1034 T v = xss[cache_miss_rmf_index(i, 0, n_items)];
1035 if (v < zero) v = zero;
1036 if (v > one) v = one;
1037 res.pi0[i] = v;
1038 }
1039 res.MI.assign(n_items, zero);
1040 res.M = zero;
1041 for (std::size_t i = 0; i < n_items; ++i) {
1042 res.MI[i] = lam_i[i] * res.pi0[i];
1043 res.M += res.MI[i];
1044 }
1045 res.MU.assign(u, zero);
1046 for (std::size_t v = 0; v < u; ++v) {
1047 T s = zero;
1048 for (std::size_t i = 0; i < n_items; ++i) s += lambda(v, i) * res.pi0[i];
1049 res.MU[v] = s;
1050 }
1051 return res;
1052}
1053
1054/** cache_miss_rmf on the linear chain, i.e. with no declared access graph. */
1055template <class T>
1056CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m_in,
1057 const Matrix<T>& lambda, const T& tmax) {
1058 return cache_miss_rmf(gamma, m_in, lambda, tmax, std::vector<std::vector<Matrix<T> > >());
1059}
1060
1061/** cache_miss_rmf with the reference horizon tmax = 1e4. */
1062template <class T>
1063CacheMissRmfResult<T> cache_miss_rmf(const std::vector<T>& gamma, const std::vector<int>& m,
1064 const Matrix<T>& lambda) {
1065 return cache_miss_rmf(gamma, m, lambda, T(num_traits<T>::from_int(10000)));
1066}
1067
1068/**
1069 * Transient mean-field trajectory over [t0,t1] from a given initial occupancy,
1070 * the optional TSPAN/X0INIT path of cache_miss_rmf.m. Fills tout, xtraj, pi0_t
1071 * and MU_t of the result; the steady-state fields are left at their defaults
1072 * because the reference computes them independently of the transient.
1073 *
1074 * A non-empty `clk` also fills `sojourn`: the clock-weighted average of the
1075 * transient, integrated with the drift (`cache_sojourn_ode`), and its per-user
1076 * miss rate, which is the weighted average of MU_t because MU_t is affine in x.
1077 */
1078template <class T>
1080 const Matrix<T>& lambda, const T& t0, const T& t1,
1081 const std::vector<T>& x0init,
1082 const std::vector<std::vector<Matrix<T> > >& accost,
1085 "cache_miss_rmf_transient requires transcendental arithmetic");
1086 const T zero = num_traits<T>::from_int(0);
1087 const T one = num_traits<T>::from_int(1);
1088 const std::size_t u = lambda.rows();
1089 const std::size_t n_items = lambda.cols();
1090 const std::size_t h = m_in.size();
1091 const std::size_t model_dim = n_items * (h + 1);
1092 if (x0init.size() != model_dim)
1093 throw InputError("cache_miss_rmf_transient: initial occupancy has the wrong length");
1094
1095 std::vector<T> m(h, zero);
1096 for (std::size_t k = 0; k < h; ++k) m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
1097
1098 std::vector<T> lam_i(n_items, zero);
1099 T lam_tot = zero;
1100 for (std::size_t v = 0; v < u; ++v)
1101 for (std::size_t i = 0; i < n_items; ++i) {
1102 lam_i[i] += lambda(v, i);
1103 lam_tot += lambda(v, i);
1104 }
1105 std::vector<T> p(n_items, zero);
1106 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
1107
1109 opt.rtol = num_traits<T>::from_double(1e-8);
1110 opt.atol = num_traits<T>::from_double(1e-10);
1111 // The transient follows the SAME drift the fixed point is taken on: the
1112 // general one under a declared access graph, the chain drift otherwise.
1113 const std::vector<Matrix<T> > G = rmf_detail::build_item_graphs(accost, lambda, n_items, h);
1114 const auto f = [&](const T& t, const std::vector<T>& x) {
1115 (void)t;
1116 if (!G.empty()) return rmf_detail::drift_graph(x, p, G, m, n_items, h);
1117 return rmf_detail::drift(x, p, m, n_items, h);
1118 };
1119 const OdeSolution<T> s = ode_rosenbrock4(f, t0, t1, x0init, opt);
1120
1122 const std::size_t nt = s.t.size();
1123 res.tout = s.t;
1124 res.xtraj = Matrix<T>(model_dim, nt, zero);
1125 for (std::size_t j = 0; j < nt; ++j)
1126 for (std::size_t i = 0; i < model_dim; ++i) res.xtraj(i, j) = s.y[j][i];
1127 res.pi0_t = Matrix<T>(n_items, nt, zero);
1128 for (std::size_t i = 0; i < n_items; ++i)
1129 for (std::size_t j = 0; j < nt; ++j) {
1130 T v = s.y[j][cache_miss_rmf_index(i, 0, n_items)];
1131 if (v < zero) v = zero;
1132 if (v > one) v = one;
1133 res.pi0_t(i, j) = v;
1134 }
1135 res.MU_t = Matrix<T>(u, nt, zero);
1136 for (std::size_t v = 0; v < u; ++v)
1137 for (std::size_t j = 0; j < nt; ++j) {
1138 T acc = zero;
1139 for (std::size_t i = 0; i < n_items; ++i) acc += lambda(v, i) * res.pi0_t(i, j);
1140 res.MU_t(v, j) = acc;
1141 }
1142 if (!clk.empty()) {
1143 res.sojourn = cache_sojourn_ode(f, t0, t1, x0init, clk);
1144 if (!res.sojourn.empty()) {
1145 res.sojourn.MU.assign(u, zero);
1146 for (std::size_t v = 0; v < u; ++v) {
1147 T acc = zero;
1148 for (std::size_t i = 0; i < n_items; ++i) {
1149 T pb = res.sojourn.xbar[cache_miss_rmf_index(i, 0, n_items)];
1150 if (pb < zero) pb = zero;
1151 if (pb > one) pb = one;
1152 acc += lambda(v, i) * pb;
1153 }
1154 res.sojourn.MU[v] = acc;
1155 }
1156 }
1157 }
1158 res.M = zero;
1159 return res;
1160}
1161
1162/** cache_miss_rmf_transient on the linear chain, i.e. with no declared access graph. */
1163template <class T>
1165 const Matrix<T>& lambda, const T& t0, const T& t1,
1166 const std::vector<T>& x0init,
1168 return cache_miss_rmf_transient(m_in, lambda, t0, t1, x0init,
1169 std::vector<std::vector<Matrix<T> > >(), clk);
1170}
1171
1172/** Result of `cache_miss_rmf_expansion_transient`. */
1173template <class T>
1175 std::vector<T> t; ///< (n_points) output instants, t[0] = 0
1176 Matrix<T> X; ///< (n_points x model_dim) mean-field trajectory
1177 Matrix<T> V; ///< (n_points x model_dim) 1/N correction trajectory
1178 std::vector<Matrix<T> > W; ///< (n_points) covariance, each model_dim x model_dim
1179};
1180
1181/**
1182 * Refined mean-field TRANSIENT, `CacheRMF.meanFieldExpansionTransient`.
1183 *
1184 * The steady-state refinement solves F' V = -(1/2) sum F''_bc W_bc at the fixed
1185 * point; the transient one carries the same three objects along the trajectory,
1186 * as one coupled system in y = [X (d), V (d), W (d x d)]:
1187 *
1188 * dX/dt = F(X),
1189 * dV/dt = F'(X) V + (1/2) sum_{b,c} F''_{a,b,c} W_{b,c},
1190 * dW/dt = F'(X) W + W F'(X)^T + Q(X),
1191 *
1192 * started from V(0) = 0, W(0) = 0 at the same initial occupancy the fixed point
1193 * uses: the first m(1) items in list 1, the next m(2) in list 2, the rest
1194 * outside. The reported trajectory is X(t) + V(t)/N.
1195 *
1196 * `order = 0` integrates the drift alone and returns V and W identically zero,
1197 * which is the reference's own escape rather than a degenerate case of the
1198 * coupled system.
1199 *
1200 * DOUBLE ONLY, and for the reason the fluid solvers are: the coupled system is
1201 * integrated by LSODA at the reference's own `ode15s` tolerances (RelTol 1e-6,
1202 * AbsTol 1e-10) on the reference's own output grid `linspace(0, time,
1203 * n_points)`, and LSODA's coefficients assume double precision.
1204 *
1205 * THE HESSIAN IS HOISTED OUT OF THE RIGHT-HAND SIDE. The drift is quadratic, so
1206 * F'' does not depend on x -- the reference recomputes it per step, which is
1207 * d^3 work per evaluation for a value that never changes.
1208 */
1209template <class T>
1211 const std::vector<int>& m_in, const Matrix<T>& lambda, const T& time, std::size_t n_points,
1212 int order) {
1213 if (!std::is_same<T, double>::value)
1214 throw UnsupportedError(ErrorKind{"Arithmetic"},
1215 "cache_miss_rmf_expansion_transient: the coupled (X,V,W) system is integrated with "
1216 "LSODA, whose coefficients assume double precision; rerun with --arith double");
1217 const T zero = num_traits<T>::from_int(0);
1218 const std::size_t u = lambda.rows();
1219 const std::size_t n_items = lambda.cols();
1220 const std::size_t h = m_in.size();
1221 if (u == 0 || n_items == 0)
1222 throw InputError("cache_miss_rmf_expansion_transient: empty request-rate matrix");
1223 if (h == 0)
1224 throw InputError("cache_miss_rmf_expansion_transient: at least one cache list is required");
1225 if (n_points < 2)
1226 throw InputError("cache_miss_rmf_expansion_transient: at least two output points are "
1227 "required to describe a trajectory");
1228 if (!(num_traits<T>::to_double(time) > 0.0))
1229 throw InputError("cache_miss_rmf_expansion_transient: the horizon must be positive");
1230
1231 std::vector<T> m(h, zero);
1232 for (std::size_t k = 0; k < h; ++k) {
1233 if (m_in[k] <= 0)
1234 throw InputError("cache_miss_rmf_expansion_transient: a list has non-positive capacity");
1235 m[k] = num_traits<T>::from_int(static_cast<long>(m_in[k]));
1236 }
1237
1238 std::vector<T> lam_i(n_items, zero);
1239 T lam_tot = zero;
1240 for (std::size_t v = 0; v < u; ++v)
1241 for (std::size_t i = 0; i < n_items; ++i) {
1242 lam_i[i] += lambda(v, i);
1243 lam_tot += lambda(v, i);
1244 }
1245 if (lam_tot == zero)
1246 throw InputError("cache_miss_rmf_expansion_transient: all request rates are zero");
1247 std::vector<T> p(n_items, zero);
1248 for (std::size_t i = 0; i < n_items; ++i) p[i] = lam_i[i] / lam_tot;
1249
1250 const std::size_t d = n_items * (h + 1);
1251
1252 // The same initial occupancy `cache_miss_rmf` starts its fixed point from.
1253 std::vector<T> x0(d, zero);
1254 {
1255 std::size_t obj = 0;
1256 for (std::size_t k = 1; k <= h; ++k)
1257 for (int jj = 0; jj < m_in[k - 1]; ++jj) {
1258 ++obj;
1259 if (obj <= n_items)
1260 x0[cache_miss_rmf_index(obj - 1, k, n_items)] = num_traits<T>::from_int(1);
1261 }
1262 for (std::size_t i = obj; i < n_items; ++i)
1263 x0[cache_miss_rmf_index(i, 0, n_items)] = num_traits<T>::from_int(1);
1264 }
1265
1266 std::vector<double> grid(n_points, 0.0);
1267 const double tend = num_traits<T>::to_double(time);
1268 for (std::size_t j = 0; j < n_points; ++j)
1269 grid[j] = tend * static_cast<double>(j) / static_cast<double>(n_points - 1);
1270
1271 const std::size_t total = order == 0 ? d : d + d + d * d;
1272 std::vector<double> y0(total, 0.0);
1273 for (std::size_t i = 0; i < d; ++i) y0[i] = num_traits<T>::to_double(x0[i]);
1274
1275 const std::vector<T> Fpp = order == 0 ? std::vector<T>() : rmf_detail::hessian(p, m, n_items, h);
1276
1277 const auto rhs = [&](double t, const double* y, double* dy) {
1278 (void)t;
1279 std::vector<T> x(d, zero);
1280 for (std::size_t i = 0; i < d; ++i) x[i] = num_traits<T>::from_double(y[i]);
1281 const std::vector<T> F = rmf_detail::drift(x, p, m, n_items, h);
1282 for (std::size_t i = 0; i < d; ++i) dy[i] = num_traits<T>::to_double(F[i]);
1283 if (order == 0) return;
1284 const Matrix<T> Fp = rmf_detail::jacobian(x, p, m, n_items, h);
1285 const Matrix<T> Q = rmf_detail::noise_matrix(x, p, m, n_items, h);
1286 for (std::size_t a = 0; a < d; ++a) {
1287 T acc = zero;
1288 for (std::size_t b = 0; b < d; ++b) acc += Fp(a, b) * num_traits<T>::from_double(y[d + b]);
1289 // 0.5 * sum_{b,c} F''_{a,b,c} W_{b,c}
1290 T hcontr = zero;
1291 for (std::size_t b = 0; b < d; ++b)
1292 for (std::size_t c = 0; c < d; ++c) {
1293 const T hv = Fpp[(a * d + b) * d + c];
1294 if (hv == zero) continue;
1295 hcontr += hv * num_traits<T>::from_double(y[2 * d + b * d + c]);
1296 }
1297 dy[d + a] = num_traits<T>::to_double(acc) +
1298 0.5 * num_traits<T>::to_double(hcontr);
1299 }
1300 for (std::size_t a = 0; a < d; ++a)
1301 for (std::size_t b = 0; b < d; ++b) {
1302 T acc = Q(a, b);
1303 for (std::size_t c = 0; c < d; ++c)
1304 acc += Fp(a, c) * num_traits<T>::from_double(y[2 * d + c * d + b]) +
1305 num_traits<T>::from_double(y[2 * d + a * d + c]) * Fp(b, c);
1306 dy[2 * d + a * d + b] = num_traits<T>::to_double(acc);
1307 }
1308 };
1309
1310 LsodaOptions lopt;
1311 lopt.rtol = 1e-6;
1312 lopt.atol = 1e-10;
1313 const LsodaSolution s = lsoda_integrate(rhs, y0, grid, lopt);
1314 if (!s.success || s.y.size() != n_points)
1315 throw NumericError(ErrorKind{"NonConvergence"}, "cache_miss_rmf_expansion_transient: the coupled (X,V,W) system could "
1316 "not be integrated over the requested horizon");
1317
1319 out.t.assign(n_points, zero);
1320 out.X = Matrix<T>(n_points, d, zero);
1321 out.V = Matrix<T>(n_points, d, zero);
1322 out.W.assign(n_points, Matrix<T>(d, d, zero));
1323 for (std::size_t j = 0; j < n_points; ++j) {
1324 out.t[j] = num_traits<T>::from_double(s.t[j]);
1325 for (std::size_t i = 0; i < d; ++i) {
1326 out.X(j, i) = num_traits<T>::from_double(s.y[j][i]);
1327 if (order != 0) out.V(j, i) = num_traits<T>::from_double(s.y[j][d + i]);
1328 }
1329 if (order == 0) continue;
1330 for (std::size_t a = 0; a < d; ++a)
1331 for (std::size_t b = 0; b < d; ++b)
1332 out.W[j](a, b) = num_traits<T>::from_double(s.y[j][2 * d + a * d + b]);
1333 }
1334 return out;
1335}
1336
1337} // namespace cache
1338} // namespace line
1339
1340#endif // LINE_API_CACHE_CACHE_MISS_RMF_H
Grid-independent sojourn-weighted average of a cache mean-field transient.
Malformed or inconsistent input (dimensions, negative populations, ...): LINE:input:<kind>.
Definition error.h:103
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The algorithm cannot produce a trustworthy answer on this instance: LINE:numerical:<kind>.
Definition error.h:110
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LSODA: the LINE-facing wrapper over the vendored solver in third_party/lsoda.hpp.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
CacheMissRmfResult< T > cache_miss_rmf(const std::vector< T > &gamma, const std::vector< int > &m_in, const Matrix< T > &lambda, const T &tmax, const std::vector< std::vector< Matrix< T > > > &accost)
Refined mean-field miss rates of a RANDOM(m) multi-list cache.
CacheRmfExpansionTransient< T > cache_miss_rmf_expansion_transient(const std::vector< int > &m_in, const Matrix< T > &lambda, const T &time, std::size_t n_points, int order)
Refined mean-field TRANSIENT, CacheRMF.meanFieldExpansionTransient.
CacheSojournResult< T > cache_sojourn_ode(const F &f, const T &t0, const T &t1, const std::vector< T > &x0, const CacheSojournClock< T > &clk)
Integrates dx/dt = f(t, x) over [t0, t1] from x0 together with clk; see the file comment.
CacheMissRmfResult< T > cache_miss_rmf_transient(const std::vector< int > &m_in, const Matrix< T > &lambda, const T &t0, const T &t1, const std::vector< T > &x0init, const std::vector< std::vector< Matrix< T > > > &accost, const CacheSojournClock< T > &clk=CacheSojournClock< T >())
Transient mean-field trajectory over [t0,t1] from a given initial occupancy, the optional TSPAN/X0INI...
std::size_t cache_miss_rmf_index(std::size_t i, std::size_t k, std::size_t n_items)
Flat index of (item i, list k), k = 0 meaning "not cached" (rmf_index.m).
double dot(const std::vector< double > &a, const std::vector< double > &b)
The inner product of a row vector with a column held as a vector.
Definition mg1.h:240
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_abs(const T &v)
Definition number.h:184
OdeSolution< T > ode_rosenbrock4(const F &f, const J &jac, const T &t0, const T &t1, const std::vector< T > &y0, const OdeOptions< T > &opt)
Integrate y' = f(t,y) from t0 to t1 with an analytic Jacobian.
Definition ode.h:303
LsodaSolution lsoda_integrate(const LsodaRhs &f, const std::vector< double > &y0, const std::vector< double > &t_eval, const LsodaOptions &opt=LsodaOptions())
Integrate dy/dt = f(t, y) from t_eval.front() through every later entry of t_eval,...
Definition lsoda.h:178
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
Definition eig.h:59
Matrix< double > lyap_schur(const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C)
MATLAB lyap(A,B,C) at double via Bartels-Stewart: A X + X B + C = 0.
Definition sylvester.h:207
Number-type abstraction for the templated API port.
Adaptive stiff ODE integrator: a four-stage Rosenbrock method of order four with an embedded order-th...
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Integration controls.
Definition lsoda.h:64
double atol
absolute tolerance, applied to every component
Definition lsoda.h:66
double rtol
relative tolerance, applied to every component
Definition lsoda.h:65
Result of an integration, mirroring OdeSolution in ode.h.
Definition lsoda.h:126
bool success
false when LSODA returned istate < 0
Definition lsoda.h:132
std::vector< std::vector< double > > y
y[i] is the state at t[i]
Definition lsoda.h:128
std::vector< double > t
output times, t[0] = t_eval[0]
Definition lsoda.h:127
Integration controls.
Definition ode.h:117
Result of an integration.
Definition ode.h:142
std::vector< std::vector< T > > y
y[i] is the state at t[i]
Definition ode.h:144
std::vector< T > t
accepted time points, t[0] = t0
Definition ode.h:143
Return value of cache_miss_rmf, mirroring [M,MU,MI,pi0,tout,pi0_t,MU_t,xtraj].
std::vector< T > pi0
(n_items) per-item miss probability, clipped to [0,1]
Matrix< T > pi0_t
(n_items x nt) transient miss probability
std::vector< T > tout
transient time grid, empty unless tspan was given
Matrix< T > xtraj
(model_dim x nt) transient occupancy
bool refined
true when the 1/N correction was accepted
Matrix< T > MU_t
(u x nt) transient per-user miss rate
Matrix< T > Wcov
LNA stationary covariance of the occupancy (model_dim x model_dim); empty when unavailable or not fin...
std::vector< T > MI
(n_items) per-item miss rate
CacheSojournResult< T > sojourn
Grid-independent average of the transient against a clock; empty unless one was given.
std::vector< T > xss
the occupancy the metrics were read from
std::vector< T > MU
(u) per-user miss rate
Result of cache_miss_rmf_expansion_transient.
std::vector< Matrix< T > > W
(n_points) covariance, each model_dim x model_dim
Matrix< T > X
(n_points x model_dim) mean-field trajectory
Matrix< T > V
(n_points x model_dim) 1/N correction trajectory
std::vector< T > t
(n_points) output instants, t[0] = 0
A phase-type clock: dphi/dt = phi A, phi(t0) = phi0, density phi c.
A sojourn average: xbar (empty when wtot is not positive), clock mass, per-user miss rate.
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).