Simulación de Eventos Discretos · Tema 16

Rutina principal de simulación

El bucle que une reloj, FEL y eventos — extraer, saltar, despachar y repetir hasta el fin sin romper la causalidad.

01 · Punto de partida

El director de la orquesta

Ya tenés los músicos: reloj (11), avance (12), calendario (13), FEL (14) e inicialización (15). Falta el director que los hace sonar en orden: la rutina principal. Sin ella son piezas sueltas; con ella, un simulador.

Su partitura cabe en cinco líneas que ya conocés del Tema 2, pero ahora con invariantes, despacho y cierre profesional. Este tema la deja lista para copiar en todos tus simuladores.

  • ¿En qué orden exacto se extrae, se salta y se procesa?
  • ¿Cómo elige el programa la rutina de cada tipo?
  • ¿Qué chequeos delatan un FEL desordenado?
  • ¿Cuándo se para: por vacío, por horizonte o por conteo?

02 · Definición

El bucle en cinco líneas

◇

Extraer

heappop del FEL: lo mínimo por (t, prio, seq). Si está vacío, no hay futuro.

⏭

Saltar

reloj = t con assert t >= reloj. El salto es la esencia del método.

⚙

Despachar

rutinas[tipo](datos): cada tipo sabe actualizar estado, stats y agendar futuro.

inicializar→extraer + saltar→despachar + agendar→¿fin? cerrar : repetir
Fines típicos y cuándo usarlos.
Fin porCondiciónUsalo cuando
VacíoFEL = ∅Siempre como seguridad; a veces como fin natural
Horizontereloj > T o próximo > T“Simular 30 días” (lo más común)
Conteoatendidos ≥ N“Atender 1000 clientes” (compara configuraciones)

03 · Invariantes

Cuatro guardianes del bucle sano

  1. 1
    Monotonía.

    assert t >= reloj antes de asignar. Un retroceso delata FEL desordenada o evento con hora vieja.

  2. 2
    Despacho total.

    Todo tipo del FEL tiene su rutina en el dict. Un tipo sin rutina es KeyError a mitad de la corrida larga.

  3. 3
    Estado válido.

    Tras cada evento: 0 ≤ ocupados ≤ c, N ≥ 0. Si falla, el bug está en tomar/liberar, no en el azar.

  4. 4
    Cierre con regla.

    Al parar por T, decidir qué hacer con lo en curso (truncar, completar o ignorar) y escribirlo. Sin regla, dos programas dan dos respuestas. (Tema 19)

Despacho por dict
rutinas = {"llegada": al_llegar, "salida": al_salir}: agregar un tipo es agregar una entrada, no un if gigante.
Peek de fin
Mirar fel[0] antes de extraer para parar sin pasarse de T: “lo próximo es 13 > 12, cierro”.
Contador de pasos
Iteraciones procesadas (≠ tiempo). Sirve para logs, barras de progreso y límites de seguridad anti-bucle infinito.
Rutina ≠ evento
La rutina principal es el bucle; las rutinas de evento son las funciones por tipo. No mezclar nombres evita llamar al bucle desde un evento.

Rutina sana

Corta y vigilada

10 líneas con asserts, despacho por dict y fin explícito. Cada bug canta en el acto.

Rutina enferma

Larga y muda

if tipo == … elif … de 200 líneas sin asserts: el retroceso viaja en silencio y las stats mienten.

04 · Traza completa

El banco, dirigido paso a paso

Llegadas en 2 y 4, S=3, 1 caja, T=12. Cada fila es una vuelta del bucle:

Cinco vueltas: extraer, saltar, despachar.
PasoExtraeRelojDespachaN después
0— (init)0FEL=[2:L, 4:L], N=00
1llegada 22N=1, ocupa, agenda salida 51
2llegada 44N=2, a cola (sin agenda)2
3salida 55N=1, el de cola inicia, agenda salida 81
4salida 88N=0, libera; FEL vacía → fin0

05 · Representación en Python

El simulador mínimo que sí simula

Este bloque es el corazón reutilizable del curso: inicializa, repite el bucle con despacho y cierra con estadísticas. Copialo como plantilla de los Temas 17–20 y 38.

Python en tu navegador. Ejecuta el mini-simulador: procesa la traza de arriba y reporta atendidos y espera media. Cambiá A y S y re-ejecutá.

import heapq, itertools

