File size: 9,414 Bytes
81f30c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""Rellena §6.3 (experimento principal) de docs/final_report.md con los números de una corrida.

Uso: python scripts/fill_final_report.py --run-id eval-v1 [--full-index-run-id eval-v1-full]
                                        [--problems data/eval/problems/eval_v1.jsonl]

Reemplaza el bloque entre "### 6.3 Main experiment" y "## 7. Discussion" (tablas en inglés,
generadas; la prosa interpretativa se escribe a mano después). Idempotente.
"""

from __future__ import annotations

import argparse
import json
import re
from itertools import combinations
from math import comb
from pathlib import Path

from ragsolver.orchestrator.run import wilson

CFG_LABEL = {"full": "full", "no_rag": "no RAG", "no_critic": "no critic", "baseline_a": "baseline A", "baseline_b": "baseline B (unverified)"}


def _rows(run_dir: Path) -> list[dict]:
    return [json.loads(l) for l in (run_dir / "results.jsonl").read_text(encoding="utf-8").splitlines() if l.strip()]


def _ci(x) -> str:
    return f"[{x[0]}, {x[1]}]" if isinstance(x, (list, tuple)) and len(x) == 2 else "–"


def main_table(summary: dict) -> list[str]:
    L = ["| Configuration | n | pass@1 | Wilson 95 % | LLM calls (mean, bootstrap 95 %) | Retrieval calls | Tokens (mean) | Wall s (median) | Lemma-use precision |", "|---|---|---|---|---|---|---|---|---|"]
    for c in ("full", "no_rag", "no_critic", "baseline_a", "baseline_b"):
        s = summary.get(c)
        if not s:
            continue
        pass1 = "n/a (unverified)" if c == "baseline_b" else f"**{s['pass@1']:.3f}**"
        L.append(f"| {CFG_LABEL[c]} | {s['n']} | {pass1} | {'–' if c == 'baseline_b' else _ci(s.get('pass@1_wilson95'))} | {s['llm_calls_mean']} {_ci(s.get('llm_calls_mean_ci95_bootstrap'))} | {s.get('retrieval_calls_mean', '–')} | {s['tokens_mean']} | {s['wall_s_median']} | {s.get('lemma_use_precision') if s.get('lemma_use_precision') is not None else '–'} |")
    return L


def pass_by_attempt(summary: dict) -> list[str]:
    cfgs = [c for c in ("full", "no_rag", "no_critic") if summary.get(c, {}).get("pass_by_attempt")]
    if not cfgs:
        return []
    ks = sorted({int(k) for c in cfgs for k in summary[c]["pass_by_attempt"]})
    L = ["| Configuration | " + " | ".join(f"≤ {k} attempt(s)" for k in ks) + " |", "|---|" + "---|" * len(ks)]
    for c in cfgs:
        pba = summary[c]["pass_by_attempt"]
        L.append(f"| {CFG_LABEL[c]} | " + " | ".join(str(pba.get(str(k), pba.get(k, "–"))) for k in ks) + " |")
    return L


def strata(rows: list[dict], meta: dict) -> list[str]:
    rows = [r for r in rows if r["problem_id"] in meta]
    cfgs = [c for c in ("full", "no_rag", "no_critic", "baseline_a") if any(r["config"] == c for r in rows)]
    L = []
    for key, title in (("split", "Split"), ("area", "Area"), ("difficulty", "Level")):
        values = sorted({meta[r["problem_id"]].get(key) for r in rows}, key=str)
        L += [f"*{title}* — pass@1 (solved/n)", "", f"| {title} | " + " | ".join(CFG_LABEL[c] for c in cfgs) + " |", "|---|" + "---|" * len(cfgs)]
        for v in values:
            cells = []
            for c in cfgs:
                rs = [r for r in rows if r["config"] == c and meta[r["problem_id"]].get(key) == v]
                k = sum(r["status"] == "done" for r in rs)
                cells.append(f"{k}/{len(rs)} ({k / len(rs):.2f})" if rs else "–")
            L.append(f"| {v} | " + " | ".join(cells) + " |")
        L.append("")
    return L


def mcnemar(rows: list[dict]) -> list[str]:
    by = {(r["config"], r["problem_id"]): r for r in rows}
    probs = list(dict.fromkeys(r["problem_id"] for r in rows))
    cfgs = [c for c in ("full", "no_rag", "no_critic", "baseline_a") if any(r["config"] == c for r in rows)]
    ok = lambda c, p: by.get((c, p), {}).get("status") == "done"
    pairs = list(combinations(cfgs, 2))
    alpha = 0.05 / max(len(pairs), 1)
    L = [f"Exact McNemar tests on paired outcomes (α = 0.05, Bonferroni over {len(pairs)} pairs → {alpha:.4f}).", "", "| A | B | only A | only B | p | significant |", "|---|---|---|---|---|---|"]
    for a, b in pairs:
        n01 = sum(1 for p in probs if ok(a, p) and not ok(b, p))
        n10 = sum(1 for p in probs if ok(b, p) and not ok(a, p))
        n = n01 + n10
        pval = min(1.0, 2 * sum(comb(n, k) for k in range(0, min(n01, n10) + 1)) / 2**n) if n else 1.0
        L.append(f"| {CFG_LABEL[a]} | {CFG_LABEL[b]} | {n01} | {n10} | {pval:.3f} | {'yes' if pval < alpha else 'no'} |")
    if any(r["config"] == "baseline_b" for r in rows) and "baseline_a" in cfgs:
        self_ok = [p for p in probs if by.get(("baseline_b", p), {}).get("self_assessed_correct")]
        fp = sum(1 for p in self_ok if not ok("baseline_a", p))
        L += ["", f"**False positives avoided** (proposal §8.3): baseline B self-assessed its proof as correct on {len(self_ok)}/{len(probs)} problems; on {fp} of them the direct formalization (baseline A) is rejected by the kernel."]
    return L


def cost(rows: list[dict], run_dir: Path) -> str:
    """Tokens de los resultados + costo exacto de OpenRouter (`usage.cost`) sumado de las trazas,
    que existe desde el 22-09 (DECISIONS #59); para las llamadas anteriores sólo hay tokens."""
    ti = sum(r["tokens_in"] for r in rows)
    to = sum(r["tokens_out"] for r in rows)
    calls = sum(r["llm_calls"] for r in rows)
    exact, n_exact, n_llm = 0.0, 0, 0
    for f in (run_dir / "traces").glob("*.jsonl"):
        for l in f.read_text(encoding="utf-8").splitlines():
            e = json.loads(l)
            if e.get("agent") in ("planner", "generator", "formalizer", "critic", "baseline_a", "baseline_b") and "tokens_in" in e.get("request", {}):
                n_llm += 1
                if "cost_usd" in e["request"]:
                    exact += e["request"]["cost_usd"]; n_exact += 1
    return f"Total: {len(rows)} (problem, configuration) pairs, {calls} LLM calls, {ti / 1e6:.2f} M input / {to / 1e6:.2f} M output tokens. Exact provider cost recorded for {n_exact} of {n_llm} traced calls: US${exact:.3f} (earlier calls predate per-call cost logging)."


def full_index_table(main_summary: dict, full_summary: dict) -> list[str]:
    L = ["| Configuration | Index | n | pass@1 | Wilson 95 % | Lemma-use precision | Retrieval calls |", "|---|---|---|---|---|---|---|"]
    for c in ("full", "no_critic"):
        for label, s in (("3 903-row sample", main_summary.get(c)), ("full Mathlib (278 160)", full_summary.get(c))):
            if s:
                L.append(f"| {CFG_LABEL[c]} | {label} | {s['n']} | {s['pass@1']:.3f} | {_ci(s.get('pass@1_wilson95'))} | {s.get('lemma_use_precision', '–')} | {s.get('retrieval_calls_mean', '–')} |")
    return L


def build_section(run_dir: Path, problems: Path, full_run_dir: Path | None) -> str:
    summary = json.loads((run_dir / "summary.json").read_text(encoding="utf-8"))
    rows = _rows(run_dir)
    meta = {json.loads(l)["problem_id"]: json.loads(l) for l in problems.read_text(encoding="utf-8").splitlines() if l.strip()}
    n_probs = len({r["problem_id"] for r in rows})
    L = [f"### 6.3 Main experiment (`{run_dir.name}`, {n_probs} problems × {len(summary)} configurations)", "",
         "Single model for every configuration (`deepseek/deepseek-v4-flash-0731`, reasoning off, temperature 0.2, seed 13); 3 903-row Mathlib table; k_max = 3; budget 20 calls. Tables generated by `scripts/fill_final_report.py` from `summary.json` and `results.jsonl`.", "",
         *main_table(summary), ""]
    pba = pass_by_attempt(summary)
    if pba:
        L += ["**Pass by attempt** (cumulative pass@1 after k attempts; marginal gain of the critic loop, §8.3):", "", *pba, ""]
    L += ["**Stratified results** (`" + problems.name + "`; dev = prompts/thresholds tuned on it, test = held out):", "", *strata(rows, meta)]
    L += ["**Paired comparisons**", "", *mcnemar(rows), ""]
    L += ["**Cost.** " + cost(rows, run_dir), ""]
    if full_run_dir and (full_run_dir / "summary.json").exists():
        fs = json.loads((full_run_dir / "summary.json").read_text(encoding="utf-8"))
        L += [f"**Effect of the index size** (`{full_run_dir.name}`: `full` and `no critic` re-run with the full Mathlib index, everything else identical; `no RAG` and the baselines do not depend on the index):", "", *full_index_table(summary, fs), ""]
    return "\n".join(L) + "\n"


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--run-id", default="eval-v1")
    ap.add_argument("--full-index-run-id", default="eval-v1-full")
    ap.add_argument("--problems", default="data/eval/problems/eval_v1.jsonl")
    ap.add_argument("--report", default="docs/final_report.md")
    args = ap.parse_args()

    runs = Path("data/eval/runs")
    section = build_section(runs / args.run_id, Path(args.problems), runs / args.full_index_run_id if args.full_index_run_id else None)
    p = Path(args.report)
    text = p.read_text(encoding="utf-8")
    new, n = re.subn(r"### 6\.3 Main experiment.*?(?=\n## 7\. Discussion)", section.rstrip("\n") + "\n", text, count=1, flags=re.S)
    if n != 1:
        raise SystemExit("no se encontró el bloque §6.3 … ## 7 en " + str(p))
    new = new.replace("Main experiment **[TODO eval_v1]**:", "Main experiment:")
    p.write_text(new, encoding="utf-8")
    print(f"§6.3 reescrita en {p} ({len(section.splitlines())} líneas)")


if __name__ == "__main__":
    main()