Source code for emflow.run.verifier

"""Verifier: Experiment + untrusted-submission policy.

The evaluation loop itself is the ordinary :class:`~emflow.run.experiment.Experiment`
โ€” leakage protection lives in the data layer (every observation is a
feed-served view), not in a special runner. What the Verifier adds is
submission hygiene for *untrusted* models:

* the submitter provides a **fresh, untrained** :class:`~emflow.models.predictor.Predictor`
  (directly or via ``get_model()``); the verifier fits it itself on the
  official training view, so it can never have been fit on test targets;
* scoring runs on the **holdout** split, which agents must never iterate on
  (they hillclimb on ``validation``);
* the score is computed by the verifier from the returned predictions โ€” nothing
  the submission prints is trusted;
* results append to a leaderboard CSV, and when the problem carries published
  reference scores the scorecard reports where the submission would have
  placed in the original competition.

Honest scope note: for problems whose full dataset ships locally (the tutorial
problems), a determined adversary can read the raw files outside the feed โ€”
local verification protects against *accidental* leakage. The trustworthy
setup for agent benchmarking keeps holdout labels in a private ``rb://`` repo
that only the verifier's token can read, and runs submissions with no network.
"""

from __future__ import annotations

import csv
import datetime as _dt
from pathlib import Path
import typing as t

from ..models.predictor import Predictor
from ..problems import Problem, load_problem
from .experiment import Experiment
from .result import Result

DEFAULT_LEADERBOARD = Path(__file__).resolve().parents[2] / "submissions" / "leaderboard.csv"

LEADERBOARD_FIELDS = [
    "submitted_at", "submission", "problem", "split", "objective", "score",
    "n_origins", "n_scored", "persistence_score", "beats_persistence", "rank",
]


[docs] class Verifier: """Train and strictly evaluate a submitted predictor on a problem's holdout.""" def __init__(self, problem: t.Union[str, Problem] = "swedish-temperatures:ar", leaderboard_path=DEFAULT_LEADERBOARD, split: str = "holdout"): self.problem = load_problem(problem) if isinstance(problem, str) else problem self.leaderboard_path = Path(leaderboard_path) if leaderboard_path else None self.split = split
[docs] def verify(self, submission, name=None, record=True, verbose=True, mode: str = "auto", metadata: t.Optional[dict] = None) -> Result: """Evaluate a submission and return its :class:`Result`. ``submission`` is a fresh :class:`Predictor` or a zero-arg factory returning one (the ``get_model()`` convention). ``metadata`` (optional, str/number values) is recorded verbatim on the leaderboard row and echoed on the scorecard. This is the seam for search harnesses to make performance claims honest about selection: rebase-hillclimb passes e.g. ``{"n_trials": N}`` from its run journal, and any deflation/PBO math lives there โ€” emflow only records it. """ if metadata: collisions = set(metadata) & set(LEADERBOARD_FIELDS) if collisions: raise ValueError( f"metadata keys {sorted(collisions)} collide with leaderboard fields" ) model = submission() if callable(submission) and not isinstance(submission, Predictor) \ else submission if not isinstance(model, Predictor): raise TypeError( f"submission must be an emflow Predictor (or a factory returning " f"one), got {type(model).__name__}" ) if name: model.name = name result = Experiment(self.problem, model, split=self.split).run(mode=mode) if verbose: self._print_scorecard(result, metadata) if record and self.leaderboard_path: self._append_leaderboard(result, metadata) return result
# -- reporting ---------------------------------------------------------------- def _row(self, r: Result, metadata: t.Optional[dict] = None) -> dict: skill = r.analysis.get("PersistenceSkill", {}) row = { "submitted_at": _dt.datetime.now().isoformat(timespec="seconds"), "submission": r.model, "problem": r.problem, "split": r.split, "objective": r.objective, "score": round(r.score, 6), "n_origins": r.n_origins, "n_scored": r.n_scored, "persistence_score": round(skill.get("persistence_score", float("nan")), 6), "beats_persistence": skill.get("beats_persistence", ""), "rank": r.rank_against(self.problem) or "", } if metadata: row.update({key: metadata[key] for key in sorted(metadata)}) return row def _print_scorecard(self, r: Result, metadata: t.Optional[dict] = None) -> None: row = self._row(r, metadata) print(f"\n{'=' * 64}") print(f"Submission : {r.model}") print(f"Problem : {r.problem} [{r.split} split]") print(f"Scored on : {r.n_scored:,} timestamps over {r.n_origins:,} origins") print(f"{'-' * 64}") print(f"{r.objective:<32}{r.score:>12.4f}") skill = r.analysis.get("PersistenceSkill", {}) if skill: print(f"{'Persistence baseline':<32}{skill['persistence_score']:>12.4f}") for key, value in r.analysis.get("QuantileCalibration", {}).items(): print(f"{key:<32}{value:>12.3f}") print(f"{'-' * 64}") if skill: verdict = ("PASS โ€” beats persistence" if skill["beats_persistence"] else "FAIL โ€” does not beat persistence") print(f"Verdict : {verdict}") if row["rank"]: n = len(self.problem.reference_scores) print(f"Ranking : would have placed {row['rank']} of {n} " f"in the original competition") if metadata: for key in sorted(metadata): print(f"{key:<11}: {metadata[key]}") print(f"Leakage : impossible by construction (feed-served observations)") print(f"{'=' * 64}\n") def _append_leaderboard(self, r: Result, metadata: t.Optional[dict] = None) -> None: path = self.leaderboard_path path.parent.mkdir(parents=True, exist_ok=True) row = self._row(r, metadata) if path.exists(): # Respect the existing header: fill absent columns, warn on and # drop keys the file doesn't know about (schema is fixed at creation). with path.open(newline="") as f: fieldnames = next(csv.reader(f)) dropped = set(row) - set(fieldnames) if dropped: import warnings warnings.warn( f"leaderboard {path} has no columns for metadata keys " f"{sorted(dropped)}; they were not recorded" ) row = {k: row.get(k, "") for k in fieldnames} new = False else: fieldnames = LEADERBOARD_FIELDS + sorted(set(row) - set(LEADERBOARD_FIELDS)) new = True with path.open("a", newline="") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) if new: writer.writeheader() writer.writerow(row) print(f"Appended to leaderboard: {path}")