"""
=============================================================================
第 13 讲 · 代码 2：MDP 中的乐观——R-max 与 MBIE-EB
=============================================================================

对应讲义：lectures/lecture13_mdp_exploration.md 第 3–4 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
第 11 讲的 UCB：给每个臂加一个"不确定性奖励"，然后贪心。能不能把同样的思想用到 MDP 上？

关键的区别：在 MDP 中，乐观不只是"这个动作可能很好"，而是"**这条路通往的地方**可能很好"。
乐观的奖励加在 (s, a) 上之后，通过**规划**（动态规划）沿着转移传播回去——
离未知区域越近的状态、通往未知区域的动作，价值越高。于是智能体会**有计划地、连续多步地**走向未知区域。
这叫"深度探索"（deep exploration），是 ε-贪心那种"每一步独立地随机一下"做不到的。

    R-max   ：没见够 m 次的 (s, a)，乐观地假设在那里能一直拿到最大奖励
    MBIE-EB ：每个 (s, a) 的奖励加上 β / sqrt(N(s, a))——UCB 的直接推广

本文件：
    1. 深度探索：R-max / MBIE-EB 每个回合能走多远？与 ε-贪心对比
    2. 乐观如何传播：MBIE-EB 规划出的乐观价值随回合的变化
    3. 参数的影响：R-max 的 m、MBIE-EB 的 β

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture13
    python 02_optimism_rmax_mbie.py      （约 65 秒）
"""

import numpy as np

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

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


