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.
| Fin por | Condición | Usalo cuando |
|---|---|---|
| Vacío | FEL = ∅ | Siempre como seguridad; a veces como fin natural |
| Horizonte | reloj > T o próximo > T | “Simular 30 días” (lo más común) |
| Conteo | atendidos ≥ N | “Atender 1000 clientes” (compara configuraciones) |
03 · Invariantes
Cuatro guardianes del bucle sano
- 1Monotonía.
assert t >= relojantes de asignar. Un retroceso delata FEL desordenada o evento con hora vieja. - 2Despacho total.
Todo tipo del FEL tiene su rutina en el
dict. Un tipo sin rutina esKeyErrora mitad de la corrida larga. - 3Estado válido.
Tras cada evento:
0 ≤ ocupados ≤ c,N ≥ 0. Si falla, el bug está en tomar/liberar, no en el azar. - 4Cierre 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 unifgigante.- 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:
| Paso | Extrae | Reloj | Despacha | N después |
|---|---|---|---|---|
| 0 | — (init) | 0 | FEL=[2:L, 4:L], N=0 | 0 |
| 1 | llegada 2 | 2 | N=1, ocupa, agenda salida 5 | 1 |
| 2 | llegada 4 | 4 | N=2, a cola (sin agenda) | 2 |
| 3 | salida 5 | 5 | N=1, el de cola inicia, agenda salida 8 | 1 |
| 4 | salida 8 | 8 | N=0, libera; FEL vacía → fin | 0 |
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.
Cámara lenta del bucle
pop → reloj=t → despachar → ¿fin?
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
- Con A=2, S=3, avanzá k=0→1→2: ¿qué extrae cada vuelta y a dónde salta el reloj?
- En la vuelta de la salida 5, ¿qué agenda? ¿Por qué la llegada 4 no agendó nada?
- ¿En qué vuelta la FEL queda vacía? ¿Es fin natural o por horizonte?
Ver respuestas sugeridas
- 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).
- 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.
- 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.