Simulación de Eventos Discretos · Tema 02

Simulación orientada a eventos

Cómo saltar de evento en evento con un reloj, un calendario y una rutina principal — en vez de revisar el sistema segundo a segundo — y por qué es más rápido y exacto.

01 · Punto de partida

Revisar cada minuto o poner el despertador

En el Tema 1 vimos que la fila del banco solo cambia cuando alguien llega o termina. Entre esos instantes, el estado está congelado. Entonces hay dos formas de simular 12 minutos:

Opción A (pasos fijos): mirar el reloj en t=0, 1, 2, …, 12 y preguntar “¿pasó algo?”. La mayoría de las veces la respuesta es “no”, pero igual gastaste 13 revisiones.
Opción B (orientada a eventos): anotar que lo próximo ocurre en t=2, poner el “despertador” allí y dormir hasta ese momento. Solo despertás cuando hay trabajo real.

La Opción B es la simulación orientada a eventos: el reloj salta al próximo evento en vez de avanzar de a un paso.

  • ¿Cuántas revisiones inútiles hace el método de pasos fijos?
  • ¿Cómo sabe el simulador cuál es “lo próximo” que va a ocurrir?
  • ¿Qué pasa si dos eventos caen en el mismo instante?
  • ¿Cuándo conviene cada método?

02 · Definición

Los seis ingredientes inseparables

Toda simulación por eventos, desde una fila de banco hasta un data center, usa las mismas seis piezas. Si falta una, el simulador no funciona:

◷

Reloj t

Tiempo virtual. No es el tiempo real de tu PC: es el tiempo del mundo simulado, que salta de evento en evento.

◧

Estado s

Lo mínimo para continuar: cuántos hay, si el servidor está ocupado, a qué hora termina lo actual.

▤

Calendario FEL

FEL (Future Event List, Lista de Eventos Futuros) ordenada por tiempo: [(2, llegada), (5, salida), …]. Siempre sabemos “lo próximo”.

⚙

Rutinas de evento

Un if por tipo: si es llegada suma uno y agenda la próxima; si es salida libera y atiende al siguiente.

∑

Estadísticas

Contadores que se actualizan en cada evento: esperas acumuladas, máximos, utilización. Sin esto no hay medidas.

⟳

Rutina principal

El bucle: saca lo próximo del FEL → avanza el reloj → ejecuta la rutina → agenda futuro → repite hasta el fin.

Pasos fijos frente a próximo evento con el mismo sistema.
AspectoPasos fijos (dt = 1)Orientada a eventos
Avance del relojt = 0, 1, 2, …, 12 (13 pasos)t salta solo a instantes con eventos (ej. 2, 4, 5…)
Trabajo inútilRevisa instantes “nada”Cero: cada iteración procesa un evento real
ExactitudAproximada si el evento cae entre pasosExacta: usa el tiempo real del evento (2.37, no 2)
CostoCrece con T/dt (horizonte / resolución)Crece con nº de eventos (ideal si son ralos)
Cuándo convieneCambios en casi cada paso o flujo continuoEventos ralos e irregulares: colas, redes, logística

03 · La máquina

La rutina principal, línea por línea

Este es el esqueleto que repetiremos en todo el curso (Temas 15–18 lo desarmarán pieza por pieza). Leelo como una receta:

Inicializar→Sacar lo próximo→Avanzar reloj y procesar→¿Fin? No: repetir
  1. 0
    Inicializar.

    Reloj en 0, estado inicial, FEL con los primeros eventos (ej. primera llegada), estadísticas en cero, condición de fin (ej. simular hasta t=100 o 1000 clientes).

  2. 1
    Sacar lo próximo.

    Extraer del FEL el evento con menor tiempo. Si el FEL se vacía, no hay futuro: la simulación termina.

  3. 2
    Avanzar el reloj.

    t = tiempo_evento. El salto puede ser de 0.1 o de 50: el reloj no es uniforme.

  4. 3
    Procesar y agendar.

    Actualizar estado y estadísticas; según el tipo, agendar nuevos eventos futuros (una llegada agenda la siguiente; una salida puede agendar la próxima salida).

  5. 4
    Chequear fin.

    Si t >= T_fin o se atendieron N clientes, cerrar y reportar. Si no, volver al paso 1.

