Validación y Análisis · Tema 03

Verificación de la implementación

El papel ya está auditado. Ahora hay que demostrar que el programa ejecuta exactamente esa regla: ni un paso corrido, ni un evento fuera de orden, ni un azar sin control.

01 · Punto de partida

Del «corre» al «obedece»

Que el programa ejecute sin excepciones no prueba nada: un índice corrido, una cola atendida en orden inverso o una semilla reusada también «corren». La verificación de la implementación pregunta algo más duro:

  • ¿Cada línea ejecuta la regla auditada en el Tema 2?
  • ¿Los eventos ocurren en el orden y el instante correctos?
  • ¿El azar es reproducible y no contamina entre réplicas?
  • ¿Qué evidencia guardás de que todo eso se cumple?

02 · Definición

Qué se verifica y con qué oracle

Cada verificación necesita un oráculo: una fuente independiente de la respuesta correcta.

◈

Casos exactos

Entradas triviales con salida calculable a mano: balance cero, un paso, vaciado total. El oráculo es la aritmética.

≣

Invariantes

Propiedades que siempre valen: 0 ≤ nivel ≤ tope, conservación de clientes, tiempo no retrocede. El oráculo es la lógica.

⧉

Determinismo

Misma spec + misma semilla = misma historia, bit a bit. El oráculo es la repetición.

Oráculos mínimos de una implementación.
ChequeoOráculoFalla típica que caza
Un paso a manoCálculo manualSigno invertido, orden de operaciones
Piso y techoInvariante 0 ≤ s ≤ topeFalta de max/min, desborde silencioso
Misma semilla dos vecesIgualdad de historiasReloj o estado global contamina el azar
Longitud y tiempoConteo de pasosOff-by-one: 11 o 13 pasos en vez de 12

03 · Técnicas

La caja de herramientas del verificador

  1. 1
    Pruebas unitarias con asserts.

    Una función, una regla, un valor esperado exacto. Se ejecutan siempre, en cada cambio. Son la red de seguridad.

  2. 2
    Trazas de eventos.

    Registrar (t, evento, estado antes → después) en las primeras corridas. Leer 10 líneas de traza revela órdenes imposibles.

  3. 3
    Chequeo de invariantes en caliente.

    Afirmar en cada paso que 0 ≤ nivel ≤ tope y que el tiempo avanza. Si algo lo viola, el programa se detiene con mensaje, no sigue mintiendo.

  4. 4
    Semilla fija y aislamiento del azar.

    Un generador por réplica, creado con su semilla. Nada de random global compartido ni de usar la hora como semilla en tests.

  5. 5
    Regresión y revisión de código.

    Guardar una historia «dorada» y compararla en cada cambio. Otra persona lee el diff preguntando «¿qué regla del papel ejecuta esta línea?».

spec auditada→test exacto→traza + invariante→historia dorada

04 · Defectos típicos

Los sospechosos habituales

Tiempo y orden

Off-by-one y eventos cruzados

Devolver 11 historias en vez de 12, atender la llegada antes que la salida del mismo instante, actualizar el reloj después de usarlo. Se cazan contando pasos y leyendo trazas.

Estado y azar

Piso olvidado y semilla reusada

Permitir niveles negativos, compartir el generador entre réplicas, no reiniciar el estado entre corridas. Se cazan con invariantes y test de determinismo.

Unidades en el código
La spec dice litros/paso pero el código suma litros/minuto. Mismo defecto del Tema 2, ahora en la traducción a código.
Flotantes y comparaciones
Comparar nivel == 0 con flotantes en vez de tolerancia o de <= 0. El invariante debe usar margen.
Estado global
Listas o generadores que sobreviven entre llamadas. Cada corrida debe partir de una construcción fresca.

05 · Representación en Python

Tests, traza e invariantes en código

Una implementación verificada no es solo la función que simula: es la función más su suite de chequeos. Este es el molde mínimo.

Python en tu navegador. Ejecutá la suite. Después introducí un bug (quitá el max(0,…)) y observá qué test lo caza primero.

def simular(nivel_inicial, entrada, salida, tope, pasos, traza=None):
    nivel = nivel_inicial
    historia = []
    for t in range(1, pasos + 1):
        antes = nivel
        nivel = min(tope, max(0, nivel + entrada - salida))
        # Invariante en caliente: el programa se acusa a sí mismo
        assert 0 <= nivel <= tope, f"invariante roto en t={t}: {nivel}"
        assert t == len(historia) + 1, "tiempo fuera de orden"
        historia.append(nivel)
        if traza is not None:
            traza.append((t, antes, entrada, salida, nivel))
    return historia

# Suite de verificación (oráculos exactos, sin datos reales)
assert simular(10, 3, 2, 14, 3) == [11, 12, 13]
assert simular(5, 4, 4, 14, 3) == [5, 5, 5]
assert simular(2, 1, 3, 14, 3) == [0, 0, 0]
assert simular(14, 3, 2, 14, 2) == [14, 14]
assert len(simular(10, 3, 2, 14, 12)) == 12
print("Suite OK: 5/5 tests exactos")

La suite tarda milisegundos y se ejecuta en cada cambio. Ese es el punto: verificación barata y continua.

Traza legible + determinismo del azar

import random

def replica_con_azar(semilla, pasos):
    rng = random.Random(semilla)  # un generador por réplica, aislado
    nivel, historia = 10, []
    for t in range(pasos):
        llegada = rng.choice([2, 3, 4])  # azar controlado
        nivel = max(0, min(14, nivel + llegada - 2))
        historia.append(nivel)
    return historia

