"""
=============================================================================
第 7 讲 · 代码 2：广义优势估计（GAE）——在偏差与方差之间调一个旋钮
=============================================================================

对应讲义：lectures/lecture07_actor_critic_ppo.md 第 3 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
策略梯度需要优势 A(s_t, a_t) 的估计。我们手里有两个极端：

    λ = 1：Â_t = G_t - V̂(s_t)          （MC，REINFORCE + 基线）  无偏，方差大
    λ = 0：Â_t = δ_t = r_t + γV̂(s_{t+1}) - V̂(s_t)   （一步 TD，Actor-Critic）  方差小，V̂ 不准时有偏

GAE（Schulman et al., 2016）把所有中间情况统一成一个参数 λ：

    Â_t^{GAE(γ,λ)} = δ_t + (γλ) δ_{t+1} + (γλ)² δ_{t+2} + ... = Σ_l (γλ)^l δ_{t+l}

这和第 3 讲的 TD(λ) 是同一个思想：对不同步数的估计做几何加权平均。

本文件在 chain_mdp 上，固定一个策略，用**精确的**策略梯度作为标准答案，测量：
    - 不同 λ 下，梯度估计的偏差和方差
    - Critic 准确 vs 不准确时，最佳的 λ 如何变化
    - 数据量（批大小）如何影响最佳的 λ

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture07
    python 02_gae.py      （约 20 秒）
"""

import numpy as np

from chain_mdp import N_STATES, GAMMA, policy_probs, exact, exact_gradient, sample_episode, grad_log_pi

np.set_printoptions(precision=3, suppress=True)


# =============================================================================
# 第 1 部分：GAE 的计算
# =============================================================================
def compute_gae(episode, V_hat, lam, gamma=GAMMA):
    """
    计算一个回合中每个时刻的 GAE 优势。

    【高效的递推】直接按定义求和要 O(T²)。注意到
        Â_t = δ_t + γλ · Â_{t+1}
    （和第 1 讲"回报从后往前算"一模一样），从最后一步往前扫一遍即可，O(T)。

    【参数】
        episode : [(s, a, r, s_next), ...]，s_next = None 表示终止。
        V_hat   : Critic 的价值估计（长度 N_STATES）。终止状态的价值视为 0。
        lam     : λ ∈ [0, 1]。

    【返回】长度 T 的数组，第 t 个元素是 Â_t。

    【边界】
        - 回合被截断（没有到达终止状态）时，最后一步应当用 V̂(s_T) 自举，而不是 0。
          本环境的回合总会自然结束，所以这里不需要处理截断（PPO 代码里需要）。
    """
    T = len(episode)
    adv = np.zeros(T)
    running = 0.0
    for t in range(T - 1, -1, -1):
        s, _, r, s_next = episode[t]
        v_next = 0.0 if s_next is None else V_hat[s_next]
        delta = r + gamma * v_next - V_hat[s]
        running = delta + gamma * lam * running
        adv[t] = running
    return adv


def lambda_return_minus_v(episode, V_hat, lam, gamma=GAMMA):
    """
    用**另一种**方式计算同一个量，用来验证 compute_gae：
        λ-回报 G_t^λ = (1-λ) Σ_{n≥1} λ^{n-1} G_t^{(n)}（第 3 讲 8.3 节），
        Â_t = G_t^λ - V̂(s_t)。
    这里直接按定义，把每个 n 步回报都算出来再加权（慢，但直观）。
    """
    T = len(episode)
    rewards = [r for _, _, r, _ in episode]
    out = np.zeros(T)
    for t in range(T):
        G_lam, weight_left = 0.0, 1.0
        for n in range(1, T - t + 1):
            # n 步回报：n 步真实奖励 + γ^n V̂(s_{t+n})（若在 n 步内结束，则就是完整回报）
            G_n = sum(gamma ** k * rewards[t + k] for k in range(n))
            if t + n < T:
                s_tn = episode[t + n][0]
                G_n += gamma ** n * V_hat[s_tn]
                w = (1 - lam) * lam ** (n - 1)
                G_lam += w * G_n
                weight_left -= w
            else:
                G_lam += weight_left * G_n      # 回合结束后，剩下的权重全部给完整回报
                break
        out[t] = G_lam - V_hat[episode[t][0]]
    return out


def gradient_estimate(theta, episode, V_hat, lam):
    """单个回合的策略梯度估计：Σ_t γ^t Â_t ∇log π(a_t|s_t)。"""
    adv = compute_gae(episode, V_hat, lam)
    g = np.zeros_like(theta)
    for t, (s, a, _, _) in enumerate(episode):
        g += GAMMA ** t * adv[t] * grad_log_pi(theta, s, a)
    return g


