Skip to content

English · Español

Lab 00 — Entorno y utilidades

Objetivo: entregar src/utils/seeding.py y src/utils/logging.py — dos módulos diminutos que importa cada fase posterior.

Tiempo estimado: 60–90 minutos.

Prerrequisitos: el entorno de la Fase 0 está montado (uv, just, ruff, mypy, pytest).


Lo que produces

  1. src/utils/seeding.pyseed_everything(seed: int) -> int. Siembra el random de Python, el default_rng de NumPy y PYTHONHASHSEED. Devuelve la semilla para registro en logs.
  2. src/utils/logging.pyget_logger(name: str). Devuelve un logger de structlog configurado para emitir JSON a stdout, con un campo de contexto phase que quien llama puede fijar.
  3. tests/test_seeding.py — verifica el determinismo: dos llamadas a seed_everything(42) seguidas de np.random.default_rng().random(5) producen arrays idénticos.
  4. tests/test_logging.py — verifica que el logger emite una línea JSON con los campos esperados.
  5. experiments/06-environment-check/manifest.json + README.md — un pequeño smoke test que importa ambas utilidades, registra un mensaje y siembra un RNG.

TODOs

Bloque A — seed_everything

  • Firma de la función: def seed_everything(seed: int) -> int. Con anotaciones de tipo. Docstring.
  • Siembra random.seed(seed) de Python.
  • Fija os.environ["PYTHONHASHSEED"] = str(seed) — nota que esto solo afecta a procesos hijo; documenta el caveat.
  • NO llames a np.random.seed(seed) (estado global — ver theory/01). En su lugar, devuelve la semilla y deja que quien llama haga rng = np.random.default_rng(seed_everything(42)).
  • Decide y documenta: ¿debería sembrarse también torch? La Fase 6 no importa torch en src/; difiere a los tests de la Fase 8 que lo necesitan. Mantén seeding.py libre de torch.
  • Registra un evento de structlog cuando se llame: log.info("seed_set", seed=seed).

Bloque B — get_logger

  • Firma de la función: def get_logger(name: str).
  • Configura structlog una vez en tiempo de importación del módulo (idempotente).
  • Processors: timestamp (ISO 8601, UTC), nivel de log, el mensaje en sí, JSONRenderer.
  • Proporciona un helper bind_phase(phase: str) que devuelve un logger pre-bindeado con un campo phase.
  • Testea que llamar a get_logger("foo").info("bar", x=1) emite JSON válido en stdout que contiene event="bar", x=1, un timestamp y logger="foo".

Bloque C — tests

  • tests/test_seeding.py:
  • Test 1: seed_everything(42) devuelve 42.
  • Test 2: Tras dos llamadas a seed_everything(42), dos llamadas a np.random.default_rng(42).random(5) producen arrays idénticos.
  • Test 3: Tras seed_everything(42), random.random() es determinista.
  • tests/test_logging.py:
  • Test 1: get_logger devuelve un objeto con métodos info, warning, error, debug.
  • Test 2: Capturando stdout, llamar a log.info("event_name", key="value") emite JSON que contiene "event": "event_name" y "key": "value". Usa la fixture capsys.
  • Test 3: bind_phase("phase-06").info("foo") incluye "phase": "phase-06" en el JSON.

Bloque D — smoke test

  • Crea experiments/06-environment-check/ con:
  • check.py — importa ambas utilidades, llama a seed_everything(42), registra un mensaje info con la semilla.
  • manifest.json{seed, versions, config, hardware} según LYNX_CORTEX.md §5.
  • README.md (1 párrafo) — qué verifica este experimento.

Restricciones

  • mypy --strict debe pasar. Todas las funciones tipadas, sin Any implícito.
  • ruff debe pasar. Longitud de línea 100 (el default del repo — confirma en pyproject.toml).
  • pytest debe pasar. Todos los tests en verde.
  • Sin print. Usa log.info incluso en el smoke test.
  • Sin np.random.seed. Usa np.random.default_rng(seed) solamente.
  • Idempotencia. seed_everything(42); seed_everything(42) debe producir el mismo comportamiento aguas abajo que una sola llamada. Lo mismo para get_logger("foo"); get_logger("foo").

Condiciones de parada

Hecho cuando:

  1. Ambos archivos de utilidad existen, ambos pasan mypy --strict y ruff.
  2. Ambos archivos de test existen, todos los tests pasan.
  3. El smoke experiment se ejecuta sin error y emite una línea de log JSON.
  4. git diff no muestra sentencias print en ningún sitio de tus archivos nuevos.

Escollos

  • structlog no configurado antes del primer uso. Si llamas a log.info(...) antes de que corra structlog.configure(...), obtienes la cadena de processors por defecto, no la tuya. Configura en tiempo de importación del módulo logging.py, y asegúrate de que los tests importen logging.py antes de ejercitar loggers.
  • PYTHONHASHSEED fijado después del arranque de Python. Fijar os.environ["PYTHONHASHSEED"] desde dentro de Python no cambia retroactivamente la hash seed del proceso actual (se decidió en el arranque del intérprete). Solo afecta a procesos hijo lanzados vía subprocess. Documéntalo en el docstring; no pretendas que hace al proceso padre determinista.
  • Captura de stdout en tests. capsys.readouterr().out devuelve un string. Para líneas JSON, puede que necesites out.strip().split("\n") y json.loads en cada línea.
  • Olvidar __init__.py. src/utils/ debería ya tener __init__.py desde la Fase 0. Si no, créalo vacío.
  • Importaciones circulares. seeding.py llama a get_logger; logging.py no importa seeding. Mantenlo unidireccional.

Cuándo consultar solutions/

Después de haber:

  1. Commiteado ambos archivos de utilidad.
  2. Ambos archivos de test en verde.
  3. El smoke experiment ejecutado y puedes pegar la salida JSON en tu README.md.

Entonces lee solutions/00-environment-and-utilities-ref.md (escrito en la apertura de fase) para comparar las elecciones de estructura.


Siguiente lab: lab/01-strides-and-views.md.