#!/usr/bin/env python3 """Aggregate decoder training/evaluation JSON without manual transcription.""" from __future__ import annotations import argparse import hashlib import json import statistics from collections import defaultdict from pathlib import Path def mean_sd(values: list[float]) -> dict[str, float | None]: if not values: return {"mean": None, "sample_stddev": None} return { "mean": statistics.fmean(values), "sample_stddev": statistics.stdev(values) if len(values) > 1 else None, } def display(value: dict[str, float | None], digits: int) -> str: mean = value["mean"] stddev = value["sample_stddev"] assert mean is not None return ( f"{mean:.{digits}f}" if stddev is None else f"{mean:.{digits}f} ± {stddev:.{digits}f}" ) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--run-dir", action="append", required=True, type=Path) parser.add_argument("--quality-name", default="quality-imagenette-validation.json") parser.add_argument("--output-json", required=True, type=Path) parser.add_argument("--output-markdown", required=True, type=Path) parser.add_argument("--minimum-runs", type=int, default=3) args = parser.parse_args() if args.minimum_runs < 1: raise SystemExit("--minimum-runs must be positive") runs: list[dict[str, object]] = [] grouped: dict[tuple[object, ...], list[dict[str, object]]] = defaultdict(list) for directory in args.run_dir: training = json.loads((directory / "training.json").read_text(encoding="utf-8")) quality = json.loads((directory / args.quality_name).read_text(encoding="utf-8")) notes_path = directory / "run-notes.json" notes = json.loads(notes_path.read_text(encoding="utf-8")) if notes_path.exists() else {} checks = { "decoder_hash": training["decoder_sha256"] == quality["decoder_sha256"], "model_hash": training["model_sha256"] == quality["model_sha256"], "manifest_hash": training["dataset_manifest_sha256"] == quality["manifest_sha256"], "layers": training["encoder_layers"] == quality["encoder_layers"], "image_size": training["image_size"] == quality["image_size"], } if not all(checks.values()): raise SystemExit(f"inconsistent run {directory}: {checks}") run = { "directory": directory.name, "seed": training["seed"], "encoder_layers": training["encoder_layers"], "image_size": training["image_size"], "training_images": training["training_images"], "evaluation_images": quality["summary"]["count"], "dataset": quality["dataset_name"], "split": quality["split"], "manifest_sha256": quality["manifest_sha256"], "model_sha256": quality["model_sha256"], "decoder_sha256": quality["decoder_sha256"], "training_record_sha256": hashlib.sha256( (directory / "training.json").read_bytes() ).hexdigest(), "quality_record_sha256": hashlib.sha256( (directory / args.quality_name).read_bytes() ).hexdigest(), "training_seconds": training["training_seconds"], # Interactive development runs are valid training/quality records, # but their wall time is not a performance result. Timing is # admitted only when the run notes opt in after documenting a # controlled host state; absence of notes must fail closed. "training_timing_valid": notes.get("training_timing_valid", False), "final_training_l1": training["final_l1"], "global_psnr_db": quality["summary"]["global_psnr_db"], "median_image_psnr_db": quality["summary"]["psnr_db"]["median"], "median_image_ssim": quality["summary"]["ssim"]["median"], "median_image_mae": quality["summary"]["mae"]["median"], } runs.append(run) key = ( run["encoder_layers"], run["image_size"], run["dataset"], run["split"], run["manifest_sha256"], run["model_sha256"], ) grouped[key].append(run) aggregates: list[dict[str, object]] = [] for (layers, size, dataset, split, manifest_sha, model_sha), cell_runs in sorted( grouped.items() ): cell_runs.sort(key=lambda run: int(run["seed"])) seeds = [int(run["seed"]) for run in cell_runs] if len(cell_runs) < args.minimum_runs: raise SystemExit( f"{layers}L/{size}/{dataset}/{split} has {len(cell_runs)} runs; " f"require at least {args.minimum_runs}" ) if len(set(seeds)) != len(seeds): raise SystemExit(f"duplicate seed in {layers}L/{size}/{dataset}/{split}: {seeds}") training_counts = {int(run["training_images"]) for run in cell_runs} evaluation_counts = {int(run["evaluation_images"]) for run in cell_runs} if len(training_counts) != 1 or len(evaluation_counts) != 1: raise SystemExit( f"inconsistent sample counts in {layers}L/{size}/{dataset}/{split}" ) valid_training_times = [ float(run["training_seconds"]) for run in cell_runs if run["training_timing_valid"] ] aggregate = { "encoder_layers": layers, "image_size": size, "dataset": dataset, "split": split, "manifest_sha256": manifest_sha, "model_sha256": model_sha, "seeds": seeds, "runs": len(cell_runs), "training_images_per_run": cell_runs[0]["training_images"], "evaluation_images_per_run": cell_runs[0]["evaluation_images"], "global_psnr_db": mean_sd( [float(run["global_psnr_db"]) for run in cell_runs] ), "median_image_psnr_db": mean_sd( [float(run["median_image_psnr_db"]) for run in cell_runs] ), "median_image_ssim": mean_sd( [float(run["median_image_ssim"]) for run in cell_runs] ), "median_image_mae": mean_sd( [float(run["median_image_mae"]) for run in cell_runs] ), "valid_training_timing_runs": len(valid_training_times), "training_seconds": mean_sd(valid_training_times), } aggregates.append(aggregate) artifact = {"schema_version": 1, "runs": runs, "aggregates": aggregates} args.output_json.parent.mkdir(parents=True, exist_ok=True) args.output_markdown.parent.mkdir(parents=True, exist_ok=True) args.output_json.write_text(json.dumps(artifact, indent=2) + "\n", encoding="utf-8") lines = [ "# Decoder quality summary", "", "Generated from immutable training and per-image evaluation JSON.", "", "| Encoder | Size | Seeds | Validation images/run | Global PSNR (dB) | Median-image PSNR (dB) | Median RGB SSIM | Median MAE |", "|---:|---:|---:|---:|---:|---:|---:|---:|", ] for cell in aggregates: lines.append( f"| {cell['encoder_layers']}L | {cell['image_size']} | {cell['runs']} " f"| {cell['evaluation_images_per_run']} " f"| {display(cell['global_psnr_db'], 2)} " f"| {display(cell['median_image_psnr_db'], 2)} " f"| {display(cell['median_image_ssim'], 4)} " f"| {display(cell['median_image_mae'], 4)} |" ) lines.extend( [ "", "| Seed | Decoder SHA-256 | Training (s) | Final-batch train L1 | Global PSNR (dB) | Median RGB SSIM |", "|---:|---|---:|---:|---:|---:|", ] ) for run in sorted(runs, key=lambda item: (item["encoder_layers"], item["image_size"], item["seed"])): training_seconds = ( f"{float(run['training_seconds']):.1f}" if run["training_timing_valid"] else "excluded" ) lines.append( f"| {run['seed']} | `{run['decoder_sha256']}` " f"| {training_seconds} " f"| {float(run['final_training_l1']):.5f} " f"| {float(run['global_psnr_db']):.2f} " f"| {float(run['median_image_ssim']):.4f} |" ) args.output_markdown.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"wrote {args.output_json} and {args.output_markdown}") if __name__ == "__main__": main()