""" make_report.py -- assemble Results/ into report.pdf. Everything numerical and every figure is pulled straight from Results/metrics and Results/plots, so the report can never drift from the code that produced it. The prose comes from report_notes.md: each `## key` section there is dropped into the matching slot below. Slots with no notes are rendered as a visible TODO box, so nothing can be silently left blank before submission. Usage ----- python make_report.py python make_report.py --notes report_notes.md --out report.pdf """ from __future__ import annotations import argparse import json import re from pathlib import Path from reportlab.lib import colors from reportlab.lib.enums import TA_JUSTIFY from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import cm from reportlab.platypus import (Image, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle) ROOT = Path(__file__).resolve().parent METRICS = ROOT / "Results" / "metrics" PLOTS = ROOT / "Results" / "plots" OVERLAYS = ROOT / "Results" / "overlays" SEED = 16 TAG = f"seed{SEED}" styles = getSampleStyleSheet() S_TITLE = ParagraphStyle("t", parent=styles["Title"], fontSize=20, spaceAfter=6) S_SUB = ParagraphStyle("s", parent=styles["Normal"], fontSize=11, textColor=colors.HexColor("#555555"), alignment=1) S_H1 = ParagraphStyle("h1", parent=styles["Heading1"], fontSize=15, spaceBefore=14, textColor=colors.HexColor("#1a3d6d")) S_H2 = ParagraphStyle("h2", parent=styles["Heading2"], fontSize=12.5, spaceBefore=10, textColor=colors.HexColor("#24527a")) S_BODY = ParagraphStyle("b", parent=styles["BodyText"], fontSize=9.5, leading=13, alignment=TA_JUSTIFY) S_CAP = ParagraphStyle("c", parent=styles["Normal"], fontSize=8, textColor=colors.HexColor("#666666"), alignment=1, spaceAfter=8) S_TODO = ParagraphStyle("todo", parent=S_BODY, backColor=colors.HexColor("#fff3cd"), borderColor=colors.HexColor("#d39e00"), borderWidth=0.7, borderPadding=5, textColor=colors.HexColor("#664d03")) # --------------------------------------------------------------------------- def load_json(name: str): p = METRICS / name if not p.exists(): return None try: return json.loads(p.read_text()) except Exception: return None def find_json(pattern: str): """Return {tag: parsed_json} for every metrics file matching a glob.""" out = {} for p in sorted(METRICS.glob(pattern)): try: out[p.stem] = json.loads(p.read_text()) except Exception: pass return out def parse_notes(path: Path) -> dict: if not path.exists(): return {} text = path.read_text() parts = re.split(r"^##\s+(.+)$", text, flags=re.MULTILINE) notes = {} for i in range(1, len(parts), 2): key = parts[i].strip().lower() body = parts[i + 1].strip() if body and not body.startswith("