# =============================================================================
# 第 2 部分：演示
# =============================================================================
def main():
    rng = np.random.default_rng(0)
    theta = np.array([[0.2, 0.0], [0.0, 0.1], [0.1, 0.0], [0.0, 0.3],
                      [-0.1, 0.2], [0.0, 0.0], [0.1, 0.1]])
    pi = policy_probs(theta)
    ex = exact(pi)
    g_true = exact_gradient(theta)
    V = ex["V"]

    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：验证 GAE 的递推公式 = λ-回报减去 V̂（两种完全不同的算法）")
    print("=" * 78)
    ep = sample_episode(theta, rng)
    V_noisy = V + rng.normal(0, 1.0, size=N_STATES)
    for lam in [0.0, 0.5, 1.0]:
        a1 = compute_gae(ep, V_noisy, lam)
        a2 = lambda_return_minus_v(ep, V_noisy, lam)
        print(f"  λ = {lam}：前 5 个时刻 {a1[:5]}，与定义式的最大差 {np.abs(a1 - a2).max():.1e}")
    G = np.zeros(len(ep))
    run = 0.0
    for t in range(len(ep) - 1, -1, -1):
        run = ep[t][2] + GAMMA * run
        G[t] = run
    print(f"  检查 λ=1 时 Â_t = G_t - V̂(s_t)：{np.allclose(compute_gae(ep, V_noisy, 1.0), G - V_noisy[[e[0] for e in ep]])}")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：不同 λ 下梯度估计的偏差与方差（20000 个回合）")
    print("=" * 78)
    critics = {
        "Critic 精确（V̂ = V^π）": V,
        "Critic 有系统误差（V̂ = V^π + [3,-2,2,-3,2,-2,3]）": V + np.array([3, -2, 2, -3, 2, -2, 3.0]),
    }
    lambdas = [0.0, 0.5, 0.8, 0.9, 0.95, 1.0]
    episodes = [sample_episode(theta, rng) for _ in range(20_000)]
    summary = {}
    for cname, V_hat in critics.items():
        print(f"\n  【{cname}】")
        print("      λ      偏差 ‖E[ĝ] - ∇J‖     总方差 tr(Cov[ĝ])")
        rows = []
        for lam in lambdas:
            gs = np.array([gradient_estimate(theta, e, V_hat, lam) for e in episodes])
            bias = np.linalg.norm(gs.mean(axis=0) - g_true)
            var = gs.var(axis=0).sum()
            rows.append((lam, bias, var))
            print(f"    {lam:5.2f}        {bias:8.3f}              {var:8.2f}")
        summary[cname] = rows
    print("\n  （偏差的测量本身有约 sqrt(方差/20000) ≈ 0.01~0.03 的随机误差。）")
    print("  观察：")
    print("  - 偏差：Critic 精确时，所有 λ 都（几乎）无偏（测得的\"偏差\"只是随机误差）；")
    print("    Critic 有误差时，λ 越小偏差越大（越依赖 V̂），λ = 1 仍然无偏（V̂ 只是基线）。")
    print("  - 方差：λ 从 0.5 到 1，方差随 λ 增大而明显增大——包含的未来随机性越来越多。")
    print("    但并不是严格单调的：λ = 0 的方差反而比 λ = 0.5 大。一个原因是每个 δ_t 本身就带噪声，")
    print("    而且 Critic 不准时 δ_t 的波动会被放大（第二张表中 λ = 0 的方差高达 82）。")
    print("    \"λ 越小方差越小\"只是一个粗略的经验法则，不是定理。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：综合误差 = 偏差² + 方差 / 批大小——最佳 λ 取决于 Critic 质量和数据量")
    print("=" * 78)
    for cname, rows in summary.items():
        print(f"  【{cname}】")
        for batch in [1, 10, 100, 1000]:
            mse = [(lam, b ** 2 + v / batch) for lam, b, v in rows]
            lam_best, m_best = min(mse, key=lambda x: x[1])
            shown = "  ".join(f"λ={lam}:{m:7.3f}" for lam, m in mse)
            print(f"    批大小 {batch:4d}：{shown}   → 最佳 λ = {lam_best}")
    print("  观察：")
    print("  - Critic 精确时没有偏差，最佳 λ 只由方差决定（这里是 0.5），与批大小无关。")
    print("  - Critic 有误差时：数据少（批大小小），方差占主导，最佳 λ 较小（0.8）；")
    print("    数据多，偏差占主导，最佳 λ 变大（0.95）。")
    print("  - 所以 Critic 越不准、数据越多，越应该选大 λ（少依赖 Critic）。")
    print("  - 实践中常用 λ = 0.95 左右：既大幅降低方差，又不太依赖 Critic 的准确性。")


if __name__ == "__main__":
    main()