def simular(A=2.0, S=3.0, T=12.0):
    fel, seq = [], itertools.count()
    heapq.heappush(fel, (A, 1, next(seq), "llegada", {}))
    if 2 * A <= T:
        heapq.heappush(fel, (2 * A, 1, next(seq), "llegada", {}))
    reloj, N, ocupado = 0.0, 0, 0
    cola, espera_total, atendidos = [], 0.0, 0

    def al_llegar(d):
        nonlocal N, ocupado
        N += 1
        if not ocupado:
            ocupado = 1
            heapq.heappush(fel, (reloj + S, 0, next(seq), "salida", {}))
        else:
            cola.append(reloj)

    def al_salir(d):
        nonlocal N, ocupado
        nonlocal espera_total, atendidos
        N -= 1
        atendidos += 1
        if cola:
            llegada = cola.pop(0)
            espera_total += reloj - llegada
            heapq.heappush(fel, (reloj + S, 0, next(seq), "salida", {}))
        else:
            ocupado = 0

    rutinas = {"llegada": al_llegar, "salida": al_salir}
    while fel and fel[0][0] <= T:
        t, _, _, tipo, datos = heapq.heappop(fel)
        assert t >= reloj
        reloj = t
        rutinas[tipo](datos)
        assert N >= 0 and ocupado in (0, 1)
    return {"reloj": reloj, "N": N, "atendidos": atendidos,
            "wq_media": espera_total / max(1, atendidos)}

print(simular())

06 · Exploración

Laboratorio: dirigir vuelta por vuelta

Elegí llegadas (A) y servicio (S). La traza genera todas las vueltas hasta T=12. Mové paso k para pararte en cada vuelta: la línea blanca marca el reloj, la ficha dice qué se extrajo y qué quedó, y la curva muestra N hasta allí. Es la rutina principal en cámara lenta.

EXPERIMENTO 16

Cámara lenta del bucle

pop → reloj=t → despachar → ¿fin?

Los resultados numéricos aparecen debajo.
Paso k / total0 / 0
Reloj0
Eventoinit
N tras procesar0

Mueve k para dirigir.

Escalera tenue: historia completa N(t). Tramo lima: lo ya procesado hasta k. Blanca: reloj actual. La ficha narra la vuelta.

Preguntas para explorar

  1. Con A=2, S=3, avanzá k=0→1→2: ¿qué extrae cada vuelta y a dónde salta el reloj?
  2. En la vuelta de la salida 5, ¿qué agenda? ¿Por qué la llegada 4 no agendó nada?
  3. ¿En qué vuelta la FEL queda vacía? ¿Es fin natural o por horizonte?
Ver respuestas sugeridas
  1. k=0 init (reloj 0); k=1 llegada 2 (salta 0→2, agenda salida 5); k=2 llegada 4 (salta 2→4, a cola sin agenda).
  2. Agenda la salida 8 del que estaba en cola (reintento). La llegada 4 encontró caja ocupada: su “agenda” fue entrar a la lista, no al FEL.
  3. Tras la salida 8 (o la última ≤12): FEL vacía antes del horizonte → fin natural. Con más llegadas sería por T=12.

07 · Comprensión

Confusiones frecuentes

«Procesar sin extraer está bien si sé lo que viene»

No: solo el pop garantiza el mínimo con empates resueltos. Mirar un índice arbitrario atiende el futuro en desorden.

«El reloj se incrementa con dt en el bucle»

Eso es pasos fijos. En la rutina por eventos el reloj se asigna desde el evento extraído, nunca se suma.

«El despacho con if/elif largos da igual»

Funciona hasta que agregás el tipo 6 y olvidás una rama. El dict hace total al despacho: tipo sin entrada falla en voz alta.

«Parar es solo salir del while»

Parar exige regla de cierre (qué hacer con lo en curso) y reporte. Salir sin cerrar deja stats a medias.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: contar vueltas vs. tiempo

Con visitas [0, 2, 4, 5, 8], ¿cuántas vueltas y cuánto tiempo simulado?

visitas = [0.0, 2.0, 4.0, 5.0, 8.0]
print("vueltas:", len(visitas) - 1, "| simulado:", visitas[-1] - visitas[0])
Ver solución razonada

4 vueltas, 8.0 simulados. Vueltas (CPU) ≠ tiempo (historia): 4 pops cruzaron 8 minutos virtuales.

Ejercicio 2: despacho total

¿Qué pasa si llega un tipo sin rutina?

rutinas = {"llegada": lambda d: None}
try:
    rutinas["salida"]({})
except KeyError as e:
    print("KeyError:", e, "-> agregar la rutina")
Ver solución

Falla en voz alta en vez de ignorar en silencio: el dict es tu amigo. Así se caza el tipo olvidado.

Ejercicio 3: peek de cierre

Con próximo en 13 y T=12, ¿extraer o cerrar?

Ver una posible respuesta
T, proximo = 12.0, 13.0
if proximo > T:
    print("cierro en", T, "sin extraer 13.0")
else:
    print("extraigo")

Se cierra mirando con fel[0] sin extraer: el evento 13 queda fuera del horizonte (Tema 19 decide si trunca o ignora).

09 · Síntesis

Ideas para recordar

  • Bucle: extraer mínimo → reloj=t → despachar por dict → agendar → chequear fin.
  • Guardianes: monotonía, despacho total, estado válido y cierre con regla.
  • Fin por vacío, horizonte o conteo; peek con fel[0] para no pasarse.
  • El bucle no cambia en el curso; solo crecen los tipos y las stats.
  • Vueltas (iteraciones) ≠ tiempo simulado: 4 pops pueden cruzar 8 minutos.

En el próximo tema veremos la generación de eventos: cómo nacen llegadas y fines con sus horas.