Evento incondicionado
Ocurrirá sí o sí a su hora: la próxima llegada ya sorteada. Va directo al FEL.
Evento condicionado
Solo ocurre si se cumple una condición: “empezar servicio” requiere servidor libre y alguien en cola. Se intenta al procesar cada evento.
Empate de eventos
Dos eventos a la misma hora: se necesita regla explícita (ej. primero salidas, luego llegadas) para que la simulación sea reproducible.
Reloj vs. CPU
El reloj simulado puede saltar 100 unidades en un milisegundo real. No confundas tiempo simulado con tiempo de ejecución.

04 · Paso a paso

Una mini-cola simulada a mano

Sistema: un cajero, llega uno en t=2 y otro en t=4, cada atención dura 3. Estado inicial vacío. Seguí la rutina principal como lo haría el programa:

Traza manual: el reloj salta 0 → 2 → 4 → 5 → 8.
Reloj tEvento extraídoEstado despuésFEL después
0— (inicialización)N=0, libre[(2, llegada), (4, llegada)]
2llegadaN=1, ocupado[(4, llegada), (5, salida)]
4llegadaN=2 (1 en cola + 1 en servicio)[(5, salida)]
5salidaN=1, sigue ocupado[(8, salida)]
8salidaN=0, libre[] → fin

Con pasos fijos dt=1 habrías revisado t=0…8 (9 veces) para solo 4 eventos útiles. Con eventos saltaste directo a 2, 4, 5 y 8. Esa diferencia se vuelve abismal en un año simulado con eventos cada horas.

Analogía del despertador

Pasos fijos = mirar cada minuto

Te despertás 480 veces para tomar un colectivo que pasa 3 veces. Funciona, pero no dormís.

Analogía del despertador

Eventos = 3 alarmas

Programás alarmas a las horas exactas y dormís entre ellas. Cada despertada es productiva.

05 · Representación en Python

De revisar todo a atender solo eventos

Primero la versión ingenua por pasos fijos: recorre cada instante aunque no pase nada. Después la versión por eventos con heapq, la cola de prioridad que Python usa como FEL (siempre entrega lo de menor tiempo).

Python en tu navegador. Ejecutá ambos bloques y compará: dan el mismo estado final, pero uno revisa 13 instantes y el otro solo los eventos. Podés editar intervalos y re-ejecutar.

# Versión A: pasos fijos dt=1 hasta T=12 (13 revisiones)
T = 12
N = 2
llega_cada, sirve_cada = 2, 3
revisiones = 0
for t in range(T + 1):
    revisiones += 1
    llega = (t % llega_cada == 0 and t != 0)
    sale = (t % sirve_cada == 0 and t != 0 and N > 0)
    N = N + (1 if llega else 0) - (1 if sale else 0)
print("final:", N, "| revisiones:", revisiones)
import heapq

# Versión B: por eventos. El reloj salta solo a tiempos con eventos.
reloj = 0
N = 2
fel = [(2, "llegada"), (3, "salida"), (4, "llegada"), (6, "llegada")]
heapq.heapify(fel)
procesados = 0

while fel:
    t, e = heapq.heappop(fel)  # lo próximo
    reloj = t                  # ¡el salto!
    if e == "llegada":
        N += 1
    else:
        N = max(0, N - 1)
    procesados += 1

print("final:", N, "| reloj:", reloj, "| eventos procesados:", procesados)

06 · Exploración

Laboratorio: cuántas revisiones te ahorrás

Mismo sistema del Tema 1 (llega uno cada A, se atiende uno cada S, 12 pasos), pero ahora contado de dos formas: pasos fijos (siempre 13 revisiones) frente a eventos (solo instantes que cambian el estado). Movete los controles y mirá el ahorro.

EXPERIMENTO 02

Despertar solo cuando hay trabajo

relojes: t ← próximo evento del FEL

Los resultados numéricos aparecen debajo.
Revisiones paso fijo13
Eventos procesados0
Ahorro0%
Estado final N(12)0

Mueve los controles para comparar.

La escalera lima es el estado. Las marcas verticales verdes son instantes con evento (el reloj los visita); las grises son instantes “nada” que pasos fijos revisa en vano y eventos saltea.

Preguntas para explorar

  1. Con llega cada 4 y sirve cada 4, ¿cuántos eventos hay en 12 pasos? ¿Qué ahorro lográs?
  2. Con llega cada 1 y sirve cada 1, ¿sigue conveniendo eventos? ¿Cuándo se achica la ventaja?
  3. Si el horizonte fuera 365 días con 20 eventos, ¿cuántas revisiones haría cada método con dt=1 día?
