"""
=============================================================================
CS486 · 第 7 讲 · 代码 01：自我纠错什么时候有用？——一个四个参数的模型
=============================================================================

【这个脚本要回答的问题】（讲义 §2）
    一轮"检查 → 修改"能不能提高准确率，取决于四个概率（selfcorrect_lab.one_round_gain）：
        TPR    ：答案错了时，检查发现它错了的概率
        FPR    ：答案对了时，检查却说它错了的概率
        p_fix  ：被标为错的错误答案，修改后变对的概率
        p_break：被标为错的正确答案，修改后变错的概率
    新准确率 = acc·(1 − FPR·p_break) + (1 − acc)·TPR·p_fix。

    演示 1：一轮纠错的收益——初始准确率 × 检查的质量。
    演示 2：反复纠错会收敛到哪里？（不动点和初始准确率无关）
    演示 3：Huang et al.（2024）指出的评测漏洞——"用真实答案决定何时停止"会把收益夸大多少？
    演示 4：要让纠错有用，检查需要多好？——给定初始准确率，FPR 的上限。

【运行】
    python3 01_when_correction_helps.py      # 约 5 秒（只用标准库）

【依赖】本目录的 selfcorrect_lab.py（这个脚本只用到其中的纯 Python 部分）。
"""

import os
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

import selfcorrect_lab as sc                                             # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

CHECKERS = {
    "弱（TPR 0.5，FPR 0.3）": (0.5, 0.3),
    "中（TPR 0.8，FPR 0.1）": (0.8, 0.1),
    "强（TPR 0.95，FPR 0.02）": (0.95, 0.02),
}
P_FIX, P_BREAK = 0.5, 0.5
ACCS = (0.3, 0.5, 0.7, 0.9)