traza = []
print(simular(10, 3, 2, 14, 3, traza=traza))
for t, antes, e, s, despues in traza:
    print(f"t={t}: {antes} +{e}-{s} -> {despues}")

a = replica_con_azar(42, 6)
b = replica_con_azar(42, 6)
print("determinista:", a == b, a)

06 · Exploración

Laboratorio: suite de verificación en vivo

Este simulador de tanque esconde dos interruptores de bug: desfase de un paso (off-by-one) y olvido del piso (permite negativos). Mové la entrada y activá cada bug para ver qué tests de la suite caen y cómo la curva naranja se separa de la esperada (lima).

EXPERIMENTO 03

¿Obedece el código al papel?

suite: 6 tests · esperada vs. programa

Los resultados numéricos aparecen debajo.
Tests aprobados6 / 6
Divergencia máx.0,00
InvarianteOK
VeredictoVerificada

Implementación verificada: 6/6 tests exactos, sin desfase y con piso.

La curva lima es la historia que la especificación promete. La curva naranja es lo que el programa calcula con los bugs activados. Los puntos rojos marcan pasos donde el invariante 0 ≤ nivel ≤ tope se viola.

Preguntas para explorar

  1. Activá el desfase con entrada 3 y salida 2. ¿Cuántos tests caen? ¿Por qué un error de un paso contamina toda la historia?
  2. Activá «olvidar el piso» con entrada 1 y salida 4. ¿Dónde aparecen los negativos? ¿Qué test los caza aunque el balance parezca razonable?
  3. ¿Por qué la suite usa valores exactos y no tolerancias? ¿Qué pasaría si validaras (con MAE) un programa con desfase?
Ver respuestas sugeridas
  1. Cae el test de longitud/orden y todos los valores se corren un lugar: el error se propaga porque cada nivel depende del anterior. Un off-by-one nunca es «solo un paso».
  2. Los negativos aparecen desde que el nivel cruza cero; los caza el invariante 0 ≤ nivel, no el promedio. El MAE podría disimularlos si luego se compensa.
  3. La verificación exige igualdad exacta: el programa debe obedecer bit a bit. Las tolerancias son para la validación contra la realidad (Tema 1), nunca para disimular un bug.

07 · Comprensión

Confusiones frecuentes

«Si no lanza excepciones, está verificado»

Las excepciones cazan crashes, no obediencia. Solo los oráculos (valores exactos, invariantes, determinismo) verifican.

«Probar con datos reales verifica el código»

Probar con datos reales valida (o invalida) el modelo, pero un bug puede compensarse con un sesgo y parecer correcto. La verificación usa datos sintéticos exactos.

«Los tests lentos se corren al final»

La suite de verificación debe tardar segundos y correrse en cada cambio. Lo que se corre al final, no se corre.

«El azar no se puede testear»

Sí: con semilla fija el azar es determinista. Se testea la igualdad de historias y el aislamiento entre réplicas, no el «parecido aleatorio».

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: cazar el off-by-one

Esta función devuelve un paso de más. Escribí el assert que la delata.

def sim_off(nivel, entrada, salida, pasos):
    h = []
    for t in range(pasos + 1):  # BUG: un paso de más
        nivel = max(0, nivel + entrada - salida)
        h.append(nivel)
    return h

print(len(sim_off(10, 3, 2, 3)))
print("assert len(...) == 3:", len(sim_off(10, 3, 2, 3)) == 3)
Ver solución razonada

Con 3 pasos devuelve 4 valores: len == 4. El assert assert len(sim_off(10,3,2,3)) == 3 falla y delata el range(pasos+1). La corrección es range(pasos) con reloj 1…pasos en la traza.

Ejercicio 2: invariante que grita

Agregá el invariante que detiene esta simulación en cuanto el nivel se vuelve imposible.

def sim_sin_piso(nivel, entrada, salida, pasos):
    h = []
    for t in range(pasos):
        nivel = nivel + entrada - salida  # sin max(0, ...)
        assert 0 <= nivel, f"invariante roto en t={t+1}: nivel={nivel}"
        h.append(nivel)
    return h

print(sim_sin_piso(2, 1, 3, 2))
Ver solución

El segundo paso da nivel −2 y el assert detiene con mensaje. Sin el invariante, la función devolvería [0, -2] en silencio y contaminaría promedios y gráficos.

Ejercicio 3: determinismo entre réplicas

Dos réplicas con la misma semilla deben coincidir; con distinta semilla, casi seguro no. Comprobalo.

Ver una posible respuesta
import random
def replica(semilla):
    rng = random.Random(semilla)
    return [rng.choice([2, 3, 4]) for _ in range(5)]

print(replica(7) == replica(7))  # True: determinista
print(replica(7) == replica(8))  # casi seguro False: independientes

Si la primera comparación diera False, el azar no está aislado (generador global, hora como semilla o estado compartido). La verificación lo rechaza.

09 · Síntesis

Ideas para recordar

  • Verificar la implementación es demostrar obediencia exacta a la spec: casos exactos, invariantes y determinismo.
  • La suite es barata, rápida y continua: se ejecuta en cada cambio, no al final.
  • Trazas legibles e historias doradas convierten errores invisibles en fallas gritonas.
  • Un generador por réplica con semilla fija vuelve testeable lo aleatorio.
  • Solo con implementación verificada tiene sentido depurar a fondo (Tema 4) y validar contra datos (Temas 5–8).

En el próximo tema veremos la depuración de simulaciones: técnicas sistemáticas para encontrar y aislar el bug cuando la suite se pone roja.