{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f486bc83",
   "metadata": {},
   "source": [
    "# Simulación de una cola — Extensiones\n",
    "### Cuaderno de trabajo: aleatoriedad, réplicas con intervalo de confianza, y un segundo servidor\n",
    "\n",
    "Vas a extender la simulación de la cola de un solo servidor con las tres cosas que quedaron pendientes:\n",
    "\n",
    "1. **Aleatoriedad**: en vez de valores fijos, los tiempos entre llegadas y de servicio se generan con el LCG que ya construiste, transformado a Exponencial y Uniforme.\n",
    "2. **Réplicas + intervalo de confianza**: correr la simulación `R` veces con semillas distintas, y calcular un IC 95% para el retraso promedio (misma fórmula del modelo estático de Montecarlo).\n",
    "3. **Segundo servidor**: generalizar el simulador para que reciba `c` (número de servidores), y comparar `c=1` contra `c=2`.\n",
    "\n",
    "Busca las celdas marcadas con `# TODO`.\n",
    "\n",
    "**Contenido:**\n",
    "1. Generador de aleatorios (repaso)\n",
    "2. Simulador general de cola con `c` servidores — *(aquí completas tú)*\n",
    "3. Prueba de una réplica\n",
    "4. Réplicas múltiples y el intervalo de confianza — *(aquí completas tú)*\n",
    "5. Comparación c=1 vs. c=2\n",
    "6. Visualizaciones: histogramas, barras de error, animación comparativa\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2a66155",
   "metadata": {},
   "source": [
    "## 1. Generador de aleatorios (repaso)\n",
    "\n",
    "El mismo LCG de la clase pasada: $X_{n+1} = (a X_n + c) \\mod m$, con $U_n = X_n/m$. A partir de `generar_aleatorio()` construimos, por transformada inversa, un generador Exponencial (para tiempos entre llegadas y de servicio) y uno Uniforme (por si se quiere usar en vez del exponencial)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c7c26b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "import heapq\n",
    "import statistics\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# --- LCG (Park & Miller) ---\n",
    "a_LCG = 16807\n",
    "c_LCG = 0\n",
    "m_LCG = 2147483647\n",
    "X_semilla = 12345   # se reemplaza antes de cada réplica\n",
    "\n",
    "def generar_aleatorio():\n",
    "    global X_semilla\n",
    "    #TODO 0: implementar el LCG \n",
    "    X_semilla = ...\n",
    "    return X_semilla / m_LCG\n",
    "\n",
    "def generar_exponencial(tasa):\n",
    "    # TODO 1: transformada inversa de la Exponencial(tasa)\n",
    "    # Pista: X = -(1/tasa) * ln(1 - U)\n",
    "    U = generar_aleatorio()\n",
    "    return ...\n",
    "\n",
    "def generar_uniforme(a, b):\n",
    "    # TODO 2: transformada inversa de la Uniforme(a, b)\n",
    "    U = generar_aleatorio()\n",
    "    return ...\n",
    "\n",
    "print(generar_exponencial(1.0), generar_uniforme(0, 5))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c37df19",
   "metadata": {},
   "source": [
    "## 2. Simulador general de cola con `c` servidores\n",
    "\n",
    "Vamos a generalizar el bucle de eventos del notebook anterior para que funcione con cualquier número de servidores `c`:\n",
    "\n",
    "- En vez de un solo `servidor_ocupado` (0/1), ahora hay una lista `servidores_libres` con los índices de los servidores disponibles.\n",
    "- **Llegada**: si hay algún servidor libre, el cliente toma uno cualquiera y empieza de inmediato (`D=0`); si no, se une a la cola.\n",
    "- **Salida**: el evento indica qué servidor se desocupó. Si hay cola, el siguiente cliente pasa a ocupar ese mismo servidor; si no, ese servidor vuelve a estar libre.\n",
    "\n",
    "Completa los `# TODO` en la celda de abajo — son la misma lógica del notebook anterior, adaptada a que ahora puede haber más de un servidor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95f83c60",
   "metadata": {},
   "outputs": [],
   "source": [
    "def simular_cola(c, n_objetivo, gen_entre_llegadas, gen_servicio):\n",
    "    reloj = 0.0\n",
    "    servidores_libres = list(range(c))\n",
    "    ocupados_count = 0\n",
    "    cola = []\n",
    "\n",
    "    FEL = []\n",
    "    heapq.heappush(FEL, (gen_entre_llegadas(), 'llegada', None))\n",
    "\n",
    "    llegada_de = {}\n",
    "    siguiente_id = 1\n",
    "    retrasos = []\n",
    "    area_cola = 0.0\n",
    "    area_ocupados = 0.0\n",
    "    t_prev = 0.0\n",
    "    trace_rows = []\n",
    "    historial = [(0.0, 0, 0)]\n",
    "\n",
    "    while len(retrasos) < n_objetivo:\n",
    "        t_evt, tipo, info = heapq.heappop(FEL)\n",
    "        dt = t_evt - t_prev\n",
    "        area_cola += len(cola) * dt\n",
    "        area_ocupados += ocupados_count * dt\n",
    "        t_prev = t_evt\n",
    "        reloj = t_evt\n",
    "\n",
    "        if tipo == 'llegada':\n",
    "            cid = siguiente_id\n",
    "            siguiente_id += 1\n",
    "            llegada_de[cid] = reloj\n",
    "            heapq.heappush(FEL, (reloj + gen_entre_llegadas(), 'llegada', None))\n",
    "\n",
    "            if servidores_libres:\n",
    "                # TODO 3: hay al menos un servidor libre -> el cliente entra de inmediato\n",
    "                # Pista: saca un servidor de 'servidores_libres' con .pop(), súmalo a\n",
    "                # 'ocupados_count', el retraso D es 0.0, y programa su salida con\n",
    "                # heapq.heappush(FEL, (reloj + gen_servicio(), 'salida', s))\n",
    "                ...\n",
    "                trace_rows.append((round(reloj, 2), f'Llegada C{cid}', ocupados_count, len(cola), D))\n",
    "            else:\n",
    "                cola.append(cid)\n",
    "                trace_rows.append((round(reloj, 2), f'Llegada C{cid}', ocupados_count, len(cola), None))\n",
    "        else:\n",
    "            s = info\n",
    "            if cola:\n",
    "                # TODO 4: se desocupó el servidor 's' y hay clientes esperando\n",
    "                # Pista: saca al primero de 'cola', calcula su retraso D (reloj - su llegada),\n",
    "                # y programa su salida usando el MISMO servidor 's' que se acaba de desocupar\n",
    "                ...\n",
    "                trace_rows.append((round(reloj, 2), f'Salida (servidor {s})', ocupados_count, len(cola), round(D, 2)))\n",
    "            else:\n",
    "                servidores_libres.append(s)\n",
    "                ocupados_count -= 1\n",
    "                trace_rows.append((round(reloj, 2), f'Salida (servidor {s})', ocupados_count, len(cola), None))\n",
    "\n",
    "        historial.append((reloj, len(cola), ocupados_count))\n",
    "\n",
    "    D_hat = sum(retrasos) / len(retrasos)\n",
    "    q_hat = area_cola / reloj\n",
    "    u_hat = area_ocupados / (c * reloj)\n",
    "    return D_hat, q_hat, u_hat, reloj, retrasos, trace_rows, historial\n",
    "\n",
    "print(\"Simulador definido.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71cf47f5",
   "metadata": {},
   "source": [
    "## 3. Prueba de una réplica\n",
    "\n",
    "Antes de correr réplicas, probamos el simulador una sola vez con `c=1`, tasas `λ=1.0` (llegadas) y `μ=1.5` (servicio), para ver que produce una traza razonable — igual que el notebook anterior, pero ahora con tiempos aleatorios en vez de fijos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3446ccac",
   "metadata": {},
   "outputs": [],
   "source": [
    "LAM, MU = 1.0, 1.5   # tasas: 1 llegada/min en promedio, servicio de 1.5 clientes/min por servidor\n",
    "\n",
    "X_semilla = 111\n",
    "D_hat, q_hat, u_hat, T, retrasos, trace, hist = simular_cola(\n",
    "    1, 15,\n",
    "    lambda: generar_exponencial(LAM),\n",
    "    lambda: generar_exponencial(MU),\n",
    ")\n",
    "\n",
    "import pandas as pd\n",
    "print(f\"D_hat={D_hat:.2f}  q_hat={q_hat:.2f}  u_hat={u_hat:.2f}  T={T:.2f}\")\n",
    "pd.DataFrame(trace, columns=[\"Reloj\", \"Evento\", \"Servidores ocupados\", \"Cola\", \"Retraso\"])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b13f4f45",
   "metadata": {},
   "source": [
    "### Usando Uniforme en vez de Exponencial\n",
    "\n",
    "El simulador no sabe ni le importa qué distribución usás — solo necesita una función sin argumentos que devuelva un tiempo. Para mostrar que el servicio podría ser, por ejemplo, `Uniforme(0.3, 2.0)` en vez de `Exponencial(μ)`, basta con cambiar qué función le pasamos como `gen_servicio`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6e2afce",
   "metadata": {},
   "outputs": [],
   "source": [
    "X_semilla = 222\n",
    "# TODO 5: corre 'simular_cola' con c=1, n_objetivo=15, el mismo gen_entre_llegadas de siempre,\n",
    "# pero con gen_servicio = una Uniforme(0.3, 2.0) en vez de la Exponencial\n",
    "D_hat_u, q_hat_u, u_hat_u, T_u, *_ = simular_cola(\n",
    "    1, 15,\n",
    "    ...,\n",
    "    ...,\n",
    ")\n",
    "print(f\"Con servicio Uniforme(0.3, 2.0):  D_hat={D_hat_u:.2f}  q_hat={q_hat_u:.2f}  u_hat={u_hat_u:.2f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7720b51b",
   "metadata": {},
   "source": [
    "## 4. Réplicas múltiples y el intervalo de confianza\n",
    "\n",
    "Corremos la simulación `R` veces, reiniciando el LCG con una semilla distinta antes de cada una (para que las réplicas sean independientes). Completa la fórmula del intervalo de confianza al 95% — es la misma que usaste en el modelo estático de Montecarlo:\n",
    "\n",
    "$$\\text{IC}_{95\\%} = \\bar{D} \\pm 1.96 \\frac{s}{\\sqrt{R}}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "298dabb4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def correr_replicas(c, R, n_objetivo, semilla_base=1000):\n",
    "    global X_semilla\n",
    "    valores = []\n",
    "    for r in range(R):\n",
    "        X_semilla = semilla_base + 97 * r     # semilla distinta e independiente por réplica\n",
    "        D_hat, *_ = simular_cola(\n",
    "            c, n_objetivo,\n",
    "            lambda: generar_exponencial(LAM),\n",
    "            lambda: generar_exponencial(MU),\n",
    "        )\n",
    "        valores.append(D_hat)\n",
    "\n",
    "    media = statistics.mean(valores)\n",
    "    s = statistics.stdev(valores)\n",
    "    # TODO 6: calcula el margen del IC 95% (1.96 * s / raiz(R)) usando math.sqrt\n",
    "    ic = ...\n",
    "    return media, s, ic, valores\n",
    "\n",
    "R, n_obj = 30, 30\n",
    "media1, s1, ic1, vals1 = correr_replicas(1, R, n_obj)\n",
    "print(f\"c=1  D_hat promedio = {media1:.3f}   IC95% = [{media1-ic1:.3f}, {media1+ic1:.3f}]\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b4cf7a8",
   "metadata": {},
   "source": [
    "## 5. Comparación c=1 vs. c=2\n",
    "\n",
    "Corremos el mismo experimento de réplicas, pero ahora con dos servidores, y comparamos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d86037d",
   "metadata": {},
   "outputs": [],
   "source": [
    "media2, s2, ic2, vals2 = correr_replicas(2, R, n_obj)\n",
    "print(f\"c=2  D_hat promedio = {media2:.3f}   IC95% = [{media2-ic2:.3f}, {media2+ic2:.3f}]\")\n",
    "print()\n",
    "print(f\"Reducción del retraso promedio al agregar un servidor: {(1 - media2/media1)*100:.1f}%\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c975971",
   "metadata": {},
   "source": [
    "## 6. Visualizaciones\n",
    "\n",
    "### Histograma de D̂ por réplica\n",
    "\n",
    "Cada barra es una de las `R` réplicas. Se grafican por separado porque están en escalas muy distintas: con un solo servidor el sistema está más cargado (`ρ = λ/μ` más cerca de 1) y el retraso varía mucho más de una réplica a otra; con dos servidores el retraso es bajo y mucho más estable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e4fbf16",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(1, 2, figsize=(10, 3.5))\n",
    "\n",
    "axes[0].hist(vals1, bins=10, color='#E24B4A', alpha=0.8)\n",
    "axes[0].axvline(media1, color='black', linestyle='--', label=f'media = {media1:.2f}')\n",
    "axes[0].set_title(\"c = 1 servidor\")\n",
    "axes[0].set_xlabel(\"D_hat de cada réplica\")\n",
    "axes[0].set_ylabel(\"Número de réplicas\")\n",
    "axes[0].legend()\n",
    "\n",
    "axes[1].hist(vals2, bins=10, color='#639922', alpha=0.8)\n",
    "axes[1].axvline(media2, color='black', linestyle='--', label=f'media = {media2:.2f}')\n",
    "axes[1].set_title(\"c = 2 servidores\")\n",
    "axes[1].set_xlabel(\"D_hat de cada réplica\")\n",
    "axes[1].legend()\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39ddaa82",
   "metadata": {},
   "source": [
    "### Comparación con barras de error\n",
    "\n",
    "Un solo gráfico que resume todo: el punto es la media de las `R` réplicas, y las barras son el intervalo de confianza al 95%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "021fd50a",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(5, 4))\n",
    "ax.errorbar([1, 2], [media1, media2], yerr=[ic1, ic2], fmt='o', markersize=10,\n",
    "            color='#2B6CB0', capsize=8, linewidth=2)\n",
    "ax.set_xticks([1, 2])\n",
    "ax.set_xticklabels(['c = 1', 'c = 2'])\n",
    "ax.set_xlim(0.5, 2.5)\n",
    "ax.set_ylabel(\"D_hat promedio (IC 95%)\")\n",
    "ax.set_title(\"Retraso promedio según el número de servidores\")\n",
    "ax.grid(alpha=0.3, axis='y')\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ce368aa",
   "metadata": {},
   "source": [
    "### Animación comparativa: Q(t) con uno y con dos servidores\n",
    "\n",
    "Tomamos una réplica representativa de cada configuración y animamos la longitud de la cola `Q(t)` de ambas, una encima de la otra y sobre el **mismo eje de tiempo**, para ver en vivo cómo el segundo servidor evita que la cola crezca."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "08e8ac5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import bisect\n",
    "import matplotlib.animation as animation\n",
    "from IPython.display import HTML\n",
    "\n",
    "X_semilla = 555\n",
    "_, _, _, T1, _, _, hist1 = simular_cola(1, 25, lambda: generar_exponencial(LAM), lambda: generar_exponencial(MU))\n",
    "X_semilla = 555\n",
    "_, _, _, T2, _, _, hist2 = simular_cola(2, 25, lambda: generar_exponencial(LAM), lambda: generar_exponencial(MU))\n",
    "\n",
    "t1 = [h[0] for h in hist1]; q1 = [h[1] for h in hist1]\n",
    "t2 = [h[0] for h in hist2]; q2 = [h[1] for h in hist2]\n",
    "\n",
    "def valor_en(tiempos, valores, t):\n",
    "    i = bisect.bisect_right(tiempos, t) - 1\n",
    "    return valores[max(i, 0)]\n",
    "\n",
    "T_max = min(T1, T2) * 0.95\n",
    "frames_t = [T_max * k / 59 for k in range(60)]\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 3.5))\n",
    "ax.set_xlim(0, T_max)\n",
    "ax.set_ylim(-0.3, max(max(q1), max(q2)) + 1)\n",
    "ax.set_xlabel(\"Tiempo\")\n",
    "ax.set_ylabel(\"Q(t)\")\n",
    "ax.set_title(\"Longitud de la cola: 1 servidor vs. 2 servidores\")\n",
    "ax.grid(alpha=0.3)\n",
    "\n",
    "linea1, = ax.plot([], [], color='#E24B4A', linewidth=2, label='c = 1')\n",
    "linea2, = ax.plot([], [], color='#639922', linewidth=2, label='c = 2')\n",
    "ax.legend(loc='upper left')\n",
    "\n",
    "def actualizar(frame):\n",
    "    t_actual = frames_t[frame]\n",
    "    xs = [x for x in t1 if x <= t_actual] + [t_actual]\n",
    "    ys = [valor_en(t1, q1, x) for x in xs]\n",
    "    linea1.set_data(xs, ys)\n",
    "    xs2 = [x for x in t2 if x <= t_actual] + [t_actual]\n",
    "    ys2 = [valor_en(t2, q2, x) for x in xs2]\n",
    "    linea2.set_data(xs2, ys2)\n",
    "    return linea1, linea2\n",
    "\n",
    "anim = animation.FuncAnimation(fig, actualizar, frames=len(frames_t), interval=120, blit=True, repeat=False)\n",
    "plt.close(fig)\n",
    "HTML(anim.to_jshtml())\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