def main():
    t0 = time.perf_counter()
    print_header(f"演示 1：一轮纠错的准确率变化（修复成功率 p_fix = {P_FIX}，改坏率 p_break = {P_BREAK}）")
    rows, gains = [], {}
    for name, (tpr, fpr) in CHECKERS.items():
        cells = [name]
        for a in ACCS:
            _, g = sc.one_round_gain(a, tpr, fpr, P_FIX, P_BREAK)
            gains[(name, a)] = g
            cells.append(f"{g * 100:+.1f} 点")
        a_star = sc.fixed_point(tpr, fpr, P_FIX, P_BREAK)
        cells.append(pct(a_star))
        rows.append(cells)
    print_table(["检查的质量", *[f"初始 {pct(a, 0)}" for a in ACCS], "盈亏平衡点"], rows, [26, 12, 12, 12, 12, 12])
    print("  （盈亏平衡点：初始准确率低于它时纠错有收益，高于它时纠错有害——它恰好等于下面演示 2 的不动点）")

    print_header("演示 2：反复纠错——蒙特卡洛（20000 道题），中等检查器 vs 弱检查器")
    rows, curves = [], {}
    shown = (0, 1, 2, 5, 10, 30)
    for name in ("中（TPR 0.8，FPR 0.1）", "弱（TPR 0.5，FPR 0.3）"):
        tpr, fpr = CHECKERS[name]
        for a in (0.3, 0.9):
            c = sc.simulate_rounds(a, tpr, fpr, P_FIX, P_BREAK, rounds=30)
            curves[(name, a)] = c
            rows.append([name, pct(a, 0)] + [pct(c[r]) for r in shown])
    print_table(["检查器", "初始", *[f"{r} 轮" for r in shown]], rows, [24, 6] + [8] * len(shown))
    for name in ("中（TPR 0.8，FPR 0.1）", "弱（TPR 0.5，FPR 0.3）"):
        print(f"  {name} 的不动点（解析解）：{pct(sc.fixed_point(*CHECKERS[name], P_FIX, P_BREAK))}")

    print_header("演示 3：用真实答案决定何时停止（'对了就不再改'）会把收益夸大多少？（弱检查器）")
    rows, oracle = [], {}
    for a in (0.5, 0.9):
        honest = sc.simulate_rounds(a, *CHECKERS["弱（TPR 0.5，FPR 0.3）"], P_FIX, P_BREAK, rounds=10)
        cheat = sc.simulate_rounds(a, *CHECKERS["弱（TPR 0.5，FPR 0.3）"], P_FIX, P_BREAK, rounds=10, oracle_stop=True)
        oracle[a] = (honest, cheat)
        rows.append([pct(a, 0), "检查器决定（真实使用）"] + [pct(honest[r]) for r in (1, 3, 10)])
        rows.append([pct(a, 0), "真实答案决定（评测漏洞）"] + [pct(cheat[r]) for r in (1, 3, 10)])
    print_table(["初始", "何时停止", "1 轮", "3 轮", "10 轮"], rows, [6, 26, 8, 8, 8])

    print_header("演示 4：初始准确率 a 时，要让一轮纠错不亏，检查的 FPR 最多能有多大？（p_fix = p_break = 0.5）")
    rows, fpr_max = [], {}
    for a in (0.5, 0.7, 0.9, 0.95, 0.99):
        cells = [pct(a, 0)]
        for tpr in (0.5, 0.8, 0.95):
            # (1 − a)·tpr·p_fix ≥ a·fpr·p_break  ⇔  fpr ≤ (1 − a)·tpr·p_fix / (a·p_break)
            m = min(1.0, (1 - a) * tpr * P_FIX / (a * P_BREAK))
            fpr_max[(a, tpr)] = m
            cells.append(f"{m:.3f}")
        rows.append(cells)
    print_table(["初始准确率", "TPR 0.5", "TPR 0.8", "TPR 0.95"], rows, [12, 10, 10, 10])

    print_observations([
        f"一轮纠错的收益取决于'救回来的'和'改坏的'谁多（演示 1）。同一个中等检查器，初始 30% 时 "
        f"{gains[('中（TPR 0.8，FPR 0.1）', 0.3)] * 100:+.1f} 个点，初始 90% 时 {gains[('中（TPR 0.8，FPR 0.1）', 0.9)] * 100:+.1f} 个点；"
        f"弱检查器在初始 70% 时已经是 {gains[('弱（TPR 0.5，FPR 0.3）', 0.7)] * 100:+.1f} 个点。"
        "**模型越强，可救的越少、可坏的越多**——自我纠错在弱模型上'有用'，在强模型上可能有害。",
        f"反复纠错收敛到一个和初始准确率无关的不动点（演示 2）：弱检查器下，30% 和 90% 的起点 30 轮后都在 "
        f"{pct(curves[('弱（TPR 0.5，FPR 0.3）', 0.3)][-1])} / {pct(curves[('弱（TPR 0.5，FPR 0.3）', 0.9)][-1])} 左右"
        f"（解析解 {pct(sc.fixed_point(0.5, 0.3, P_FIX, P_BREAK))}）。一个 90% 的模型被一个弱的自我检查'纠正'成了 62.5% 的模型。"
        "不动点 = TPR·p_fix / (TPR·p_fix + FPR·p_break)：它只取决于检查和修复的质量，不取决于模型一开始有多好。",
        f"'对了就停'的评测漏洞（演示 3）：初始 90% 时，诚实的 10 轮纠错把准确率拉到 {pct(oracle[0.9][0][10])}，"
        f"而用真实答案决定何时停止时是 {pct(oracle[0.9][1][10])}——同一个弱检查器，结论从'有害'变成了'大有帮助'。"
        "Huang et al.（2024）指出，一些早期的自我纠错结果正是这样得到的：它们用正确答案判断'是否需要继续修改'。",
        f"强模型对检查器的要求极高（演示 4）：初始 90% 时，即使 TPR = 0.95，FPR 也必须低于 {fpr_max[(0.9, 0.95)]:.3f}；"
        f"初始 99% 时要低于 {fpr_max[(0.99, 0.95)]:.3f}。也就是说，对一个已经很强的模型，自我检查几乎不能对正确的答案起疑。"
        "这就是为什么'内在自我纠错'在强模型上常常失败，而有可靠外部反馈（代码运行结果、测试用例、证明检查器）的纠错往往有效。",
        "这个模型假设检查和修复都是独立的随机事件。真实系统里它们是相关的：同一个模型检查自己时，盲点和生成时的盲点重合"
        "（第 4 讲的离策略盲区），修复往往重复原来的错误。代码 02 在实验台上测量真实的这四个量。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
