Source code for line_solver.api.cache.erec

"""
Exact Recursive (EREC) Cache Analysis Algorithms.

Native Python implementations of exact recursive methods for cache
analysis, providing precise solutions for small to medium-sized
cache systems.

References:
    Che et al., "Hierarchical Web Caching Systems: Modeling, Design and
    Experimental Results." IEEE JSAC, 2002.
"""

import numpy as np
from scipy import special
from typing import Union, Tuple


[docs] def cache_erec(gamma: np.ndarray, m: np.ndarray, sigma: np.ndarray = None, k: np.ndarray = None) -> float: """ Compute the cache normalizing constant using exact recursive method. With item sizes and per-list storage cost caps this evaluates the constrained normalizing constant E(m,k) of Casale-Gast (IEEE/ACM Trans. Networking 29(2), 2021), Sec. IX, from the recursion E(m,k) = E_i(m,k) + sum_j m_j gamma_ij E_i(m-1_j, k-sigma_i 1_j), with the extra boundary E(m,k)=0 whenever some residual cap is negative. Args: gamma: Cache access factors matrix (n x h), where n is number of items and h is number of cache levels. m: Cache capacity vector (1 x h or h,). sigma: Optional item storage costs (sizes), positive integers (n,). k: Optional per-list storage cost caps, non-negative integers (h,). Returns: Normalizing constant E. """ gamma = np.asarray(gamma, dtype=np.float64) m = np.asarray(m, dtype=np.float64).ravel() if sigma is None or k is None or len(np.asarray(sigma).ravel()) == 0 \ or len(np.asarray(k).ravel()) == 0: return cache_erec_aux(gamma, m, gamma.shape[0]) return _cache_erec_cost(gamma, m, np.asarray(sigma, dtype=np.float64).ravel(), np.asarray(k, dtype=np.float64).ravel())
def _cache_erec_cost(gamma: np.ndarray, m: np.ndarray, sigma: np.ndarray, k: np.ndarray) -> float: """Dynamic program over the (residual capacity, residual cost cap) lattice.""" n, h = gamma.shape if len(sigma) != n: raise ValueError('The item size vector must have one entry per item.') if len(k) != h: raise ValueError('The cost cap vector must have one entry per cache list.') if np.any(sigma <= 0) or np.any(sigma != np.round(sigma)): raise ValueError('Item sizes must be positive integers.') if np.any(k < 0) or np.any(k != np.round(k)): raise ValueError('Storage cost caps must be non-negative integers.') if np.min(m) < 0 or np.sum(m) > n: return 0.0 if np.sum(m) == 0: return 1.0 mv = m.astype(int) kv = k.astype(int) sv = sigma.astype(int) dims = np.concatenate([mv + 1, kv + 1]) lattice = int(np.prod(dims)) if lattice > 10000000: raise ValueError('The cost-constrained normalizing constant lattice has %d ' 'states, which exceeds the exact method limit; use the ' 'sampling method.' % lattice) # F(mm,kk) over items 1..t; at t=0 only the empty cache contributes F = np.zeros(dims, dtype=np.float64) empty = tuple([0] * h + [slice(None)] * h) F[empty] = 1.0 for t in range(n): Fprev = F F = Fprev.copy() for j in range(h): g = gamma[t, j] if g == 0.0: continue # shift by one unit of capacity in list j and sigma_t of its cap src = [slice(None)] * (2 * h) dst = [slice(None)] * (2 * h) src[j] = slice(0, dims[j] - 1) dst[j] = slice(1, dims[j]) if sv[t] > dims[h + j] - 1: continue src[h + j] = slice(0, dims[h + j] - sv[t]) dst[h + j] = slice(sv[t], dims[h + j]) mult = np.arange(1, dims[j]).reshape( [-1 if d == j else 1 for d in range(2 * h)]) F[tuple(dst)] += g * mult * Fprev[tuple(src)] return float(F[tuple(dims - 1)])
[docs] def cache_erec_aux(gamma: np.ndarray, m: np.ndarray, k: int) -> float: """ Auxiliary method for computing cache normalizing constant using exact recursive method. This method performs the core computation recursively, adjusting the size of the input matrix. Args: gamma: Cache access factors matrix (n x h). m: Cache capacity vector (h,). k: Current number of rows in the recursive step. Returns: Normalizing constant for the given configuration. """ gamma = np.asarray(gamma, dtype=np.float64) m = np.asarray(m, dtype=np.float64).ravel() h = len(m) # Number of cache levels # Base cases m_sum = np.sum(m) m_min = np.min(m) if m_sum == 0: return 1.0 if m_sum > k or m_min < 0: return 0.0 if k == 1 and m_sum == 1.0: # Find the index of the non-zero element in m j = np.argmax(m) return gamma[0, j] # Recursive case E = cache_erec_aux(gamma, m, k - 1) for j in range(h): if m[j] > 0: # Create m with one element reduced at position j m_oner = m.copy() m_oner[j] -= 1 term = cache_erec_aux(gamma, m_oner, k - 1) * gamma[k - 1, j] * m[j] E += term return E
[docs] def cache_prob_erec(gamma: np.ndarray, m: np.ndarray, sigma: np.ndarray = None, k: np.ndarray = None) -> np.ndarray: """ Compute cache state probabilities using exact recursive method. This method calculates the probabilities of the cache being in different states based on the cache access factors and capacity. With item sizes and per-list storage cost caps it evaluates pi_ij = m_j gamma_ij E_i(m-1_j, k-sigma_i 1_j) / E(m,k). Args: gamma: Cache access factors matrix (n x h), where n is number of items and h is number of cache levels. m: Cache capacity vector (1 x h or h,). sigma: Optional item storage costs (sizes) (n,). k: Optional per-list storage cost caps (h,). Returns: Matrix (n x h+1) containing cache state probabilities. Column 0 is miss probability, columns 1..h are hit probabilities at each cache level. """ gamma = np.asarray(gamma, dtype=np.float64) m = np.asarray(m, dtype=np.float64).ravel() capped = not (sigma is None or k is None or len(np.asarray(sigma).ravel()) == 0 or len(np.asarray(k).ravel()) == 0) if capped: sigma = np.asarray(sigma, dtype=np.float64).ravel() k = np.asarray(k, dtype=np.float64).ravel() n = gamma.shape[0] # Number of items h = gamma.shape[1] # Number of cache levels E = cache_erec(gamma, m, sigma if capped else None, k if capped else None) prob = np.zeros((n, h + 1)) for i in range(n): for j in range(h): # Create sub-gamma with row i removed sub_gamma = np.delete(gamma, i, axis=0) # Create m with one element reduced at position j m_oner = m.copy() m_oner[j] -= 1 if capped: kij = k.copy() kij[j] -= sigma[i] if kij[j] < 0: Ei = 0.0 else: Ei = cache_erec(sub_gamma, m_oner, np.delete(sigma, i), kij) else: Ei = cache_erec_aux(sub_gamma, m_oner, n - 1) if E != 0: value = m[j] * gamma[i, j] * Ei / E else: value = 0.0 prob[i, j + 1] = value # Miss probability is 1 - sum of hit probabilities row_sum = np.sum(prob[i, 1:h + 1]) prob[i, 0] = abs(1 - row_sum) return prob
[docs] def cache_mva(gamma: np.ndarray, m: np.ndarray ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]: """ Mean Value Analysis for cache systems. Computes cache performance metrics using MVA approach. Args: gamma: Request rate matrix (n x h). m: Cache size parameters (h,). Returns: Tuple of (pi, pi0, pij, x, u, E) containing: - pi: Steady-state probabilities - pi0: Miss probabilities per item - pij: Hit probabilities per item per level (n x h) - x: Throughput vector - u: Utilization vector - E: Normalizing constant """ gamma = np.asarray(gamma, dtype=np.float64) m = np.asarray(m, dtype=np.float64).ravel() n = gamma.shape[0] # Number of items h = gamma.shape[1] # Number of cache levels # Compute normalizing constant E = cache_erec(gamma, m) # Compute state probabilities prob = cache_prob_erec(gamma, m) pi0 = prob[:, 0] # Miss probabilities pij = prob[:, 1:] # Hit probabilities # Compute throughputs and utilizations total_rates = np.sum(gamma, axis=1) x = total_rates * (1 - pi0) # Throughput (hit rate) u = x / (np.sum(x) + 1e-10) if np.sum(x) > 0 else np.zeros(n) # pi is the full probability vector pi = prob return pi, pi0, pij, x, u, E
[docs] def cache_cost(gamma: np.ndarray, m: np.ndarray, sigma: np.ndarray, k: np.ndarray = None, pij: np.ndarray = None) -> np.ndarray: """ Mean storage cost held by each cache list. Evaluates K_j = sum_i sigma_i pi_ij, the expected storage cost of the items resident in list j at steady state, as defined in Casale-Gast (IEEE/ACM Trans. Networking 29(2), 2021), Sec. IX. Args: gamma: Cache access factors matrix (n x h). m: Cache capacity vector (h,). sigma: Item storage costs (sizes) (n,). k: Optional per-list storage cost caps (h,). pij: Optional precomputed occupancy matrix (n x h+1), column 0 the miss probability. Returns: Mean storage cost of each list (h,). """ gamma = np.asarray(gamma, dtype=np.float64) sigma = np.asarray(sigma, dtype=np.float64).ravel() n, h = gamma.shape if len(sigma) != n: raise ValueError('The item size vector must have one entry per item.') if pij is None: pij = cache_prob_erec(gamma, m, sigma, k) pij = np.asarray(pij, dtype=np.float64) return np.array([float(sigma @ pij[:, 1 + j]) for j in range(h)])
[docs] def cache_cost_pathcheck(gamma: np.ndarray, sigma: np.ndarray, k: np.ndarray, parent: np.ndarray) -> np.ndarray: """ Detect promotion paths blocked by storage cost caps. The constrained normalizing constant E(m,k) sums the product form over every size-feasible cache state, while under RR-C(m) an item only reaches list j by being promoted one list at a time along the path from the miss list. A cap on an intermediate list therefore makes size-feasible states unreachable and E(m,k) normalizes over states the cache never visits. An empty report is a necessary, not sufficient, condition for the two sets to agree. Args: gamma: Cache access factors matrix (n x h). sigma: Item storage costs (sizes) (n,). k: Per-list storage cost caps (h,). parent: Parent list of each list, 0-based with -1 for the lists rooted in the miss list. Returns: Array of blocked triples [item, list, blocking list], 0-based. """ gamma = np.asarray(gamma, dtype=np.float64) sigma = np.asarray(sigma, dtype=np.float64).ravel() k = np.asarray(k, dtype=np.float64).ravel() parent = np.asarray(parent, dtype=int).ravel() n, h = gamma.shape viol = [] for i in range(n): for j in range(h): if gamma[i, j] == 0.0 or sigma[i] > k[j]: continue # item i never resides in list j anyway l = parent[j] while l >= 0: if sigma[i] > k[l]: viol.append([i, j, l]) break l = parent[l] return np.array(viol, dtype=int).reshape(-1, 3)
__all__ = [ 'cache_erec', 'cache_erec_aux', 'cache_prob_erec', 'cache_cost', 'cache_cost_pathcheck', 'cache_mva', ]