"""
=============================================================================
第 1 讲 · 02：test-time compute 预览 —— "Large Language Monkeys" 的三个现象
=============================================================================

对应讲义：lectures/lecture01_overview.md 第 5 节（inference-time scaling）
第 2 讲会把这里的每个现象展开讲（coverage 的 power law、compute-optimal 的分配、Archon 等）。

-----------------------------------------------------------------------------
【核心想法】
-----------------------------------------------------------------------------
"无限猴子定理"说：一只猴子在打字机上乱敲，只要时间足够长，总能敲出莎士比亚全集。
语言模型比猴子聪明得多 —— 它的每次尝试都有一定概率做对。那么：
    **同一道题让模型做很多次（repeated sampling），再用一个 verifier 挑出做对的那次，能做对多少题？**

Brown et al. 2024（Large Language Monkeys）把"至少有一次做对的题目比例"叫做 coverage，
也就是所有题目 pass@k 的平均值（toy_llm.pass_at_k）。

-----------------------------------------------------------------------------
【三个演示】
-----------------------------------------------------------------------------
演示 1  coverage 随采样次数 k 的增长：1B 小模型 vs 100B 大模型。
        · 小模型采很多次，coverage 可以超过大模型采一次；
        · 在相同的计算量（FLOPs）下比较，小模型 + 多采样依然有竞争力；
        · 每道题的成功率 p 分布很不均匀 —— 这决定了 coverage 曲线的形状。
演示 2  temperature 的取舍：温度低，单次更准但多样性差；温度太高，每次都更容易错。
演示 3  coverage 只是"上限"：没有完美的 verifier 时，majority voting 和有噪声的 reward model
        挑出的答案远远落后于 coverage，甚至会随 k 增大而变差 —— 这就是第 3 讲 verification 的动机。

运行时间：约 1 秒（只用标准库）。
"""

import math
import random

from toy_llm import ToyLLM, make_problem_set, mean, pass_at_k, majority_vote, pad, bar, fmt_size

N_SAMPLES = 500                 # 每道题采样的次数（pass@k 的 k 最大只能到这里）
KS = (1, 2, 5, 10, 20, 50, 100, 200, 500)


def collect_samples(model, problems, rng, n, temperature=1.0):
    """
    对每道题采样 n 次，返回每道题的 (答案列表, 是否正确的列表)。

    【参数】model：ToyLLM；problems：题目列表；rng；n：每题采样次数；temperature
    【返回】list，第 i 个元素是 (answers, corrects)，分别是长度为 n 的 list
    【边界】n 很大时会比较慢：每次采样都要逐步模拟。500 次 × 100 题不到 1 秒。
    """
    results = []
    for p in problems:
        answers, corrects = [], []
        for _ in range(n):
            s = model.generate(p, rng, temperature)
            answers.append(s.answer)
            corrects.append(s.correct)
        results.append((answers, corrects))
    return results


def coverage(results, k):
    """
    coverage@k：所有题目的 pass@k 的平均值（用无偏估计）。

    【参数】results：collect_samples 的返回值；k：采样预算（≤ 每题样本数）
    【返回】0~1 的 float
    【自我改进联系】coverage 假设有一个**完美的 verifier**（oracle）：只要 k 次里有一次对，就能把它挑出来。
        对数学题（核对答案）、带 unit test 的代码，这个假设近似成立；对写作、开放问答则不成立（演示 3）。
    """
    return mean(pass_at_k(len(c), sum(c), k) for _, c in results)


