"""
=============================================================================
CS486 · 第 11 讲 · 代码 01：时间跨度的数学——每步的可靠性、恢复、风险率的形状、METR 式拟合、pass^k
=============================================================================

【这个脚本要回答的问题】（讲义 §2）
    - METR（Kwa et al. 2025）：前沿模型能以 50% 概率完成的任务长度（以人类专家用时计）大约每 7 个月翻一番
      （2025 年初 Claude 3.7 Sonnet 约 50 分钟）；80% 跨度比 50% 跨度短 4–6 倍。
    - Ord（2025）：如果每单位时间失败的风险恒定，成功率随任务长度指数衰减，50% 跨度就是"半衰期"。
      后来的再分析（Hamilton）用 Weibull 分布拟合，发现风险率随任务进行而下降。
    - Sinha et al.（2025，"The Illusion of Diminishing Returns"）：单步准确率的微小提高，会让能完成的任务长度成倍增长。
    - τ-bench（Yao et al. 2024）：pass^k——同一个任务跑 k 次全部成功的概率；gpt-4o 在零售领域 pass^8 不到 25%。
    这些都是同一个简单模型的不同侧面。这个脚本把它们算清楚。

    演示 1：恒定风险率——每步出错率 e 和时间跨度的关系；"收益递减的错觉"。
    演示 2：恢复——发现错误并重做一步，等于把风险率乘以（1 − 发现率）。
    演示 3：风险率的形状——80% 跨度 / 50% 跨度之比，反推 Weibull 的 κ。
    演示 4：异质性本身就会让风险率"下降"。
    演示 5：METR 式拟合——170 个任务、每个跑一次，50% 跨度能估计得多准？
    演示 6：pass@k 与 pass^k——平均成功率相同，一致性可以完全不同。

【运行】
    python3 01_horizon_math.py           # 约 5 秒（只用标准库）
    python3 01_horizon_math.py --quick   # 冒烟测试

【依赖】本目录的 agent_lab.py（这个脚本只用到它的标准库部分）。
"""

import argparse
import math
import os
import random
import sys
import time

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

import agent_lab as ag                                                   # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402


def fmt_len(x):
    """把步数写得好读：1234 → "1,234"；无穷 → "∞"。"""
    return "∞" if x == float("inf") else f"{x:,.0f}"


