"""
=============================================================================
第 13 讲 · 代码 3：后验采样强化学习（PSRL）、无模型的计数奖励，以及随问题规模的变化
=============================================================================

对应讲义：lectures/lecture13_mdp_exploration.md 第 5–7 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
1. **PSRL**：第 12 讲汤普森采样在 MDP 上的推广。每个回合开始时：
       - 从 MDP 的后验中抽一个 MDP（转移用 Dirichlet 后验，奖励用 Beta 后验）；
       - 求出这个 MDP 的最优策略；
       - 按它走完整个回合（**整个回合坚持同一个策略**——这正是"深度探索"的来源）。
2. **无模型的计数奖励**：不学模型，直接在 Q-learning 的目标里加 c / sqrt(N(s, a))，
   并把 Q 乐观地初始化。深度 RL 中"基于计数的探索"就是这个思路的推广。
3. **规模**：链越长，ε-贪心越绝望；乐观和后验采样的遗憾只按多项式增长。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture13
    python 03_psrl_and_scaling.py      （约 85 秒）
"""

import numpy as np

from agents import ModelStats, summarize
from riverswim import H_DEFAULT, START, make_riverswim, pad, plan, policy_value

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


def psrl_sample(stats, rng):
    """
    从后验中抽一个 MDP（与 agents.py 中 "psrl" 分支相同，这里单独写出来便于观察）。

    【转移】对每个 (s, a)：P(· | s, a) ~ Dirichlet(1 + 转移计数)。
        先验 Dirichlet(1, ..., 1)：在没有数据时，认为"转移到任何一个状态都同样可能"。
    【奖励】R(s, a) ~ Beta(1 + 成功次数, 1 + 失败次数)。
    【返回】(P_sample, R_sample)
    """
    g = rng.gamma(1.0 + stats.n_sas)
    P = g / g.sum(axis=2, keepdims=True)
    R = rng.beta(1.0 + stats.r_sum, 1.0 + stats.n_sa - stats.r_sum)
    return P, R