# =============================================================================
# 演示 1：repeated sampling 的 coverage 曲线
# =============================================================================
def demo_coverage(problems, small_results, large_results, small, large):
    print("=" * 78)
    print(f"演示 1：coverage@k —— {small.name} 小模型 vs {large.name} 大模型（每题采样 {N_SAMPLES} 次）")
    print("=" * 78)
    print(pad("k", 6) + pad(f"{small.name} coverage", 16, "right") + "  " + pad("", 26)
          + pad(f"{large.name} coverage", 16, "right"))
    for k in KS:
        cs, cl = coverage(small_results, k), coverage(large_results, k)
        print(pad(str(k), 6) + pad(f"{cs:.3f}", 16, "right") + "  " + pad(bar(cs, 25), 26)
              + pad(f"{cl:.3f}", 16, "right") + "  " + bar(cl, 25))

    # 每道题的成功率 p = 正确样本数 / 样本数 的分布
    print("\n每道题的单次成功率 p 的分布（题目数）：")
    bins = [("p = 0（500 次全错）", 0.0, 0.0), ("0 < p ≤ 1%", 0.0, 0.01), ("1% < p ≤ 10%", 0.01, 0.10),
            ("10% < p ≤ 50%", 0.10, 0.50), ("p > 50%", 0.50, 1.0)]
    for label, lo, hi in bins:
        counts = []
        for results in (small_results, large_results):
            ps = [sum(c) / len(c) for _, c in results]
            if hi == 0.0:
                counts.append(sum(1 for p in ps if p == 0))
            else:
                counts.append(sum(1 for p in ps if lo < p <= hi))
        print(f"  {pad(label, 22)} {small.name}：{counts[0]:>3d}    {large.name}：{counts[1]:>3d}")

    # compute-matched：用 FLOPs 衡量预算
    flops_small = mean(small.flops_per_sample(p) for p in problems)
    flops_large = mean(large.flops_per_sample(p) for p in problems)
    ratio = flops_large / flops_small
    print(f"\n一次采样的平均计算量：{small.name} {flops_small:.2e} FLOPs，{large.name} {flops_large:.2e} FLOPs"
          f"（大模型贵 {ratio:.0f} 倍）")
    print("相同 FLOPs 预算下的比较（预算以\"大模型采样一次\"为单位）：")
    for budget in (1, 2, 5):
        k_small = int(budget * ratio)
        if k_small > N_SAMPLES:
            break
        print(f"  预算 = {budget} 次大模型采样：{large.name} pass@{budget} = {coverage(large_results, budget):.3f}"
              f"    {small.name} pass@{k_small} = {coverage(small_results, k_small):.3f}")

    print("\n观察：")
    print("  · coverage 随 k 持续上升，跨越了好几个数量级的 k 都没有停。小模型采样足够多次，")
    print("    coverage 可以超过大模型只采一次的成功率 —— \"模型知道的比我们以为的多\"。")
    print("  · 但它的增长越来越慢：k 每翻 10 倍，增加的 coverage 越来越少。原因在 p 的分布：")
    print("    p 大的题几次就能做对，剩下的都是 p 很小的\"硬骨头\"，需要的采样次数大约是 1/p。")
    print("    第 2 讲会证明：p 的分布在 0 附近有多少\"质量\"，决定了 coverage 曲线的形状（power law）。")
    print("  · 在相同的 FLOPs 下（最后三行），这个玩具世界里小模型 + 重复采样都略胜一筹，但优势随预算增大而缩小")
    print("    （两者都逼近 1）。前提是有一个能挑出正确答案的 verifier（这里是 oracle），")
    print("    而 verifier 本身的成本、延迟都还没算进去。\n")


# =============================================================================
# 演示 2：temperature 的取舍
# =============================================================================
def demo_temperature(problems, model, rng):
    print("=" * 78)
    print(f"演示 2：temperature 对 pass@1 和 coverage@100 的影响（{model.name} 模型，每题采样 100 次）")
    print("=" * 78)
    print(pad("temperature", 13) + pad("pass@1", 9, "right") + pad("coverage@10", 13, "right")
          + pad("coverage@100", 14, "right") + pad("平均不同答案数", 16, "right"))
    for t in (0.0, 0.3, 0.6, 1.0, 1.5, 2.5):
        results = collect_samples(model, problems, rng, 100, temperature=t)
        distinct = mean(len(set(a)) for a, _ in results)
        print(pad(f"{t:.1f}", 13) + pad(f"{coverage(results, 1):.3f}", 9, "right")
              + pad(f"{coverage(results, 10):.3f}", 13, "right")
              + pad(f"{coverage(results, 100):.3f}", 14, "right") + pad(f"{distinct:.1f}", 16, "right"))
    print("\n观察：")
    print("  · temperature = 0（greedy）：100 次采样完全相同，coverage@100 = pass@1，多采样毫无用处。")
    print("    如果某一步模型\"最相信\"的恰好是那个错误套路（bug 的 logit 最高），greedy 永远做不对这道题。")
    print("  · pass@1 在 temperature = 0 时最高，温度越高越低：每一步都更可能选到错误做法。")
    print("  · 温度升高，不同答案变多。coverage@100 在 1.0~1.5 附近最高，到 2.5 开始回落；")
    print("    coverage@10 的最佳温度更低（0.6 左右）。预算 k 越大，越值得用更高的温度去\"探索\"。")
    print("    Codex 论文（Chen et al. 2021）在真实代码任务上也发现：pass@1 的最佳温度约 0.2，pass@100 约 0.8。")
    print("  · 温度太高，真实模型会开始输出不连贯的文字（课上提到大约超过 1.2 就会如此）。\n")


