{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "d0996120-bb17-4476-b912-ce155100b2cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from scipy.optimize import minimize\n",
    "import numpy as np\n",
    "\n",
    "# Define Parameters\n",
    "lambda_vals = np.array([0.03, 0.04, 0.05, 0.06, 0.07, 1, 1.1, 1.2, 1.3, 1.4, 1.5])  # Request rates ascendingly\n",
    "N = len(lambda_vals)\n",
    "B = 4.4  # Cache size\n",
    "c_delta = 1  # Age linear cost\n",
    "c_f = 7  # Fetching linear cost (caching miss cost)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "fcf0c13c-5b2c-457e-9aa6-8d349fcf13fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "def theoretical_opt(lambda_vals, B, c_f, c_delta):\n",
    "    \"\"\"\n",
    "    Perform theoretical optimization to compute optimal hit probabilities.\n",
    "    \n",
    "    Parameters:\n",
    "    - lambda_vals: Array of request rates.\n",
    "    - B: Cache size (constraint on total hit probabilities).\n",
    "    - c_f: Cost of fetching (cache miss cost).\n",
    "    - c_delta: Cost of caching (hit cost).\n",
    "\n",
    "    Returns:\n",
    "    - h_optt: Optimal hit probabilities.\n",
    "    \"\"\"\n",
    "    N = len(lambda_vals)\n",
    "    h_optt = np.zeros(N)  # Initialize optimal hit probabilities\n",
    "    differenc_set = np.arange(N)  # Set of variables to optimize\n",
    "    fix_i = []  # Set of fixed variables (those already optimized)\n",
    "    n = N\n",
    "    b = B\n",
    "    flag = True\n",
    "\n",
    "    while flag:\n",
    "        if n == 0:  # If no variables left to optimize\n",
    "            if b > 0:  # If there is leftover cache size, redistribute it\n",
    "                differenc_set = np.where(h_optt == 0)[0]  # Find zero hit probability variables\n",
    "                fix_i = np.setdiff1d(np.arange(N), differenc_set)\n",
    "                n = len(differenc_set)\n",
    "                continue\n",
    "            else:  # No variables to optimize, finalize\n",
    "                h_optt[differenc_set] = 0\n",
    "                break\n",
    "        \n",
    "        # Calculate the optimal Lagrange multiplier (mu) and hit probabilities for the set of variables\n",
    "        mu = max(0, (n * c_f - b * c_delta) / np.sum(1 / lambda_vals[differenc_set]))\n",
    "        h_optt[differenc_set] = (c_f - mu / lambda_vals[differenc_set]) / c_delta\n",
    "        \n",
    "        # If mu < 0, adjust the cache size to set mu to zero in the next iteration\n",
    "        if mu < 0:\n",
    "            b = (n * c_f / c_delta)\n",
    "            continue\n",
    "        \n",
    "        # Identify violations of the hit probability constraints (h > 1 or h < 0)\n",
    "        larger_i = np.where(h_optt > 1)[0]\n",
    "        smaller_i = np.where(h_optt < 0)[0]\n",
    "\n",
    "        # If no violations, the optimal solution is reached\n",
    "        if len(smaller_i) == 0 and len(larger_i) == 0:\n",
    "            break\n",
    "        \n",
    "        # Find the furthest object from the boundary (either 0 or 1)\n",
    "        min_viol = 0\n",
    "        min_viol_i = -1\n",
    "        if len(smaller_i) > 0:\n",
    "            min_viol, min_viol_i = np.min(h_optt[smaller_i]), np.argmin(h_optt[smaller_i])\n",
    "\n",
    "        max_viol = 0\n",
    "        max_viol_i = -1\n",
    "        if len(larger_i) > 0:\n",
    "            max_viol, max_viol_i = np.max(h_optt[larger_i] - 1), np.argmax(h_optt[larger_i] - 1)\n",
    "        \n",
    "        # Choose the variable with the largest violation to adjust\n",
    "        if max_viol > abs(min_viol):\n",
    "            viol_i = max_viol_i\n",
    "            min_viol_flag = 0\n",
    "        else:\n",
    "            viol_i = min_viol_i\n",
    "            min_viol_flag = 1\n",
    "        \n",
    "        # Set the furthest object to the nearest boundary (0 or 1)\n",
    "        if min_viol_flag:\n",
    "            h_optt[viol_i] = 0\n",
    "        else:\n",
    "            h_optt[viol_i] = min(1, b)\n",
    "        \n",
    "        # Update cache size and fix the selected variable\n",
    "        b -= h_optt[viol_i]\n",
    "        fix_i.append(viol_i)\n",
    "        differenc_set = np.setdiff1d(np.arange(N), fix_i)\n",
    "        n = N - len(fix_i)\n",
    "    \n",
    "    return h_optt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5d223324-d193-416a-b2c3-1e143e981a37",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "def numerical_opt(lambda_vals, B, c_f, c_delta):\n",
    "    \"\"\"\n",
    "    Perform numerical optimization to compute optimal hit probabilities.\n",
    "\n",
    "    Parameters:\n",
    "    - lambda_vals: Array of request rates.\n",
    "    - B: Cache size (constraint on total hit probabilities).\n",
    "    - c_f: Cost of fetching (cache miss cost).\n",
    "    - c_delta: Cost of caching (hit cost).\n",
    "\n",
    "    Returns:\n",
    "    - x_opt: Optimal hit probabilities.\n",
    "    \"\"\"\n",
    "    N = len(lambda_vals)  # Number of items\n",
    "\n",
    "    # Initial guess: Even distribution of cache capacity\n",
    "    x_init = np.full(N, B / N)\n",
    "\n",
    "    # Objective function\n",
    "    def objective(x):\n",
    "        return np.sum(lambda_vals * ((1 - x) * c_f + x**2 * c_delta / 2))\n",
    "\n",
    "    # Constraint: Sum of hit probabilities <= cache size (B)\n",
    "    def constraint_total_hit(x):\n",
    "        return B - np.sum(x)  # Non-negative means constraint satisfied\n",
    "\n",
    "    # Bounds for hit probabilities: 0 <= h_i <= 1\n",
    "    bounds = [(0, 1) for _ in range(N)]\n",
    "\n",
    "    # Optimization\n",
    "    constraints = [{'type': 'ineq', 'fun': constraint_total_hit}]  # Inequality constraint\n",
    "    result = minimize(\n",
    "        objective, \n",
    "        x_init, \n",
    "        method='SLSQP',  # Sequential Least Squares Quadratic Programming\n",
    "        bounds=bounds, \n",
    "        constraints=constraints, \n",
    "        options={'disp': True}  # Set to True for optimization output\n",
    "    )\n",
    "\n",
    "    # Optimal solution\n",
    "    if result.success:\n",
    "        return result.x  # Optimal hit probabilities\n",
    "    else:\n",
    "        raise ValueError(\"Optimization failed: \" + result.message)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "073be740-dc97-454b-87e7-2f8f93c8f137",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Optimization terminated successfully    (Exit mode 0)\n",
      "            Current function value: 16.15721739129548\n",
      "            Iterations: 4\n",
      "            Function evaluations: 48\n",
      "            Gradient evaluations: 4\n",
      "Hit probabilities (numerical and theoretical):\n",
      " [[0.00000000e+00 0.00000000e+00]\n",
      " [0.00000000e+00 0.00000000e+00]\n",
      " [0.00000000e+00 0.00000000e+00]\n",
      " [0.00000000e+00 0.00000000e+00]\n",
      " [0.00000000e+00 0.00000000e+00]\n",
      " [4.58923826e-13 1.00000000e+00]\n",
      " [4.26087031e-01 0.00000000e+00]\n",
      " [9.73912969e-01 4.00000000e-01]\n",
      " [1.00000000e+00 0.00000000e+00]\n",
      " [1.00000000e+00 0.00000000e+00]\n",
      " [1.00000000e+00 0.00000000e+00]]\n",
      "Objective function values (numerical, theoretical): [16.15721739129548, 44.486]\n",
      "Constraint violations (numerical, theoretical): [1.241673430740775e-12, -3.0]\n"
     ]
    }
   ],
   "source": [
    "\n",
    "# Optimization\n",
    "h_numerical = numerical_opt(lambda_vals, B, c_f, c_delta)\n",
    "h_theoretical = theoretical_opt(lambda_vals, B, c_f, c_delta)\n",
    "\n",
    "# Comparison of Hit Probabilities\n",
    "hit_opt = np.vstack((h_numerical, h_theoretical)).T  # Combine numerical and theoretical hit probabilities\n",
    "\n",
    "# Objective Function Calculation\n",
    "obj_1 = np.sum(lambda_vals * ((1 - h_numerical) * c_f + h_numerical**2 * c_delta / 2))\n",
    "obj_2 = np.sum(lambda_vals * ((1 - h_theoretical) * c_f + h_theoretical**2 * c_delta / 2))\n",
    "obj = [obj_1, obj_2]  # Store objective function values for both methods\n",
    "\n",
    "# Constraints\n",
    "const_1 = np.sum(h_numerical) - B\n",
    "const_2 = np.sum(h_theoretical) - B\n",
    "constraint = [const_1, const_2]  # Check if the cache size constraint is satisfied\n",
    "\n",
    "# Outputs\n",
    "print(\"Hit probabilities (numerical and theoretical):\\n\", hit_opt)\n",
    "print(\"Objective function values (numerical, theoretical):\", obj)\n",
    "print(\"Constraint violations (numerical, theoretical):\", constraint)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "graphs",
   "language": "python",
   "name": "graphs"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
