LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
cache_rmf_lna.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_RMF_LNA_H
6#define LINE_API_CACHE_CACHE_RMF_LNA_H
7
8/**
9 * @file
10 * @ingroup api_cache
11 * Stationary covariance of a RANDOM(m) cache occupancy, under the LNA.
12 *
13 * Port of `cache_rmf_lna` in python/line_solver/api/cache/rmf.py, itself a twin
14 * of the MATLAB local `rmf_lna_covariance`. It solves the Lyapunov equation
15 *
16 * F'(x) W + W F'(x)' + Q(x) = 0
17 *
18 * with exactly the `jacobian` and `noise_matrix` that `cache_miss_rmf.h`
19 * already uses for the 1/N mean correction, so the mean and the covariance
20 * linearise about the IDENTICAL drift. Reusing those two rather than restating
21 * them is deliberate: the reference's `rmf_jacobian` is documented there as NOT
22 * being the derivative of `rmf_drift` at every entry, and a second transcription
23 * would silently pick the other one.
24 *
25 * THE SUBSPACE IS THE POINT, AND THE REASON THE SOLVE IS POSSIBLE AT ALL. The
26 * Jacobian is singular twice over, because a RANDOM(m) cache conserves two
27 * things: every item is in exactly one list (`sum_k x[i,k] = 1`) and every list
28 * holds exactly its capacity (`sum_i x[i,k] = m[k]`). Every jump is a SWAP,
29 * `(e_i - e_j) tensor (e_{k+1} - e_k)`, so the fluctuation lives on the tensor
30 * product of the zero-sum ITEM space with the zero-sum LIST space -- the
31 * double-centred subspace, of dimension `(n-1) * h`. Restricting to an
32 * orthonormal basis of it is EXACT, not a regularization, and it is what makes
33 * the covariance of a deterministic total come out as zero rather than as
34 * whatever a pseudo-inverse would have produced.
35 *
36 * A fixed point that is not exponentially stable ON THAT SUBSPACE has no
37 * stationary covariance, and is refused rather than answered.
38 *
39 * ARITHMETIC: double. The eigenvalue test and the Lyapunov solve are both
40 * floating point.
41 */
42
43#include <algorithm>
44#include <cmath>
45#include <cstddef>
46#include <limits>
47#include <vector>
48
50#include "line/util/eig.h"
51#include "line/util/error.h"
52#include "line/util/matrix.h"
53#include "line/util/sylvester.h"
54
55namespace line {
56namespace cache {
57
58namespace lnadetail {
59
60/**
61 * Orthonormal basis of `{u in R^n : sum(u) = 0}`, as an n-by-(n-1) matrix.
62 *
63 * Built by Gram-Schmidt on the centring projector's columns, which is what the
64 * reference's QR of `I - ones/n` produces. Any orthonormal basis of that
65 * subspace serves: the restricted solve is basis-independent because the answer
66 * is mapped back by `V W_r V'`.
67 */
68inline Matrix<double> centered_basis(std::size_t n) {
69 if (n <= 1) return Matrix<double>(n, 0, 0.0);
70 std::vector<std::vector<double>> cols;
71 for (std::size_t c = 0; c < n && cols.size() + 1 < n; ++c) {
72 std::vector<double> v(n, 0.0);
73 for (std::size_t i = 0; i < n; ++i)
74 v[i] = (i == c ? 1.0 : 0.0) - 1.0 / static_cast<double>(n);
75 for (std::size_t b = 0; b < cols.size(); ++b) {
76 double d = 0.0;
77 for (std::size_t i = 0; i < n; ++i) d += v[i] * cols[b][i];
78 for (std::size_t i = 0; i < n; ++i) v[i] -= d * cols[b][i];
79 }
80 double nv = 0.0;
81 for (std::size_t i = 0; i < n; ++i) nv += v[i] * v[i];
82 nv = std::sqrt(nv);
83 if (nv <= 1e-12) continue;
84 for (std::size_t i = 0; i < n; ++i) v[i] /= nv;
85 cols.push_back(v);
86 }
87 Matrix<double> U(n, cols.size(), 0.0);
88 for (std::size_t c = 0; c < cols.size(); ++c)
89 for (std::size_t i = 0; i < n; ++i) U(i, c) = cols[c][i];
90 return U;
91}
92
93/** `kron(A, B)` in MATLAB's ordering. */
94inline Matrix<double> kron_d(const Matrix<double>& A, const Matrix<double>& B) {
95 Matrix<double> K(A.rows() * B.rows(), A.cols() * B.cols(), 0.0);
96 for (std::size_t i = 0; i < A.rows(); ++i)
97 for (std::size_t j = 0; j < A.cols(); ++j)
98 for (std::size_t k = 0; k < B.rows(); ++k)
99 for (std::size_t l = 0; l < B.cols(); ++l)
100 K(i * B.rows() + k, j * B.cols() + l) = A(i, j) * B(k, l);
101 return K;
102}
103
104} // namespace lnadetail
105
106/**
107 * @brief Stationary covariance of a RANDOM(m) cache occupancy, under the LNA.
108 *
109 * @param x the fluid fixed point, flattened item-major (`i + k*n`)
110 * @param p per-item popularities
111 * @param m per-list capacities
112 * @param n number of items
113 * @param h number of cache lists
114 * @return (dim x dim) stationary covariance, symmetric
115 */
116inline Matrix<double> cache_rmf_lna(const std::vector<double>& x, const std::vector<double>& p,
117 const std::vector<double>& m, std::size_t n, std::size_t h,
118 std::size_t dim) {
119 if (x.size() != dim) throw InputError("cache_rmf_lna: the fixed point has the wrong length");
120
121 const Matrix<double> Fp = rmf_detail::jacobian<double>(x, p, m, n, h);
122 Matrix<double> Q = rmf_detail::noise_matrix<double>(x, p, m, n, h);
123 // Symmetrize: the noise intensity is symmetric by construction and the
124 // asymmetry that survives assembly is round-off.
125 for (std::size_t i = 0; i < Q.rows(); ++i)
126 for (std::size_t j = 0; j < i; ++j) {
127 const double v = 0.5 * (Q(i, j) + Q(j, i));
128 Q(i, j) = v;
129 Q(j, i) = v;
130 }
131
132 // The double-centred subspace: zero-sum over items, zero-sum over lists.
133 const Matrix<double> Ui = lnadetail::centered_basis(n);
134 const Matrix<double> Ul = lnadetail::centered_basis(h + 1);
135 const Matrix<double> V = lnadetail::kron_d(Ul, Ui); // item-major, i + k*n
136 if (V.cols() == 0) return Matrix<double>(dim, dim, 0.0);
137 if (V.rows() != dim)
138 throw InputError(
139 "cache_rmf_lna: the centred basis does not span the state dimension; n, h and dim "
140 "disagree");
141
142 const std::size_t d = V.cols();
143 Matrix<double> Ar(d, d, 0.0), Qr(d, d, 0.0);
144 for (std::size_t a = 0; a < d; ++a)
145 for (std::size_t b = 0; b < d; ++b) {
146 double sa = 0.0, sq = 0.0;
147 for (std::size_t i = 0; i < dim; ++i)
148 for (std::size_t j = 0; j < dim; ++j) {
149 sa += V(i, a) * Fp(i, j) * V(j, b);
150 sq += V(i, a) * Q(i, j) * V(j, b);
151 }
152 Ar(a, b) = sa;
153 Qr(a, b) = sq;
154 }
155 for (std::size_t i = 0; i < d; ++i)
156 for (std::size_t j = 0; j < i; ++j) {
157 const double v = 0.5 * (Qr(i, j) + Qr(j, i));
158 Qr(i, j) = v;
159 Qr(j, i) = v;
160 }
161
162 // Exponential stability ON THE SUBSPACE. Off it the Jacobian is singular by
163 // conservation, so testing the full spectrum would refuse every model.
164 const std::vector<std::complex<double>> ev = eig_values(Ar);
165 double worst = -std::numeric_limits<double>::infinity();
166 for (std::size_t i = 0; i < ev.size(); ++i) worst = std::max(worst, ev[i].real());
167 if (worst >= -std::sqrt(2.220446049250313e-16))
168 throw InputError(
169 "cache_rmf_lna: the cache fluid fixed point is not exponentially stable on the "
170 "reachable subspace, so the occupancy process has no stationary covariance");
171
172 // A W + W A' + Q = 0 with A = Ar; `lyap_solve(A, B, C)` solves A X + X B + C = 0.
173 Matrix<double> ArT(d, d, 0.0);
174 for (std::size_t i = 0; i < d; ++i)
175 for (std::size_t j = 0; j < d; ++j) ArT(i, j) = Ar(j, i);
176 Matrix<double> Wr = lyap_solve(Ar, ArT, Qr);
177 for (std::size_t i = 0; i < d; ++i)
178 for (std::size_t j = 0; j < i; ++j) {
179 const double v = 0.5 * (Wr(i, j) + Wr(j, i));
180 Wr(i, j) = v;
181 Wr(j, i) = v;
182 }
183
184 Matrix<double> W(dim, dim, 0.0);
185 for (std::size_t i = 0; i < dim; ++i)
186 for (std::size_t j = 0; j < dim; ++j) {
187 double s = 0.0;
188 for (std::size_t a = 0; a < d; ++a)
189 for (std::size_t b = 0; b < d; ++b) s += V(i, a) * Wr(a, b) * V(j, b);
190 W(i, j) = s;
191 }
192 for (std::size_t i = 0; i < dim; ++i)
193 for (std::size_t j = 0; j < i; ++j) {
194 const double v = 0.5 * (W(i, j) + W(j, i));
195 W(i, j) = v;
196 W(j, i) = v;
197 }
198 return W;
199}
200
201} // namespace cache
202} // namespace line
203
204#endif // LINE_API_CACHE_CACHE_RMF_LNA_H
Refined mean field (RMF) miss rates of a multi-list RANDOM(m) cache.
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
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Dense matrix and non-owning view.
Matrix< double > cache_rmf_lna(const std::vector< double > &x, const std::vector< double > &p, const std::vector< double > &m, std::size_t n, std::size_t h, std::size_t dim)
Stationary covariance of a RANDOM(m) cache occupancy, under the LNA.
Matrix< T > lyap_solve(const Matrix< T > &A, const Matrix< T > &B, const Matrix< T > &C)
MATLAB lyap(A,B,C) solves A X + X B + C = 0, i.e.
Definition sylvester.h:123
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
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).