# =============================================================================
# 演示 3：没有 oracle 时怎么挑答案
# =============================================================================
def demo_selection(problems, results, model, rng):
    print("=" * 78)
    print(f"演示 3：coverage 只是上限 —— 不同的挑选方法（{model.name} 模型）")
    print("=" * 78)
    # 有噪声的 reward model（RM）：只看最终答案打分。正确答案平均高 SIGNAL 分，再加上标准正态噪声。
    # 关键：噪声是"每个答案"抽一次，而不是"每个样本"抽一次 —— 同一个错误答案，RM 每次都给同样的
    # （可能偏高的）分数。这模拟了 RM 自己的"误解"：它对某些错误答案就是很有信心。
    # SIGNAL = 2.0 意味着：随便拿一个正确答案和一个错误答案，RM 把它们排对的概率约 92%。
    signal = 2.0
    answer_scores = []
    for p, (answers, _) in zip(problems, results):
        table = {}
        for a in answers:                     # 按样本顺序第一次见到某个答案时给它打分（保证可复现）
            if a not in table:
                table[a] = (signal if a == p.answer() else 0.0) + rng.gauss(0, 1)
        answer_scores.append(table)
    n_repeats = 8   # 每个 k 随机抽 8 次子集取平均，减小方差

    print(pad("k", 6) + pad("coverage(oracle)", 18, "right") + pad("majority vote", 15, "right")
          + pad("best-of-k (RM)", 16, "right"))
    for k in (1, 4, 16, 64, 256, N_SAMPLES):
        mv_hits, rm_hits = [], []
        for _ in range(n_repeats):
            for p, (answers, corrects), table in zip(problems, results, answer_scores):
                idx = rng.sample(range(len(answers)), k)          # 随机抽 k 个样本（不放回）
                vote, _ = majority_vote([answers[i] for i in idx])
                mv_hits.append(vote == p.answer())
                best = max(idx, key=lambda i: table[answers[i]])  # RM 分数最高的样本
                rm_hits.append(corrects[best])
        print(pad(str(k), 6) + pad(f"{coverage(results, k):.3f}", 18, "right")
              + pad(f"{mean(mv_hits):.3f}", 15, "right") + pad(f"{mean(rm_hits):.3f}", 16, "right"))
    plurality_ok = mean(majority_vote(a)[0] == p.answer() for p, (a, _) in zip(problems, results))
    n_distinct = mean(len(set(a)) for a, _ in results)
    print(f"\n用全部 {N_SAMPLES} 个样本估计：正确答案是\"最常见答案\"的题目比例 = {plurality_ok:.3f}；"
          f"每题平均出现 {n_distinct:.0f} 个不同答案")

    print("\n观察：")
    print("  · oracle coverage 一路上升到接近 1，两种不靠 oracle 的方法都远远落在后面，差距越拉越大。")
    print("  · majority vote 涨得很慢，而且有一个天花板：只有\"正确答案恰好是最常见答案\"的题，投票才可能选对。")
    print("    这个比例（上面最后一行）远低于 coverage。原因是 systematic error：固定的错误套路每次给出")
    print("    同一个错误答案，一旦犯错的概率比做对还高，错误答案就会赢得投票，样本再多也没用。")
    print("  · reward model 先升后降：k 小时，多采样让正确答案更可能出现在候选里；k 大时，候选里的")
    print("    错误答案越来越多，总有一些被 RM\"误判\"成高分，把正确答案挤掉。Cobbe et al. 2021")
    print("    （第 3 讲的阅读材料）在 GSM8K 上看到过同样的现象：候选太多，verifier 反而更容易被骗。")
    print("  · \"能生成正确答案\"和\"能认出正确答案\"之间的差距，叫 generation–verification gap（第 3 讲）。\n")


def main():
    problems = make_problem_set(random.Random(10), n_problems=100, min_steps=3, max_steps=8)
    small, large = ToyLLM(1e9), ToyLLM(1e11)
    small_results = collect_samples(small, problems, random.Random(11), N_SAMPLES)
    large_results = collect_samples(large, problems, random.Random(12), N_SAMPLES)

    demo_coverage(problems, small_results, large_results, small, large)
    demo_temperature(problems, small, random.Random(13))
    demo_selection(problems, small_results, small, random.Random(14))


if __name__ == "__main__":
    main()
