"""
=============================================================================
CS486 · 第 10 讲 · 代码 01：很多约束同时成立有多难？时间约束怎样精确地处理？
=============================================================================

【这个脚本要回答的问题】（讲义 §2–§3）
    TravelPlanner（Xie et al. 2024）上，GPT-4-Turbo 写的旅行计划只有 0.6% 满足所有约束。但如果逐条看，
    它满足"常识约束"的比例并不低：只规划（sole-planning）模式下，验证集上 80.4% 的常识约束被满足（micro），
    而 8 条常识约束**全部**满足的计划只有 17.2%（macro）。这两个数之间是什么关系？

    另一方面，时间约束（先后、时长、时间窗、截止时间）有一类可以被**精确、快速**地处理：简单时序网络（STN）。
    它不但能判断"排得开吗"，排不开时还能指出"是哪几条约束合起来做不到、差多少"。

    演示 1：独立约束的指数鸿沟 p^m。
    演示 2：用 TravelPlanner 公布的 micro / macro 检验"独立"的近似。
    演示 3：同样的 micro、不同的相关性，macro 可以差很多——只报告一个数不够。
    演示 4：一个家庭聚餐的时间安排（STN）：每个事件最早 / 最晚的时间、松弛。
    演示 5：航班延误——什么时候排不开了？排不开时，负环就是解释。

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

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

import argparse
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 schedule_lab as sch                                               # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

# TravelPlanner（Xie et al. 2024）Table 3，GPT-4-Turbo 的常识约束通过率（%）：(设置, micro, macro)
TRAVELPLANNER = [
    ("两阶段（自己用工具查信息）· 验证集", 61.1, 2.8),
    ("两阶段（自己用工具查信息）· 测试集", 63.3, 2.0),
    ("只规划（信息直接给出）· 验证集", 80.4, 17.2),
    ("只规划（信息直接给出）· 测试集", 80.6, 15.2),
]
N_COMMONSENSE = 8      # TravelPlanner 的常识约束有 8 条（在沙盒内、信息完整、在当前城市、路线合理、餐厅不重复、景点不重复、交通不冲突、最少入住天数）


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：每条约束独立地以概率 p 被满足，m 条全部满足的概率 p^m")
    ms = (1, 5, 10, 20, 50)
    print_table(["p（micro）", *[f"m={m}" for m in ms]],
                [[f"{p:.2f}", *[pct(sch.all_pass(p, m)) for m in ms]] for p in (0.99, 0.95, 0.9, 0.8)],
                [12] + [9] * len(ms))

    print_header(f"演示 2：TravelPlanner 的常识约束（{N_COMMONSENSE} 条），GPT-4-Turbo：公布的 macro vs 独立假设下的 micro^{N_COMMONSENSE}")
    rows = []
    for name, mi, ma in TRAVELPLANNER:
        rows.append([name, f"{mi:.1f}%", f"{ma:.1f}%", pct(sch.all_pass(mi / 100, N_COMMONSENSE))])
    print_table(["设置", "micro（公布）", "macro（公布）", f"micro^{N_COMMONSENSE}"], rows, [36, 14, 14, 12])

    print_header("演示 3：同样的 micro = 80%、m = 8，计划之间'质量'的离散程度不同（蒙特卡洛）")
    rng = random.Random(0)
    trials = 2000 if args.quick else 20000
    rows, d3 = [], {}
    for spread in (0.0, 0.05, 0.2, 0.5):
        mi, ma = sch.correlated_pass(rng, 0.8, 8, spread, trials)
        d3[spread] = (mi, ma)
        rows.append([f"{spread:.2f}", pct(mi), pct(ma)])
    print_table(["离散程度", "micro", "macro"], rows, [10, 10, 10])
    print("  （离散程度 0 = 每个计划的每条约束都以 80% 独立地满足；越大，'好计划'和'坏计划'差得越远，约束的失败越集中在同一批计划上）")

    print_header("演示 4：家庭聚餐的时间安排（简单时序网络）：每个事件最早 / 最晚的时间")
    ok, times = sch.family_dinner().solve()
    print("  约束：")
    seen = set()
    for _, _, _, label in sch.family_dinner().edges:
        if label not in seen:
            seen.add(label)
            print(f"    - {label}")
    rows = [[name, sch.hhmm(lo), sch.hhmm(hi), f"{hi - lo} 分钟"] for name, (lo, hi) in times.items() if name != "Z"]
    print(f"\n  一致：{ok}")
    print_table(["事件", "最早", "最晚", "松弛"], rows, [12, 8, 8, 10])

    print_header("演示 5：航班延误——什么时候排不开？排不开时，是哪几条约束冲突？")
    rows, d5 = [], {}
    for delay in (0, 60, 120, 135, 150, 180):
        net = sch.family_dinner(flight_delay=delay)
        ok, times = net.solve()
        if ok:
            rows.append([f"{delay} 分钟", "排得开", f"接机最晚 {sch.hhmm(times['离开机场'][1])} 离开机场", ""])
        else:
            cyc = net.negative_cycle()
            k = next((i for i, e in enumerate(cyc) if e[0] == 0), 0)   # 从"零点出发"的那条边开始，再倒过来——按时间顺序读
            cyc = list(reversed(cyc[k:] + cyc[:k]))
            short = -sum(e[2] for e in cyc)
            d5[delay] = (cyc, short)
            rows.append([f"{delay} 分钟", "排不开", f"负环上 {len(cyc)} 条约束", f"差 {short} 分钟"])
    print_table(["延误", "结论", "", ""], rows, [10, 8, 30, 12])
    for delay in sorted(d5)[:1] + sorted(d5)[-1:]:
        cyc, short = d5[delay]
        print(f"\n  延误 {delay} 分钟时的冲突（负环，按顺序）：")
        for u, v, w, label in cyc:
            print(f"    - {label}")
        print(f"  这几条合起来要求的时间比可用的时间多 {short} 分钟。要么把其中能商量的一条放松 {short} 分钟（例如开饭推迟 {short} 分钟），"
              "要么改变计划的结构（例如让接机的人直接带客人去餐馆）。")

    if args.quick:
        print("\n（--quick 冒烟测试：不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return
    first_bad = min(d5)
    print_observations([
        f"很多约束同时成立是指数地难（演示 1）：每条约束 95% 满足、20 条约束全部满足只有 {pct(sch.all_pass(0.95, 20))}；"
        f"90% 和 50 条时只有 {pct(sch.all_pass(0.9, 50))}。一个'看起来不错'的计划（大部分约束都满足）和一个'能用'的计划"
        "（所有约束都满足）之间，隔着一道指数鸿沟。",
        f"TravelPlanner 的数字大致符合这个算术（演示 2）：只规划模式验证集上，常识约束 micro 80.4%，"
        f"0.804^8 = {pct(sch.all_pass(0.804, 8))}，公布的 macro 是 17.2%；两阶段模式 micro 61.1% → "
        f"{pct(sch.all_pass(0.611, 8))}，公布 2.8%。数量级对得上，但不完全吻合——约束的失败并不独立。"
        "（这只是一个粗略的检验：8 条常识约束并非对每个计划都同样适用，论文的 micro 是在所有适用的约束上算的。）",
        f"只报告 micro 不够（演示 3）：micro 都是 80%，约束独立时 macro {pct(d3[0.0][1])}，计划质量很分散时 macro {pct(d3[0.5][1])}。"
        "正相关让 macro 比独立假设高得多（失败集中在少数糟糕的计划上）；负相关（例如修好一条约束常常弄坏另一条）则让它更低。比较两个系统时，要同时报告 micro、macro，"
        "最好还有每条约束各自的通过率——第 2 讲'一个数掩盖了结构'的又一个例子。",
        f"时间约束可以被精确地推理（演示 4）：简单时序网络在多项式时间里算出每个事件的时间窗——例如火鸡最晚 "
        f"{sch.hhmm(sch.family_dinner().solve()[1]['火鸡入炉'][1])} 入炉、接机的人最晚 "
        f"{sch.hhmm(sch.family_dinner().solve()[1]['离开机场'][1])} 离开机场。这些数对 LLM 来说要一步步推出来"
        "（一条依赖链有多长，就要串行地加多少次——第 1、3 讲的串行深度），对 STN 来说是一次最短路计算。",
        f"排不开的时候，负环就是解释（演示 5）：接机这条链的松弛是 {sch.family_dinner().solve()[1]['离开机场'][1] - sch.family_dinner().solve()[1]['离开机场'][0]} 分钟"
        f"（演示 4），延误 {first_bad} 分钟时就排不开了；冲突的约束正好构成一个负环（落地 → 取行李 → 开车回家 → 开饭前到家 → 开饭时间固定），"
        f"延误 {first_bad} 分钟时差 {d5[first_bad][1]} 分钟、{max(d5)} 分钟时差 {d5[max(d5)][1]} 分钟。"
        "这就是 Hao et al.（2024）用 SMT 求解器做旅行规划时给出的'不可满足核'：不只说'不行'，还说'是这几条要求合在一起做不到、"
        "放松哪一条可以'。LLM 直接回答时，很少能可靠地做到这一点（第 6、8 讲：无解的题）。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