def main():
    ap = argparse.ArgumentParser(description="时间跨度的数学")
    ap.add_argument("--quick", action="store_true", help="冒烟测试")
    args = ap.parse_args()
    t0 = time.perf_counter()

    # ------------------------------------------------------------------
    print_header("演示 1：恒定风险率——每步出错率 e，任务长度 L 步：成功率 (1 − e)^L；50% / 80% 跨度")
    rows = []
    for e in (0.10, 0.05, 0.02, 0.01, 0.005, 0.001):
        rows.append([f"{100 * (1 - e):.1f}%", fmt_len(ag.horizon_constant(e, 0.5)), fmt_len(ag.horizon_constant(e, 0.8)),
                     pct(ag.survival_constant(e, 10)), pct(ag.survival_constant(e, 100)), pct(ag.survival_constant(e, 1000))])
    print_table(["单步准确率", "50% 跨度（步）", "80% 跨度（步）", "L = 10", "L = 100", "L = 1000"], rows, [12, 16, 16, 9, 9, 9])

    # ------------------------------------------------------------------
    print_header("演示 2：恢复——每步以 e = 2% 出错；出错时以概率 d 被发现并重做（多花一步），没被发现就失败")
    e = 0.02
    rng = random.Random(0)
    rows, d2 = [], {}
    trials = 2000 if args.quick else 20000
    for d in (0.0, 0.5, 0.9, 0.99):
        eff = e * (1 - d)
        ok = steps = 0                                # steps：成功的任务一共执行了多少步（含重做）
        for _ in range(trials):                       # 蒙特卡洛核对解析式：100 步的任务
            done, k = 0, 0
            while done < 100:
                k += 1
                if rng.random() < e:
                    if rng.random() < d:
                        continue                      # 发现了：这一步重做
                    break                             # 没发现：任务失败
                done += 1
            ok += done == 100
            steps += k if done == 100 else 0
        d2[d] = (ag.survival_constant(eff, 100), ok / trials, ag.horizon_constant(eff), steps / max(1, ok))
        rows.append([f"{d:.2f}", f"{100 * eff:.2f}%", pct(d2[d][0]), pct(d2[d][1]), fmt_len(d2[d][2]), f"{d2[d][3]:.1f}"])
    print_table(["发现率 d", "等效风险率", "L = 100 成功（公式）", "（模拟）", "50% 跨度", "成功时平均执行步数"],
                rows, [10, 12, 22, 10, 10, 20])

    # ------------------------------------------------------------------
    print_header("演示 3：风险率的形状——Weibull S(t) = exp(−(t/λ)^κ)；80% 跨度 / 50% 跨度之比只取决于 κ")
    rows = [[f"{k:.1f}", f"{ag.weibull_ratio(k):.3f}", f"{1 / ag.weibull_ratio(k):.1f} 倍"] for k in (1.0, 0.9, 0.8, 0.7, 0.6, 0.5)]
    print_table(["κ", "80% 跨度 / 50% 跨度", "50% 跨度是 80% 的"], rows, [6, 22, 20])
    k4, k6 = ag.kappa_from_ratio(1 / 4), ag.kappa_from_ratio(1 / 6)
    print(f"  METR：80% 跨度比 50% 跨度短 4–6 倍 → κ ≈ {k6:.2f}–{k4:.2f}（κ = 1 是恒定风险率；κ < 1 是风险率随任务进行而下降）")

    # ------------------------------------------------------------------
    print_header("演示 4：异质性——每个任务各自是恒定风险率，但任务之间的风险率不同（对数正态分布）；混在一起看")
    rng = random.Random(1)
    rows, d4 = [], {}
    for spread in (0.0, 0.5, 1.0, 1.5):
        rates = [0.01 * math.exp(rng.gauss(0, spread) - spread ** 2 / 2) for _ in range(2000 if args.quick else 20000)]
        lo, hi = 1.0, 1e6
        for _ in range(100):                          # 二分法求总体成功率为 50% / 80% 的长度
            mid = math.sqrt(lo * hi)
            lo, hi = (mid, hi) if ag.mixture_survival(rates, mid) > 0.5 else (lo, mid)
        h50 = lo
        lo, hi = 1.0, 1e6
        for _ in range(100):
            mid = math.sqrt(lo * hi)
            lo, hi = (mid, hi) if ag.mixture_survival(rates, mid) > 0.8 else (lo, mid)
        h80 = lo
        d4[spread] = (h50, h80, h80 / h50, ag.kappa_from_ratio(h80 / h50))
        rows.append([f"{spread:.1f}", fmt_len(h50), fmt_len(h80), f"{h80 / h50:.3f}", f"{d4[spread][3]:.2f}"])
    print_table(["风险率的离散程度（log 标准差）", "50% 跨度", "80% 跨度", "之比", "等效 κ"], rows, [32, 10, 10, 8, 8])
    print("  （所有设置的平均风险率都是每步 1%；离散程度 0 = 所有任务一样难）")

    # ------------------------------------------------------------------
    print_header("演示 5：METR 式拟合——170 个任务、人类用时在 1 分钟到 16 小时之间（对数均匀），每个任务跑一次")
    rng = random.Random(2)
    true_h50 = 60.0                                    # 真实的 50% 跨度：60 分钟；风险率恒定
    lam = math.log(2) / true_h50

    def simulate(n_tasks, kappa=1.0):
        ts = [math.exp(rng.uniform(math.log(1), math.log(960))) for _ in range(n_tasks)]
        if kappa == 1.0:
            ys = [rng.random() < math.exp(-lam * t) for t in ts]
        else:
            scale = true_h50 / math.log(2) ** (1 / kappa)
            ys = [rng.random() < math.exp(-(t / scale) ** kappa) for t in ts]
        return ts, ys

    rows, d5 = [], {}
    reps = 20 if args.quick else 200
    for n_tasks in (50, 170, 1000):
        for kappa in (1.0, 0.7):
            h50s, ratios = [], []
            for _ in range(reps):
                ts, ys = simulate(n_tasks, kappa)
                a, b = ag.fit_logistic([math.log(t) for t in ts], ys)
                h50, h80 = ag.horizon_from_fit(a, b, 0.5), ag.horizon_from_fit(a, b, 0.8)
                if h50 and h80:
                    h50s.append(h50)
                    ratios.append(h80 / h50)
            h50s.sort()
            ratios.sort()
            lo, hi = h50s[int(0.05 * len(h50s))], h50s[int(0.95 * len(h50s)) - 1]
            d5[(n_tasks, kappa)] = (lo, hi, ratios[len(ratios) // 2])
            rows.append([n_tasks, f"{kappa:.1f}", f"{lo:.0f}–{hi:.0f} 分钟", f"{ratios[len(ratios) // 2]:.3f}"])
    print_table(["任务数", "真实 κ", "50% 跨度估计的 90% 范围（真值 60）", "拟合出的 80%/50% 之比（中位数）"],
                rows, [8, 8, 36, 34])
    print("  （每种设置重复 %d 次：每次重新抽任务、重新模拟成败、重新拟合 logistic）" % reps)

    # ------------------------------------------------------------------
    print_header("演示 6：pass@k 与 pass^k——三组任务，平均成功率都是 60%，但每个任务的成功率分布不同")
    rng = random.Random(3)
    groups = {
        "每个任务都是 60%": [0.6] * 1000,
        "一部分总能做对，其余常常失败（40% 的任务 100%，60% 的任务 33%）": [1.0] * 400 + [1 / 3] * 600,
        "Beta 分布（均值 60%，很分散）": [rng.betavariate(0.6, 0.4) for _ in range(1000)],
    }
    rows, d6 = [], {}
    for name, ps in groups.items():
        m = sum(ps) / len(ps)
        r = [m] + [sum(1 - (1 - p) ** k for p in ps) / len(ps) for k in (8,)] + [sum(p ** k for p in ps) / len(ps) for k in (2, 4, 8)]
        d6[name] = r
        rows.append([name, pct(r[0], 0), pct(r[1], 0), pct(r[2], 0), pct(r[3], 0), pct(r[4], 0)])
    print_table(["任务的成功率分布", "pass@1", "pass@8", "pass^2", "pass^4", "pass^8"], rows, [64, 8, 8, 8, 8, 8])

    if args.quick:
        print("\n（--quick 冒烟测试：不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return
    names = list(groups)
    print_observations([
        f"单步准确率的小提高，换来时间跨度的大提高（演示 1）：单步 99% → 99.5%（只提高 0.5 个百分点），50% 跨度从 "
        f"{fmt_len(ag.horizon_constant(0.01))} 步到 {fmt_len(ag.horizon_constant(0.005))} 步——翻倍；99.9% 时 "
        f"{fmt_len(ag.horizon_constant(0.001))} 步。在单步准确率上看起来'收益递减'，在任务长度上是指数增长"
        "（Sinha et al. 的'收益递减的错觉'）。反过来，单步 98% 的智能体做 100 步的任务，成功率只有 "
        f"{pct(ag.survival_constant(0.02, 100))}。",
        f"恢复和'更不容易出错'在数学上是一回事（演示 2）：出错时 90% 能被发现并重做，等效风险率从 2% 降到 0.2%，"
        f"100 步的成功率从 {pct(d2[0.0][0])} 到 {pct(d2[0.9][0])}，成功的任务平均只多执行 {d2[0.9][3] - 100:.1f} 步。"
        "发现错误需要外部信号——环境的报错、测试、验证器（第 7、8 讲）；所以智能体的可靠性很大程度上是**环境**的性质。",
        f"80% 跨度和 50% 跨度之比诊断风险率的形状（演示 3）：恒定风险率（κ = 1）时之比是 {ag.weibull_ratio(1.0):.2f}"
        f"（短约 3 倍）；METR 报告短 4–6 倍，对应 κ ≈ {k6:.2f}–{k4:.2f}，即风险率随任务进行而下降——和 Hamilton 对 METR 数据的 "
        "Weibull 再分析方向一致。实际意义：比 50% 跨度长得多的任务，成功率比指数模型预测的高；短任务上则更不可靠。",
        f"风险率'下降'不一定说明智能体越做越好（演示 4）：每个任务各自是恒定风险率、只是难度不同，混在一起看，"
        f"80%/50% 之比就从 {d4[0.0][2]:.3f}（κ = {d4[0.0][3]:.2f}）变成 {d4[1.5][2]:.3f}（等效 κ = {d4[1.5][3]:.2f}）——"
        "难的任务早早失败，留下来的都是容易的。这是生存分析里的'脆弱性'效应：总体曲线的形状不能直接解释成个体的行为。",
        f"METR 式拟合的不确定性（演示 5）：170 个任务、每个跑一次，50% 跨度估计的 90% 范围是 "
        f"{d5[(170, 1.0)][0]:.0f}–{d5[(170, 1.0)][1]:.0f} 分钟（真值 60）；50 个任务时 {d5[(50, 1.0)][0]:.0f}–{d5[(50, 1.0)][1]:.0f} 分钟。"
        "倍数级别的趋势（每 7 个月翻一番）经得起这样的噪声，但两个相邻模型之间的比较经不起。"
        f"还有一个偏差：1000 个任务时范围是 {d5[(1000, 1.0)][0]:.0f}–{d5[(1000, 1.0)][1]:.0f} 分钟，反而**不包含**真值——"
        "对数时间上的 logistic 不是指数衰减的正确形状，任务越多，函数形式的偏差越显眼。同样的原因，真实风险率恒定时，"
        f"logistic 拟合出的 80%/50% 之比是 {d5[(1000, 1.0)][2]:.3f} 而不是 {ag.weibull_ratio(1.0):.3f}；METR 用 logistic 拟合得到"
        "'短 4–6 倍'（之比 0.17–0.25），比这个基准小得多，所以'风险率随任务进行而下降'的结论只会更强。",
        f"pass^k 衡量一致性（演示 6）：三组任务的 pass@1 都是 60%，pass^8 却分别是 {pct(d6[names[0]][4], 0)}、"
        f"{pct(d6[names[1]][4], 0)}、{pct(d6[names[2]][4], 0)}。'每个任务都 60%'的智能体几乎从不能连续成功 8 次；"
        "'一部分任务总能做对'的智能体 pass^8 接近它能稳定做对的任务比例。τ-bench 上 gpt-4o 零售领域 pass^1 约 60%、"
        "pass^8 不到 25%：它的失败不只是少数难题，而是很多任务都'有时成功、有时失败'。只报告 pass@1 掩盖了这一点。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
