"""Score a candidate. The data and all scoring stay in this (trusted) process. The candidate runs in a separate confined process (see host.py) and is handed the stream one byte at a time, each only after it has committed to a prediction. """ from __future__ import annotations import json import os import struct import subprocess import sys import tempfile import threading import time from array import array from pathlib import Path import numpy as np from compsearch.runner import (ALPHABET, CandidateError, byte_cost_bits, code_cost_bits, curriculum_parts) from compsearch.safety import check_source PROJECT_ROOT = Path(__file__).resolve().parent.parent HOST = Path(__file__).resolve().parent / "host.py" FRAME_BYTES = ALPHABET * 8 STARTUP_SLACK_SECONDS = 10 MAX_ERROR_BYTES = 4000 def evaluate(candidate: str | Path, data: str | Path, seconds: float = 300.0, mem_mb: int = 1024) -> dict: """Score a candidate file. Always returns a dict with a 'status' key.""" source = Path(candidate).read_text(encoding="utf-8") problem = check_source(source) if problem: return {"status": "rejected", "error": problem} stream = Path(data).read_bytes() if not stream: return {"status": "error", "error": "the data stream is empty"} env = dict(os.environ) for name in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "NUMEXPR_NUM_THREADS"): env[name] = "1" # one core per candidate, so parallel runs stay fair env["PYTHONHASHSEED"] = "0" timed_out = threading.Event() with tempfile.TemporaryDirectory() as empty_dir: # candidate's working folder has nothing in it process = subprocess.Popen( [sys.executable, str(HOST), str(mem_mb)], cwd=empty_dir, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) def stop() -> None: timed_out.set() process.kill() watchdog = threading.Timer(seconds + STARTUP_SLACK_SECONDS, stop) watchdog.start() started = time.monotonic() try: result = _converse(process, source, stream) finally: watchdog.cancel() process.kill() process.wait() for pipe in (process.stdin, process.stdout): try: pipe.close() except OSError: pass elapsed = time.monotonic() - started if timed_out.is_set() or (result["status"] == "ok" and elapsed > seconds): return {"status": "timeout", "error": f"not finished after {seconds:.0f} seconds"} if result["status"] != "ok": return result code_bits = code_cost_bits(source) byte_bits = result["byte_bits"] data_bpb = float(byte_bits.sum()) / len(stream) scored = { "status": "ok", "bpb": data_bpb + code_bits / len(stream), "data_bpb": data_bpb, "code_bytes": code_bits // 8, "n_bytes": len(stream), "seconds": round(elapsed, 2), } curriculum = load_curriculum(data, len(stream)) if curriculum: parts = curriculum_parts(byte_bits, curriculum) scored["parts"] = {name: round(value, 4) for name, value in parts.items()} scored["bpb"] = sum(parts.values()) / len(parts) + code_bits / len(stream) return scored def load_curriculum(data: str | Path, stream_length: int) -> dict | None: """A stream may come with a .json file beside it that marks its segments.""" path = Path(data).with_suffix(".json") if not path.is_file(): return None curriculum = json.loads(path.read_text(encoding="utf-8")) for segment in curriculum["segments"]: if not 0 <= segment["start"] < segment["end"] <= stream_length: raise ValueError(f"{path}: segment {segment} does not fit the stream") return curriculum def _converse(process: subprocess.Popen, source: str, stream: bytes) -> dict: died = {"status": "crashed", "error": "the candidate process ended early"} encoded = source.encode("utf-8") try: process.stdin.write(struct.pack(" dict: header = process.stdout.read(4) if len(header) != 4: return {"status": "crashed", "error": "the candidate process ended early"} length = min(struct.unpack("