def main():
    N, H = 6, H_DEFAULT
    P, R = make_riverswim(N)
    V_star = plan(P, R, H)[1][0, START]

    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：PSRL 的一次运行——前 12 个回合，每个回合抽到的 MDP 与它的最优策略")
    print("=" * 78)
    rng = np.random.default_rng(3)
    stats = ModelStats(N)
    cumP = np.cumsum(P, axis=2)
    print(f"  真实的最优价值 V* = {V_star:.3f}")
    print("   回合   抽到的 MDP 认为   这个策略在真实    抽到的\"状态3向右   本回合到达的    上游(状态5)的")
    print("          能拿到的价值      MDP 中的价值      前进\"的概率        最右状态        \"向右\"被试过几次")
    for k in range(12):
        Ps, Rs = psrl_sample(stats, rng)
        Qs, Vs = plan(Ps, Rs, H)
        policy = Qs.argmax(axis=2)
        true_v = policy_value(P, R, H, policy)
        s, far = START, 0
        for h in range(H):
            a = int(policy[h, s])
            r = float(rng.random() < R[s, a])
            s2 = min(int(np.searchsorted(cumP[s, a], rng.random())), N - 1)
            stats.add(s, a, r, s2)
            far = max(far, s2)
            s = s2
        print(f"   {k + 1:3d}      {Vs[0, START]:8.2f}         {true_v:8.3f}          {Ps[3, 1, 4]:6.2f}              {far}               {int(stats.n_sa[5, 1])}")
    print("  观察：")
    print("  - 一开始后验很宽：抽到的 MDP 往往认为能拿到很高的价值（第 1 个回合是 11.25，真实最优只有 3.06），")
    print("    但按它的策略在真实 MDP 中几乎拿不到任何东西。原因是先验 Dirichlet(1, ..., 1)：")
    print("    没见过的 (s, a) 在样本中可以\"一步跳到任何地方\"，包括直接跳到上游——")
    print("    抽到的策略去尝试这些想象中的\"捷径\"，试一次就发现不行，后验就排除了一个错误的可能。")
    print("  - \"状态 3 向右前进\"的概率在样本中从 0.01 到 0.46 不等（真实值 0.35）——后验的宽窄就是不确定性。")
    print("  - 第 9 个回合，抽到的样本对上游足够乐观，智能体**坚持同一个样本的策略**一路游到了上游。")
    print("    每个回合只抽一次、整个回合坚持执行，而不是像 ε-贪心那样每一步重新掷骰子——这是后验采样实现深度探索的方式。")
    print("  - 此后几个回合的真实价值又降了下来：后验仍然很宽，还在排除其他可能。数据变多后，后验收窄，")
    print("    抽到的 MDP 越来越接近真实的 MDP，策略收敛到最优（见演示 2）。")
    print("  - PSRL 没有需要调的探索参数——探索的程度完全由后验的宽窄决定（但先验的选择很重要，见演示 3）。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print(f"演示 2：所有算法的比较（N = {N}，H = {H}，2000 个回合，20 个随机种子；V* = {V_star:.3f}）")
    print("=" * 78)
    print("   " + pad("算法", 32) + "累积遗憾（第 100 / 500 / 2000 回合）  最后 100 回合  从没到过上游")
    print("   " + pad("", 32) + "                                     每回合遗憾    的种子比例")
    rows = [("确定性等价 + ε-贪心（ε = 0.1）", "ce_eps", 0.1),
            ("Q-learning + ε-贪心（ε = 0.1）", "q_eps", 0.1),
            ("Q-learning + 计数奖励（c = 0.3）", "q_bonus", 0.3),
            ("Q-learning + 计数奖励（c = 1）", "q_bonus", 1.0),
            ("R-max（m = 20）", "rmax", 20),
            ("MBIE-EB（β = 0.1）", "mbie", 0.1),
            ("PSRL", "psrl", None)]
    for label, name, param in rows:
        s = summarize(name, P, R, H, 2000, range(20), param)
        print("   " + pad(label, 32) + "".join(f"{x:9.1f}" for x in s["cum"])
              + f"            {s['last']:6.3f}        {s['never'] * 100:5.0f}%")
    print("  观察：")
    print("  - 三种有\"深度探索\"的基于模型的方法（R-max、MBIE-EB、PSRL）的累积遗憾比 ε-贪心小一到两个数量级，")
    print("    而且几百个回合之后就几乎不再增长——它们学会了最优策略。")
    print("  - 无模型的\"Q-learning + 计数奖励\"也能学会（最后每回合遗憾接近 0），但慢得多：")
    print("    它没有模型，乐观只能通过一次次的 Q 更新慢慢传播回去，而基于模型的方法每个回合都做一次完整的规划。")
    print("    （无模型算法的遗憾是用实际经过的轨迹估计的，带有随机噪声，最后 100 回合的值可能略微为负。）")
    print("  - 调好参数的 MBIE-EB 在这里最好；PSRL 不需要调参，表现也很稳定。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：链越长会怎样？（H = 4N，1000 个回合，10 个随机种子；表中为 1000 个回合的累积遗憾）")
    print("=" * 78)
    rows = [("确定性等价 + ε-贪心（ε = 0.1）", "ce_eps", 0.1),
            ("Q-learning + 计数奖励（c = 0.3）", "q_bonus", 0.3),
            ("R-max（m = 20）", "rmax", 20),
            ("MBIE-EB（β = 0.1）", "mbie", 0.1),
            ("MBIE-EB（β = 1）", "mbie", 1.0),
            ("PSRL", "psrl", None)]
    Ns = [6, 8, 10, 12]
    vstars = []
    for n in Ns:
        Pn, Rn = make_riverswim(n)
        vstars.append(plan(Pn, Rn, 4 * n)[1][0, START])
    print("   " + pad("算法", 32) + "".join(f"     N = {n:<3d}" for n in Ns))
    print("   " + pad("（V*）", 32) + "".join(f"{v:12.2f}" for v in vstars))
    for label, name, param in rows:
        vals = []
        for n in Ns:
            Pn, Rn = make_riverswim(n)
            s = summarize(name, Pn, Rn, 4 * n, 1000, range(10), param, checkpoints=(1000,))
            tag = f"{s['cum'][0]:9.1f}" + ("*" if s["never"] > 0 else " ")
            vals.append(tag)
        print("   " + pad(label, 32) + "".join(f"  {v}" for v in vals))
    print("   （* 表示有种子在 1000 个回合里从没到过上游）")
    print("  观察：")
    print("  - ε-贪心：N ≥ 8 时，所有种子都从没到过上游——遗憾就是\"一直守着岸边\"的 1000 × (V* - 岸边的价值)。")
    print("  - R-max、MBIE-EB：遗憾随 N 大致线性增长，始终能学会。这对应理论中\"样本复杂度是 S、A、H 的多项式\"。")
    print("  - PSRL 的遗憾随 N 增长得更快。原因之一是先验：Dirichlet(1, ..., 1) 认为\"转移到任何状态都同样可能\"，")
    print("    状态越多，这个先验越\"分散\"，抽到的 MDP 里经常出现\"一步跳到上游\"这样离谱的转移，")
    print("    需要更多数据才能排除它们。实际应用中常用更稀疏的先验（比如只允许转移到相邻状态）。")
    print("    另外，MBIE-EB 的 β = 0.1 是在 N = 6 上挑出来的，对 PSRL 并不完全公平。")
    print("  - 无模型的计数奖励 Q-learning 能学会，但随 N 增长明显变慢——在 1000 个回合内越来越难收敛。")


if __name__ == "__main__":
    main()
