Source code for mathxlab.exp.run_logging
"""Utilities for run log file discovery.
The repository runs experiments via ``make run EXP=e###`` and stores outputs under
``out/e###/``. Each experiment also writes a run log under ``out/e###/logs/``.
This project intentionally uses **deterministic** run log filenames without any
timestamp information:
``out/e094/logs/run_e094.log``
To keep compatibility with older runs that produced timestamped filenames like
``run_e094_YYYYMMDD_HHMMSS.log``, discovery will migrate the newest legacy log
to the deterministic name the first time it is called.
This module is intentionally tiny and dependency-free.
"""
from __future__ import annotations
import contextlib
from dataclasses import dataclass
from pathlib import Path
from typing import Final
__all__ = [
"RunLogDiscovery",
"infer_run_log_file",
]
# ------------------------------------------------------------------------------
[docs]
@dataclass(frozen=True, slots=True)
class RunLogDiscovery:
"""
Result of run log discovery.
Attributes:
log_file: The discovered or newly created log file path.
was_created: True if the deterministic log file did not exist and was created
(or migrated) by discovery.
Examples:
>>> from mathxlab.exp.run_logging import RunLogDiscovery
>>> RunLogDiscovery # doctest: +SKIP
"""
log_file: Path
was_created: bool
# ------------------------------------------------------------------------------
[docs]
def infer_run_log_file(*, out_dir: Path, experiment_slug: str) -> RunLogDiscovery:
"""
Infer the run log file for an experiment.
Args:
out_dir: Output directory for the experiment (e.g. ``out/e094``).
experiment_slug: Experiment slug used by the Makefile (e.g. ``"e094"``).
Returns:
A :class:`RunLogDiscovery` with the deterministic log file path.
Examples:
>>> from pathlib import Path
>>> from mathxlab.exp.run_logging import infer_run_log_file
>>> info = infer_run_log_file(out_dir=Path("out/e001"), experiment_slug="e001")
"""
logs_dir = out_dir / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
slug = experiment_slug.lower()
canonical: Final[Path] = logs_dir / f"run_{slug}.log"
if canonical.exists():
return RunLogDiscovery(log_file=canonical, was_created=False)
# Backward compatibility: migrate the newest timestamped log (if present) to the
# canonical filename. This preserves the "reuse newest" behavior while keeping
# future runs timestamp-free.
legacy_candidates = sorted(
logs_dir.glob(f"run_{slug}_*.log"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if legacy_candidates:
newest = legacy_candidates[0]
try:
newest.replace(canonical)
except OSError:
# Some platforms/filesystems may not allow atomic replace across devices.
# Fall back to copy + best-effort cleanup.
data = newest.read_bytes()
canonical.write_bytes(data)
with contextlib.suppress(OSError):
newest.unlink()
return RunLogDiscovery(log_file=canonical, was_created=True)
# No existing log files: create an empty deterministic log file.
canonical.write_text("", encoding="utf-8")
return RunLogDiscovery(log_file=canonical, was_created=True)