{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "638ddf5a",
   "metadata": {},
   "source": [
    "# Simulación de una cola con un solo servidor\n",
    "### Cuaderno de trabajo\n",
    "\n",
    "Vas a construir, en Python, el mismo ejemplo manual de una cola de un solo servidor que vimos en clase (una caja, una barbería), usando la vista de mundo de **Programación de Eventos**: el reloj de la simulación salta de un evento al siguiente usando una Lista de Eventos Futuros (FEL).\n",
    "\n",
    "La simulación debe detenerse cuando el **sexto cliente inicia su servicio** (n = 6 retrasos completados).\n",
    "\n",
    "Vas a encontrar celdas marcadas con `# TODO` — ahí es donde debes completar el código. El resto del código (estructuras auxiliares, gráficas) ya está armado para que te concentres en la lógica de la simulación.\n",
    "\n",
    "Al final podrás verificar tus resultados contra los valores conocidos del ejemplo:\n",
    "\n",
    "- $\\hat{D}$(6) debe dar **0.95** min\n",
    "- $\\hat{q}$(6) debe dar **1.15** clientes\n",
    "- $\\hat{u}$(6) debe dar **0.90**\n",
    "- T(6) debe dar **8.6**\n",
    "\n",
    "**Contenido:**\n",
    "1. Datos de entrada\n",
    "2. Estructuras de estado y la FEL\n",
    "3. El bucle principal de la simulación — *(aquí completas tú)*\n",
    "4. Tabla de traza\n",
    "5. Métricas de desempeño — *(aquí completas tú)*\n",
    "6. Visualizaciones: Q(t), B(t), diagrama de Gantt — *(un cálculo por completar)*\n",
    "7. Animación dinámica del proceso completo\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4cf4bb6d",
   "metadata": {},
   "source": [
    "## 1. Datos de entrada\n",
    "\n",
    "Son exactamente los del ejemplo manual: los instantes de llegada $t_i$ de cada cliente, y la duración de servicio $S_i$ que le corresponde a cada uno (solo necesitamos hasta el cliente 6, porque ahí se detiene la simulación)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6fdb0519",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Datos de entrada: exactamente los del ejemplo manual\n",
    "llegadas = [0.4, 1.6, 2.1, 3.8, 4.0, 5.6, 5.8, 7.2]           # t_i: instante de llegada de cada cliente\n",
    "servicios = {1: 2.0, 2: 0.7, 3: 0.2, 4: 1.1, 5: 3.7, 6: 0.6}   # S_i: duración de servicio\n",
    "\n",
    "N_OBJETIVO = 6   # la simulación termina cuando el 6º cliente INICIA su servicio\n",
    "\n",
    "print(f\"Clientes que llegarán: {len(llegadas)}\")\n",
    "print(f\"Objetivo: detener la simulación al completar {N_OBJETIVO} retrasos\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65db1d38",
   "metadata": {},
   "source": [
    "## 2. Estructuras de estado y la FEL\n",
    "\n",
    "Necesitamos tres cosas para llevar la simulación:\n",
    "\n",
    "- **Variables de estado**: `reloj`, si el `servidor_ocupado` (0/1), y la `cola` (lista FIFO de clientes esperando).\n",
    "- **La Lista de Eventos Futuros (FEL)**: la implementamos con un `heap` (cola de prioridad) de la librería `heapq`, así siempre sacamos primero el evento con el tiempo más pequeño — eso es lo que hace que el reloj \"salte\" de evento en evento en vez de avanzar en pasos fijos.\n",
    "- Cada evento es una tupla `(tiempo, desempate, tipo, cliente)`. El campo de desempate (0 para llegada, 1 para salida) es solo una convención para decidir el orden si dos eventos cayeran exactamente en el mismo instante."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2cb828c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import heapq\n",
    "\n",
    "# --- Variables de estado del sistema (se actualizan evento a evento) ---\n",
    "reloj = 0.0\n",
    "servidor_ocupado = 0     # 0 = libre, 1 = ocupado\n",
    "cola = []                 # lista FIFO con los clientes que esperan (guardamos su id)\n",
    "\n",
    "# --- Lista de Eventos Futuros (FEL), implementada como heap ---\n",
    "FEL = []\n",
    "for i, t in enumerate(llegadas, start=1):\n",
    "    heapq.heappush(FEL, (t, 0, 'llegada', i))\n",
    "\n",
    "print(\"Primeros eventos en la FEL:\", sorted(FEL)[:3])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f575f23",
   "metadata": {},
   "source": [
    "## 3. El bucle principal de la simulación\n",
    "\n",
    "En cada vuelta del bucle:\n",
    "\n",
    "1. Sacamos de la FEL el evento con el tiempo más próximo.\n",
    "2. **Antes** de tocar el estado, hay que acumular el área bajo `Q(t)` y `B(t)` usando el estado que estuvo vigente *desde el evento anterior hasta ahora*.\n",
    "3. Procesamos el evento:\n",
    "   - **Llegada**, servidor libre → el cliente entra de inmediato, retraso `D = 0` *(ya está resuelto, para que veas el patrón)*.\n",
    "   - **Llegada**, servidor ocupado → *(complétalo)*.\n",
    "   - **Salida**, cola no vacía → *(complétalo)*: ¿cómo se calcula el retraso del cliente que empieza a ser atendido?\n",
    "   - **Salida**, cola vacía → *(complétalo)*.\n",
    "\n",
    "Busca los `# TODO` en la celda de abajo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f12ed0b",
   "metadata": {},
   "outputs": [],
   "source": [
    "llegada_de = {}       # cliente -> instante en que llegó (para calcular su retraso D_i)\n",
    "retrasos = []          # D_i de cada cliente, en el orden en que INICIAN servicio\n",
    "area_cola = 0.0         # integral de Q(t) dt  -> para el número promedio en cola\n",
    "area_servidor = 0.0     # integral de B(t) dt  -> para la utilización\n",
    "\n",
    "t_prev = 0.0\n",
    "trace_rows = []                    # para reconstruir la tabla del enunciado\n",
    "historial_estado = [(0.0, 0, 0)]   # (tiempo, Q(t), B(t)) para las gráficas\n",
    "\n",
    "while len(retrasos) < N_OBJETIVO:\n",
    "    t_evt, _, tipo, cliente = heapq.heappop(FEL)\n",
    "\n",
    "    dt = t_evt - t_prev\n",
    "    # TODO 1: acumula el área bajo Q(t) y B(t) usando el estado vigente ANTES de este evento\n",
    "    # Pista: son 'len(cola)' y 'servidor_ocupado', multiplicados por dt\n",
    "    area_cola += ... #TODO\n",
    "    area_servidor += ... #TODO\n",
    "    t_prev = t_evt\n",
    "    reloj = t_evt\n",
    "\n",
    "    if tipo == 'llegada':\n",
    "        llegada_de[cliente] = reloj\n",
    "        if servidor_ocupado == 0:\n",
    "            # El servidor estaba libre: el cliente entra a servicio de inmediato (ya resuelto)\n",
    "            servidor_ocupado = 1\n",
    "            D = 0.0\n",
    "            retrasos.append(D)\n",
    "            k = len(retrasos)\n",
    "            heapq.heappush(FEL, (reloj + servicios[k], 1, 'salida', cliente))\n",
    "            trace_rows.append((reloj, f'Llegada C{cliente}', servidor_ocupado, len(cola), D))\n",
    "        else:\n",
    "            # TODO 2: el servidor está ocupado -> ¿qué debe hacer este cliente?\n",
    "            # Pista: agrégalo a la lista 'cola'\n",
    "            ...\n",
    "            trace_rows.append((reloj, f'Llegada C{cliente}', servidor_ocupado, len(cola), None))\n",
    "\n",
    "    else:  # tipo == 'salida'\n",
    "        if cola:\n",
    "            siguiente = cola.pop(0)\n",
    "            # TODO 3: calcula el retraso D del cliente que va a empezar a ser atendido ahora\n",
    "            # Pista: es el instante actual (reloj) menos el instante en que ESE cliente llegó\n",
    "            D = ...\n",
    "            retrasos.append(D)\n",
    "            k = len(retrasos)\n",
    "            heapq.heappush(FEL, (reloj + servicios[k], 1, 'salida', siguiente))\n",
    "            trace_rows.append((reloj, f'Salida C{cliente}', servidor_ocupado, len(cola), round(D, 2)))\n",
    "        else:\n",
    "            # TODO 4: no hay nadie esperando -> ¿qué le pasa al servidor?\n",
    "            # Pista: 'servidor_ocupado' debe volver a 0\n",
    "            ...\n",
    "            trace_rows.append((reloj, f'Salida C{cliente}', servidor_ocupado, len(cola), None))\n",
    "\n",
    "    historial_estado.append((reloj, len(cola), servidor_ocupado))\n",
    "\n",
    "print(f\"Simulación terminada en T({N_OBJETIVO}) = {reloj:.2f}  (debería dar 8.6)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8905a0dd",
   "metadata": {},
   "source": [
    "## 4. Tabla de traza\n",
    "\n",
    "Reconstruimos la tabla evento por evento, igual a la del enunciado, para verificar que la lógica está bien implementada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "719e142c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "tabla = pd.DataFrame(\n",
    "    trace_rows,\n",
    "    columns=[\"Reloj\", \"Evento\", \"Servidor (1=ocupado)\", \"Clientes en cola\", \"Retraso D_i\"]\n",
    ")\n",
    "tabla\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13794133",
   "metadata": {},
   "source": [
    "## 5. Métricas de desempeño\n",
    "\n",
    "Con lo que ya calculaste (`retrasos`, `area_cola`, `area_servidor`, `reloj`), completa las tres fórmulas:\n",
    "\n",
    "$$\\hat{d}(6) = \\frac{\\sum_{i=1}^{6} D_i}{6} \\qquad \\hat{q}(6) = \\frac{\\text{área bajo } Q(t)}{T(6)} \\qquad \\hat{u}(6) = \\frac{\\text{área bajo } B(t)}{T(6)}$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0489b6ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "D_hat = ...   # TODO: promedio de los retrasos (usa sum() y len() sobre 'retrasos')\n",
    "q_hat = ...   # TODO: 'area_cola' dividida entre el tiempo total ('reloj')\n",
    "u_hat = ...   # TODO: 'area_servidor' dividida entre el tiempo total ('reloj')\n",
    "\n",
    "print(f\"D-hat({N_OBJETIVO}) = {D_hat}   (deberia dar approx 0.95)\")\n",
    "print(f\"q-hat({N_OBJETIVO}) = {q_hat}   (deberia dar approx 1.15)\")\n",
    "print(f\"u-hat({N_OBJETIVO}) = {u_hat}   (deberia dar approx 0.90)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fb62abf",
   "metadata": {},
   "source": [
    "## 6. Visualizaciones\n",
    "\n",
    "### Q(t): longitud de la cola en el tiempo\n",
    "\n",
    "Se construye como un **gráfico de escalón** (`plt.step`, con `where='post'`): cada punto de `historial_estado` guarda `(tiempo, Q, B)` justo después de procesar un evento, y `where='post'` hace que el valor se mantenga constante desde ese instante hasta el próximo evento — que es exactamente cómo se comporta `Q(t)` en la realidad (cambia de golpe en cada evento, no gradualmente)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a821003",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "tiempos = [h[0] for h in historial_estado]\n",
    "Qs = [h[1] for h in historial_estado]\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 3))\n",
    "ax.step(tiempos, Qs, where='post', color='#854F0B', linewidth=2)\n",
    "ax.set_xlabel(\"Tiempo\")\n",
    "ax.set_ylabel(\"Clientes en cola  Q(t)\")\n",
    "ax.set_title(\"Longitud de la cola a lo largo del tiempo\")\n",
    "ax.set_ylim(-0.3, max(Qs) + 1)\n",
    "ax.grid(alpha=0.3)\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05adac2e",
   "metadata": {},
   "source": [
    "### B(t): estado del servidor en el tiempo\n",
    "\n",
    "Misma idea, pero con el estado binario del servidor (0 = libre, 1 = ocupado). Lo ponemos justo debajo de `Q(t)`, compartiendo el eje del tiempo, para ver cómo se relacionan: cada vez que `B(t)` baja a 0 sin que haya nadie en cola, es tiempo del servidor sin usar; cada vez que `Q(t) > 0` y `B(t) = 1`, hay clientes esperando mientras el servidor sigue ocupado con otro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f324674d",
   "metadata": {},
   "outputs": [],
   "source": [
    "Bs = [h[2] for h in historial_estado]\n",
    "\n",
    "fig, axes = plt.subplots(2, 1, figsize=(9, 5), sharex=True)\n",
    "\n",
    "axes[0].step(tiempos, Qs, where='post', color='#854F0B', linewidth=2)\n",
    "axes[0].set_ylabel(\"Q(t)\")\n",
    "axes[0].set_title(\"Cola y servidor a lo largo del tiempo\")\n",
    "axes[0].grid(alpha=0.3)\n",
    "\n",
    "axes[1].step(tiempos, Bs, where='post', color='#2B6CB0', linewidth=2)\n",
    "axes[1].set_ylabel(\"B(t)\")\n",
    "axes[1].set_yticks([0, 1])\n",
    "axes[1].set_yticklabels([\"libre\", \"ocupado\"])\n",
    "axes[1].set_xlabel(\"Tiempo\")\n",
    "axes[1].grid(alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48e80845",
   "metadata": {},
   "source": [
    "### Diagrama de Gantt por cliente\n",
    "\n",
    "Para cada cliente necesitamos tres instantes: cuándo llegó, cuándo empezó a ser atendido, y cuándo terminó. Ya tienes la lista `retrasos` (los `D_i` que calculaste arriba) y `llegadas`. Completa cómo se calcula el instante en que cada cliente **empieza** su servicio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0460508b",
   "metadata": {},
   "outputs": [],
   "source": [
    "clientes = list(range(1, N_OBJETIVO + 1))\n",
    "llegada_i = [llegadas[i-1] for i in clientes]\n",
    "# TODO 5: ¿cómo se calcula el instante en que el cliente i EMPIEZA su servicio,\n",
    "# a partir de 'llegada_i' y su retraso (la lista 'retrasos', ya calculada arriba)?\n",
    "inicio_i = [...]\n",
    "fin_i = [inicio_i[i-1] + servicios[i] for i in clientes]\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 4))\n",
    "for i in clientes:\n",
    "    ax.barh(i, inicio_i[i-1] - llegada_i[i-1], left=llegada_i[i-1], color='#E24B4A',\n",
    "            label='Espera' if i == 1 else \"\")\n",
    "    ax.barh(i, fin_i[i-1] - inicio_i[i-1], left=inicio_i[i-1], color='#639922',\n",
    "            label='Servicio' if i == 1 else \"\")\n",
    "\n",
    "ax.set_yticks(clientes)\n",
    "ax.set_yticklabels([f\"C{i}\" for i in clientes])\n",
    "ax.set_xlabel(\"Tiempo\")\n",
    "ax.set_title(\"Diagrama de Gantt: espera en cola vs. servicio, por cliente\")\n",
    "ax.legend(loc='upper right')\n",
    "ax.grid(alpha=0.3, axis='x')\n",
    "plt.tight_layout()\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bb831fd",
   "metadata": {},
   "source": [
    "## 7. Animación dinámica del proceso completo\n",
    "\n",
    "Reproducimos la construcción de `Q(t)` y `B(t)` **evento por evento**, como una película: en cada cuadro (`frame`) de la animación mostramos los datos hasta ese evento, usando `FuncAnimation`. Al final la convertimos a HTML/JavaScript con `to_jshtml()` para que quede embebida en el notebook con controles de reproducción, sin necesitar un kernel activo para verla."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8039b1d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.animation as animation\n",
    "from IPython.display import HTML\n",
    "\n",
    "fig, axes = plt.subplots(2, 1, figsize=(9, 5), sharex=True)\n",
    "axes[0].set_xlim(0, reloj * 1.05)\n",
    "axes[0].set_ylim(-0.3, max(Qs) + 1)\n",
    "axes[0].set_ylabel(\"Q(t)\")\n",
    "axes[0].grid(alpha=0.3)\n",
    "axes[1].set_xlim(0, reloj * 1.05)\n",
    "axes[1].set_ylim(-0.3, 1.3)\n",
    "axes[1].set_yticks([0, 1]); axes[1].set_yticklabels([\"libre\", \"ocupado\"])\n",
    "axes[1].set_ylabel(\"B(t)\")\n",
    "axes[1].set_xlabel(\"Tiempo\")\n",
    "axes[1].grid(alpha=0.3)\n",
    "\n",
    "linea_q, = axes[0].step([], [], where='post', color='#854F0B', linewidth=2)\n",
    "linea_b, = axes[1].step([], [], where='post', color='#2B6CB0', linewidth=2)\n",
    "texto = axes[0].text(0.02, 0.85, \"\", transform=axes[0].transAxes)\n",
    "\n",
    "def actualizar(frame):\n",
    "    linea_q.set_data(tiempos[:frame+1], Qs[:frame+1])\n",
    "    linea_b.set_data(tiempos[:frame+1], Bs[:frame+1])\n",
    "    evento_actual = trace_rows[frame-1][1] if frame > 0 else 'inicio'\n",
    "    texto.set_text(f\"t = {tiempos[frame]:.2f}   |   evento: {evento_actual}\")\n",
    "    return linea_q, linea_b, texto\n",
    "\n",
    "anim = animation.FuncAnimation(fig, actualizar, frames=len(tiempos), interval=900, 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
}