def mbie_optimistic_values(stats, H, beta):
    """
    按 MBIE-EB 的方式计算乐观的价值 V_opt[h, s]（与 agents.episode_policy 中 "mbie" 分支相同，
    这里单独写出来是为了能**看到**价值本身，而不只是策略）。
    """
    N, A = stats.n_sa.shape
    q_max = (H - np.arange(H))[:, None, None] * np.ones((1, N, A))
    P, R = stats.empirical()
    bonus = beta / np.sqrt(np.maximum(stats.n_sa, 1))
    R_opt = np.where(stats.n_sa > 0, R + bonus, 1.0 + beta)
    Q, V = plan(P, R_opt, H, q_max=q_max)
    Q = np.where(stats.n_sa[None] > 0, Q, q_max)
    return Q.max(axis=2)


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：深度探索——前 15 个回合中，每个回合到达过的最右边的状态（上游是状态 5）")
    print("=" * 78)
    for label, name, param in [("确定性等价 + ε-贪心（ε = 0.3）", "ce_eps", 0.3),
                               ("Q-learning + ε-贪心（ε = 0.3）", "q_eps", 0.3),
                               ("R-max（m = 20）", "rmax", 20),
                               ("MBIE-EB（β = 0.3）", "mbie", 0.3)]:
        out = run_agent(name, P, R, H, 15, seed=0, param=param)
        print("   " + pad(label, 32) + " ".join(str(x) for x in out["max_state"]))
    print("  观察：")
    print("  - ε-贪心的智能体大多在岸边附近打转，偶尔走远一点也很快被冲回来。")
    print("  - 最初一两个回合，R-max 和 MBIE-EB 也只到了状态 1：什么都没见过时，所有动作的乐观价值都一样，")
    print("    行为和随机差不多。但岸边附近的 (s, a) 一旦被试过，乐观就把它们推向上游：")
    print("    MBIE-EB 从第 5 个回合起几乎每个回合都到达上游，它会**连续很多步**坚持向右——")
    print("    哪怕这些步本身没有任何奖励。这就是\"深度探索\"：规划告诉它\"上游那些没见够的 (s, a) 可能很好\"。")
    print("  - R-max（m = 20）要把每个 (s, a) 都试够 20 次才\"放心\"，所以会系统地来回试探，到达上游稍晚一些。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：乐观如何传播——MBIE-EB（β = 0.3）在第 h = 0 步各状态的乐观价值 V_opt(s)")
    print("=" * 78)
    rng = np.random.default_rng(1)
    stats = ModelStats(N)
    cumP = np.cumsum(P, axis=2)
    from agents import episode_policy
    show = {0, 1, 3, 10, 30, 100, 300}
    print(f"  （真实的最优价值 V*(s)，h = 0：{plan(P, R, H)[1][0]}）")
    print("   回合     V_opt(0)  V_opt(1)  V_opt(2)  V_opt(3)  V_opt(4)  V_opt(5)    没见过的 (s,a) 个数")
    for k in range(301):
        if k in show:
            v = mbie_optimistic_values(stats, H, 0.3)[0]
            print(f"   {k:4d}   " + "".join(f"{x:9.2f} " for x in v) + f"        {int((stats.n_sa == 0).sum()):3d}")
        policy = episode_policy("mbie", stats, H, 0.3, rng)
        s = START
        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)
            s = s2
    print("  观察：")
    print(f"  - 一开始什么都没见过，所有状态的乐观价值都是最大可能值 {H}（每一步都拿 1）。")
    print("  - 第 1 个回合之后，起点的乐观价值只有 3.03，甚至低于真实的 V*！原因：智能体在状态 1 试了两次\"向右\"，")
    print("    一次被冲回、一次原地不动，经验模型于是认为\"从状态 1 游不过去\"，上游的乐观价值传不回起点。")
    print("    但 MBIE-EB 给只试过两次的 (状态 1, 向右) 加了不小的探索奖励 β/sqrt(2)，它仍然有吸引力——")
    print("    下一个回合智能体继续在状态 1 向右尝试，游了过去，错误的模型被纠正（第 3 个回合起点的价值回升，")
    print("    第 10 个回合达到 5.05，高于真实值：乐观重新发挥作用）。")
    print("  - 随着岸边附近被探索，那里的乐观价值降到接近真实值；但只要上游还有没见够的 (s, a)，")
    print("    通往上游的状态就一直保持着较高的乐观价值——探索奖励沿着转移**传播**回了起点，")
    print("    把智能体\"拉\"向未知区域。")
    print("  - 数据越来越多，探索奖励 β/sqrt(N) 越来越小，乐观价值逐渐降到真实的 V* 附近——")
    print("    和 UCB 一样：乐观被数据\"消耗掉\"之后，剩下的就是正确的估计。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print(f"演示 3：参数的影响（2000 个回合，20 个随机种子；V* = {V_star:.3f}）")
    print("=" * 78)
    print("   " + pad("算法", 26) + "累积遗憾（第 100 / 500 / 2000 回合）  最后 100 回合  从没到过上游  第一次到达上游")
    print("   " + pad("", 26) + "                                     每回合遗憾    的种子比例    的回合（中位数）")
    rows = ([("确定性等价（对照）", "ce", None)]
            + [(f"R-max（m = {m}）", "rmax", m) for m in [1, 2, 5, 20, 50]]
            + [(f"MBIE-EB（β = {b}）", "mbie", b) for b in [0.01, 0.03, 0.1, 0.3, 1.0, 3.0]])
    for label, name, param in rows:
        s = summarize(name, P, R, H, 2000, range(20), param)
        print("   " + pad(label, 26) + "".join(f"{x:9.1f}" for x in s["cum"])
              + f"            {s['last']:6.3f}        {s['never'] * 100:5.0f}%          {s['first_median']:6.1f}")
    print("  观察：")
    print("  - R-max 的 m 太小（1、2）：试一两次就把 (s, a) 当作\"已知\"，而一两次的转移数据可能错得离谱——")
    print("    比如在状态 1 向右试了一次、恰好原地不动，模型就认为\"游不过去\"。此后它把这个错误模型当真，")
    print("    再也不去验证，35%～50% 的种子从没到过上游。m = 5 仍有 5% 失败。")
    print("  - R-max 的 m 大（20、50）：从不失败，但要把每个 (s, a) 都试够 m 次，前期遗憾很大；")
    print("    一旦所有 (s, a) 都\"已知\"，之后的遗憾恰好是 0（它完全转为利用）。理论要求的 m 还要大得多。")
    print("  - MBIE-EB 对 β 不敏感得多：β 从 0.03 到 0.3，累积遗憾都在 13 左右，比确定性等价小两个数量级以上。")
    print("    它没有\"已知/未知\"的硬门槛，探索奖励随 N 平滑地减小——试得少的 (s, a) 总还带着一点乐观。")
    print("    β 太小（0.01）时，这点乐观不足以抵消错误的模型，5% 的种子失败；β 太大（3）时探索过多。")
    print("  - 又一次看到\"理论 vs 实践\"：理论分析需要的 β、m 都很大（为了高概率保证），实践中小得多的值效果更好。")


if __name__ == "__main__":
    main()