Ver respuestas sugeridas
  1. Llegadas en 4, 8, 12 (3) y salidas efectivas en 4, 8, 12 (3, compensadas): pocos instantes útiles → ahorro grande (≈50–60%). El reloj salta, por ejemplo, 0→4→8→12.
  2. Hay evento en casi cada instante → el ahorro se achica. Cuando todo cambia en cada paso, pasos fijos y eventos empatan; la ventaja de eventos es máxima con eventos ralos.
  3. Pasos fijos: 366 revisiones. Eventos: 20 (+1 inicial). Ahorro ≈95%. En simulaciones reales (años con miles de eventos ralos) la diferencia es de horas a segundos.

07 · Comprensión

Confusiones frecuentes

«El reloj avanza de a uno»

En simulación por eventos el reloj toma el tiempo del próximo evento, sea +1 o +47. Avanzar de a uno es pasos fijos, no eventos.

«Procesar en cualquier orden da igual»

No: procesar una salida antes que una llegada simultánea puede cambiar quién espera. El FEL debe estar ordenado y los empates resueltos con regla explícita.

«Más eventos = simulación más lenta que pasos fijos»

Depende. Si hay evento en cada paso, empatan. Si los eventos son ralos (lo típico en colas), eventos gana por lejos porque saltea los huecos.

«El FEL es una lista cualquiera»

El FEL es una cola de prioridad ordenada por tiempo (heap). Si insertás al final sin ordenar, “lo próximo” deja de ser lo próximo y la causalidad se rompe.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: contar el ahorro

Ejecutá y explicá por qué ambos dan el mismo final con distinto costo.

T = 12
fijos = T + 1  # t = 0..12
# eventos que cambian el estado: llegadas en 2,4,6,8,10,12 y salidas en 3,6,9,12
eventos = [2, 3, 4, 6, 8, 9, 10, 12]
print("fijos:", fijos, "| eventos:", len(eventos))
print("ahorro:", round(100 * (1 - len(eventos) / fijos)), "%")
Ver solución razonada

13 revisiones fijas frente a 8 eventos → ahorro ≈38%. El estado final coincide porque los instantes “nada” (1, 5, 7, 11) no cambian nada; revisarlos es trabajo inútil.

Ejercicio 2: el salto del reloj

Sin ejecutar primero: ¿qué tiempos toma el reloj si el FEL inicial es [(2, llegada), (4, llegada), (5, salida)]? Comprobalo.

import heapq
fel = [(2, "llegada"), (4, "llegada"), (5, "salida")]
heapq.heapify(fel)
reloj = 0
visitas = [reloj]
while fel:
    t, e = heapq.heappop(fel)
    reloj = t
    visitas.append(reloj)
print(visitas)
Ver solución

[0, 2, 4, 5]: el reloj nunca vale 1 ni 3. “Dormir” entre 2 y 4 es legal porque por definición nada puede cambiar allí.

Ejercicio 3: empate con regla explícita

Una llegada y una salida empatan en t=5 con N=1. ¿Qué pasa si procesás salida primero vs. llegada primero? Codificá ambas y decidí una regla.

Ver una posible respuesta
# Regla A: salidas primero (típica en colas: libera antes de ocupar)
N = 1
N = max(0, N - 1)  # salida
N = N + 1          # llegada
print("salida primero:", N)  # 1

# Regla B: llegadas primero
N = 1
N = N + 1          # llegada
N = max(0, N - 1)  # salida
print("llegada primero:", N)  # 1
# Aquí empatan, pero con capacidad finita o estadísticas de espera difieren:
# por eso la regla debe escribirse. Recomendada: salidas (0) antes que llegadas (1).

Con cola infinita el N final coincide, pero la espera medida y los rechazos con capacidad finita no. Conclusión: los empates se resuelven con prioridad explícita, ej. (t, 0, "salida") antes que (t, 1, "llegada").

09 · Síntesis

Ideas para recordar

  • Orientada a eventos = reloj que salta al próximo evento del FEL, no pasos fijos.
  • Componentes: estado, reloj, FEL (heap), rutinas por tipo, estadísticas y rutina principal.
  • Cada iteración útil procesa un evento; los huecos “nada” se saltean sin perder exactitud.
  • La ventaja es máxima con eventos ralos; con cambios en cada paso, empatan.
  • El FEL debe estar ordenado y los empates resueltos con regla explícita y reproducible.

En el próximo tema definiremos el concepto de evento: qué es, qué tipos hay y cómo diseñarlos para que el simulador sea correcto.