#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build the AFEM group meeting PPTX deck -- Chinese version."""

from pptx import Presentation
from pptx.util import Inches, Pt, Emu, Cm
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn

# Color palette (Nature-style restrained)
WHITE       = RGBColor(0xFF, 0xFF, 0xFF)
BLACK       = RGBColor(0x1A, 0x1A, 0x1A)
DARK_GRAY   = RGBColor(0x33, 0x33, 0x33)
BODY_GRAY   = RGBColor(0x44, 0x44, 0x44)
CAPTION     = RGBColor(0x88, 0x88, 0x88)
LIGHT_LINE  = RGBColor(0xDD, 0xDD, 0xDD)
LIGHTER_LINE = RGBColor(0xEE, 0xEE, 0xEE)
ACCENT_BLUE = RGBColor(0x2C, 0x5F, 0x8A)
ACCENT_TEAL = RGBColor(0x3A, 0x7B, 0x7B)
ACCENT_WARM = RGBColor(0x8B, 0x45, 0x2C)
ACCENT_GREEN = RGBColor(0x3A, 0x7B, 0x4F)
HIGHLIGHT_BG = RGBColor(0xE8, 0xF0, 0xF8)
WARN_BG     = RGBColor(0xFE, 0xF3, 0xE8)
TABLE_HDR   = RGBColor(0xE8, 0xF0, 0xF8)
TABLE_ALT   = RGBColor(0xF5, 0xF7, 0xFA)

SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)

TITLE_SIZE = Pt(28)
SUBHEAD_SIZE = Pt(18)
BODY_SIZE = Pt(14)
SMALL_SIZE = Pt(12)
CAPTION_SIZE = Pt(8)
TAKEAWAY_SIZE = Pt(11)

prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
blank_layout = prs.slide_layouts[6]


def add_blank_slide():
    return prs.slides.add_slide(blank_layout)

def set_slide_bg(slide, color=WHITE):
    bg = slide.background
    fill = bg.fill
    fill.solid()
    fill.fore_color.rgb = color

def add_rect(slide, left, top, width, height, fill_color=None, line_color=None, line_width=None):
    shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
    shape.line.fill.background()
    if fill_color:
        shape.fill.solid()
        shape.fill.fore_color.rgb = fill_color
    else:
        shape.fill.background()
    if line_color:
        shape.line.color.rgb = line_color
        shape.line.fill.solid()
        if line_width:
            shape.line.width = line_width
    return shape

def add_textbox(slide, left, top, width, height, text="", font_size=BODY_SIZE,
                font_color=BODY_GRAY, bold=False, alignment=PP_ALIGN.LEFT,
                font_name='Microsoft YaHei', anchor=MSO_ANCHOR.TOP, line_spacing=1.3):
    txbox = slide.shapes.add_textbox(left, top, width, height)
    txbox.text_frame.word_wrap = True
    tf = txbox.text_frame
    tf.paragraphs[0].alignment = alignment
    tf.paragraphs[0].space_before = Pt(0)
    tf.paragraphs[0].space_after = Pt(0)
    tf.paragraphs[0].line_spacing = line_spacing
    run = tf.paragraphs[0].add_run()
    run.text = text
    run.font.size = font_size
    run.font.color.rgb = font_color
    run.font.bold = bold
    run.font.name = font_name
    rPr = run._r.get_or_add_rPr()
    rPr.set(qn('a:eaTypeface'), font_name)
    return txbox

def add_multiline_textbox(slide, left, top, width, height, lines, font_size=BODY_SIZE,
                          font_color=BODY_GRAY, font_name='Microsoft YaHei',
                          line_spacing=1.5, alignment=PP_ALIGN.LEFT):
    txbox = slide.shapes.add_textbox(left, top, width, height)
    txbox.text_frame.word_wrap = True
    tf = txbox.text_frame
    for i, line_data in enumerate(lines):
        if isinstance(line_data, str):
            text, is_bold, fs, clr = line_data, False, font_size, font_color
        elif len(line_data) == 2:
            text, is_bold = line_data
            fs, clr = font_size, font_color
        elif len(line_data) == 3:
            text, is_bold, fs = line_data
            clr = font_color
        else:
            text, is_bold, fs, clr = line_data
        if i == 0:
            p = tf.paragraphs[0]
        else:
            p = tf.add_paragraph()
        p.alignment = alignment
        p.space_before = Pt(2)
        p.space_after = Pt(2)
        p.line_spacing = line_spacing
        run = p.add_run()
        run.text = text
        run.font.size = fs
        run.font.color.rgb = clr
        run.font.bold = is_bold
        run.font.name = font_name
        rPr = run._r.get_or_add_rPr()
        rPr.set(qn('a:eaTypeface'), font_name)
    return txbox

def add_bullet_textbox(slide, left, top, width, height, bullets, font_size=BODY_SIZE,
                       font_color=BODY_GRAY, font_name='Microsoft YaHei',
                       bullet_char="-", line_spacing=1.5):
    txbox = slide.shapes.add_textbox(left, top, width, height)
    txbox.text_frame.word_wrap = True
    tf = txbox.text_frame
    for i, bullet_text in enumerate(bullets):
        if i == 0:
            p = tf.paragraphs[0]
        else:
            p = tf.add_paragraph()
        p.alignment = PP_ALIGN.LEFT
        p.space_before = Pt(3)
        p.space_after = Pt(3)
        p.line_spacing = line_spacing
        run_marker = p.add_run()
        run_marker.text = f"{bullet_char} "
        run_marker.font.size = font_size
        run_marker.font.color.rgb = ACCENT_BLUE
        run_marker.font.name = font_name
        rPr = run_marker._r.get_or_add_rPr()
        rPr.set(qn('a:eaTypeface'), font_name)
        run_text = p.add_run()
        run_text.text = bullet_text
        run_text.font.size = font_size
        run_text.font.color.rgb = font_color
        run_text.font.name = font_name
        rPr2 = run_text._r.get_or_add_rPr()
        rPr2.set(qn('a:eaTypeface'), font_name)
    return txbox

def add_top_bar(slide):
    add_rect(slide, Inches(0), Inches(0), SLIDE_W, Pt(3), fill_color=ACCENT_BLUE)

def add_slide_number(slide, num):
    add_textbox(slide, Inches(11.8), Inches(7.05), Inches(1.2), Inches(0.35),
                text=str(num), font_size=Pt(9), font_color=CAPTION,
                alignment=PP_ALIGN.RIGHT)

def add_source_label(slide, text, left=None, top=None):
    if left is None:
        left = Inches(0.6)
    if top is None:
        top = Inches(6.95)
    add_textbox(slide, left, top, Inches(6), Inches(0.35),
                text=text, font_size=CAPTION_SIZE, font_color=CAPTION)

def add_takeaway_bar(slide, text):
    add_rect(slide, Inches(0.6), Inches(6.55), Inches(12.1), Inches(0.38),
             fill_color=HIGHLIGHT_BG)
    add_textbox(slide, Inches(0.75), Inches(6.55), Inches(11.85), Inches(0.38),
                text=f">> {text}", font_size=TAKEAWAY_SIZE, font_color=ACCENT_BLUE,
                bold=False, anchor=MSO_ANCHOR.MIDDLE)

def add_slide_title(slide, title_text):
    add_top_bar(slide)
    add_textbox(slide, Inches(0.6), Inches(0.35), Inches(12.1), Inches(0.7),
                text=title_text, font_size=TITLE_SIZE, font_color=BLACK, bold=True)
    add_rect(slide, Inches(0.6), Inches(1.05), Inches(1.5), Pt(2), fill_color=ACCENT_BLUE)

def add_kpi_box(slide, left, top, width, height, value, label, color=ACCENT_BLUE):
    add_rect(slide, left, top, width, height, fill_color=HIGHLIGHT_BG)
    add_textbox(slide, left + Inches(0.1), top + Inches(0.08), width - Inches(0.2), Inches(0.4),
                text=value, font_size=Pt(22), font_color=color, bold=True,
                alignment=PP_ALIGN.CENTER)
    add_textbox(slide, left + Inches(0.1), top + Inches(0.5), width - Inches(0.2), Inches(0.35),
                text=label, font_size=Pt(10), font_color=CAPTION,
                alignment=PP_ALIGN.CENTER)


# ======================================================================
# SLIDE 1: TITLE
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_rect(slide, Inches(0), Inches(0), SLIDE_W, Inches(0.08), fill_color=ACCENT_BLUE)
add_rect(slide, Inches(0), Inches(0), Inches(0.08), SLIDE_H, fill_color=ACCENT_BLUE)

add_textbox(slide, Inches(1.2), Inches(1.6), Inches(10.5), Inches(1.4),
            text="AFEM：基于 GNN + PPO 强化学习\n的自适应网格细化方法",
            font_size=Pt(38), font_color=BLACK, bold=True, line_spacing=1.3)

add_textbox(slide, Inches(1.2), Inches(3.2), Inches(10.5), Inches(0.9),
            text="二维 Helmholtz 电磁散射问题的智能网格优化 -- 算法流程与创新汇总",
            font_size=Pt(18), font_color=BODY_GRAY)

add_rect(slide, Inches(1.2), Inches(4.2), Inches(3.0), Pt(2), fill_color=ACCENT_BLUE)

meta_lines = [
    ("组会汇报  |  2025 年 5 月", False, Pt(14), CAPTION),
    ("", False, Pt(8), CAPTION),
    ("物理场景：二维 Helmholtz 方程 / 圆形介质散射体 / SBC 吸收边界", False, Pt(12), CAPTION),
    ("方法栈：GNN (Message Passing) / PPO / 连续尺寸场 / 残差型误差估计", False, Pt(12), CAPTION),
]
add_multiline_textbox(slide, Inches(1.2), Inches(4.5), Inches(10.5), Inches(1.6),
                      meta_lines, line_spacing=1.5)
add_slide_number(slide, 1)


# ======================================================================
# SLIDE 2: BACKGROUND
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "研究背景：为什么自适应网格细化很重要")

left_bullets = [
    "Helmholtz 方程描述电磁波在介质中的散射与传播，是电磁兼容、隐身设计、天线仿真等领域的基础方程",
    "有限元 (FEM) 求解精度高度依赖网格质量：网格过粗导致数值色散/污染效应；网格过密浪费计算资源",
    "高频 (k >> 1) 下污染效应严重：kh > 0.5 时 FEM 解定性错误，后续误差指示子完全不可靠",
    "核心挑战：如何用最少的网格单元达到目标精度？在误差大的区域加密，误差小的区域保持稀疏",
]
add_bullet_textbox(slide, Inches(0.6), Inches(1.35), Inches(6.0), Inches(3.2),
                   left_bullets, font_size=SMALL_SIZE)

add_rect(slide, Inches(7.2), Inches(1.35), Inches(5.5), Inches(3.2), fill_color=HIGHLIGHT_BG)
physics_lines = [
    ("物理方程", True, Pt(14), ACCENT_BLUE),
    ("", False, Pt(4), BODY_GRAY),
    ("nabla^2 u_scat + k^2 * eps_r * u_scat = k^2 * (1-eps_r) * u_inc", True, Pt(13), ACCENT_TEAL),
    ("", False, Pt(4), BODY_GRAY),
    ("入射波：沿 -x 方向的平面波  u_inc = exp(i*k*x)", False, Pt(11), BODY_GRAY),
    ("散射体：圆形介质柱（eps_r 随机采样）", False, Pt(11), BODY_GRAY),
    ("边界条件：SBC 吸收边界  du/dn = i*k*u", False, Pt(11), BODY_GRAY),
    ("计算域：可配矩形域 [Lx, Ly]", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(7.4), Inches(1.5), Inches(5.1), Inches(2.8),
                      physics_lines, line_spacing=1.3)

kpis = [
    ("kh > 1.4", "高频下典型 kh 值\n(远超 0.5 安全线)", ACCENT_WARM),
    ("400 -> 20,000", "网格单元数变化范围\n(初始 -> 最大上限)", ACCENT_BLUE),
    ("[2, 20]", "训练波数 k 覆盖范围\n(涵盖中频到高频)", ACCENT_TEAL),
]
for i, (val, label, clr) in enumerate(kpis):
    add_kpi_box(slide, Inches(0.6 + i * 4.2), Inches(4.95), Inches(3.8), Inches(1.1),
                val, label, color=clr)

add_takeaway_bar(slide, "Helmholtz 高频求解的核心矛盾：精度 vs 效率。需要智能网格细化策略来平衡二者。")
add_source_label(slide, "参考文献：Ainsworth & Oden, A Posteriori Error Estimation in Finite Element Analysis, 2000")
add_slide_number(slide, 2)


# ======================================================================
# SLIDE 3: KNOWLEDGE GAP
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "知识缺口与技术瓶颈")

add_rect(slide, Inches(0.6), Inches(1.35), Inches(5.7), Inches(2.5), fill_color=None,
         line_color=LIGHT_LINE, line_width=Pt(1))
add_textbox(slide, Inches(0.8), Inches(1.4), Inches(5.3), Inches(0.4),
            text="传统自适应方法的局限", font_size=SUBHEAD_SIZE, font_color=ACCENT_WARM, bold=True)
trad_bullets = [
    "基于误差指示子的 h-adaptivity 细化规则完全由人工设计",
    "细化判据固定（如设定误差阈值），无法适应不同 PDE 的物理特征",
    "SOLVE-ESTIMATE-MARK-REFINE 循环不考虑长期回报（每一步仅看当前误差）",
    "无法学习特定问题的网格模式，无法迁移到新 PDE 配置",
]
add_bullet_textbox(slide, Inches(0.8), Inches(1.85), Inches(5.3), Inches(1.8),
                   trad_bullets, font_size=Pt(11))

add_rect(slide, Inches(7.0), Inches(1.35), Inches(5.7), Inches(2.5), fill_color=None,
         line_color=ACCENT_BLUE, line_width=Pt(1.5))
add_textbox(slide, Inches(7.2), Inches(1.4), Inches(5.3), Inches(0.4),
            text="本工作的目标", font_size=SUBHEAD_SIZE, font_color=ACCENT_BLUE, bold=True)
goal_bullets = [
    "用强化学习 (RL) 替代人工规则，自动发现最优细化策略",
    "GNN 处理变长拓扑：每个三角形单元是一个独立的 RL agent",
    "连续尺寸场输出 -> 概率性元素选择 -> 非均匀自适应网格",
    "物理预算约束 + 误差驱动奖励 -> 计算资源集中在物理关键区域",
]
add_bullet_textbox(slide, Inches(7.2), Inches(1.85), Inches(5.3), Inches(1.8),
                   goal_bullets, font_size=Pt(11), bullet_char=">")

add_textbox(slide, Inches(0.6), Inches(4.2), Inches(12.1), Inches(0.4),
            text="本次汇报的核心创新（相较前序工作）", font_size=SUBHEAD_SIZE,
            font_color=BLACK, bold=True)

innovations = [
    ("[1] 无量纲化残差误差估计", "k_local 归一化三项残差分量，消除纯几何尺度偏差，跨介质公平可比", ACCENT_BLUE),
    ("[2] Score-based 连续尺寸场", "score = -x_i 纯排序 + 物理预算约束 + Doerfler-P95 动作掩码", ACCENT_TEAL),
    ("[3] L2 聚合奖励设计", "sqrt(sum eta_child^2) <= eta_parent 保证 r_local >= 0，永不惩罚细化", ACCENT_GREEN),
    ("[4] 尺度不变性架构", "N_init x domain_area + lambda 无量纲化特征 + ln 压缩 + 前渐近区约束", ACCENT_WARM),
]
for i, (title, desc, clr) in enumerate(innovations):
    y = Inches(4.7 + i * 0.6)
    add_rect(slide, Inches(0.6), y, Pt(3), Inches(0.45), fill_color=clr)
    add_textbox(slide, Inches(0.85), y - Inches(0.02), Inches(3.0), Inches(0.45),
                text=title, font_size=Pt(13), font_color=clr, bold=True)
    add_textbox(slide, Inches(3.8), y - Inches(0.02), Inches(8.7), Inches(0.45),
                text=desc, font_size=Pt(11), font_color=BODY_GRAY)

add_takeaway_bar(slide, "核心思路：让网格细化的每一步都具有明确的物理语义，而非纯数据驱动的黑箱映射")
add_slide_number(slide, 3)


# ======================================================================
# SLIDE 4: SYSTEM OVERVIEW
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "系统架构：RL 自适应网格细化闭环管线")

stages = [
    ("物理问题\n采样", ACCENT_BLUE),
    ("初始网格\n生成", ACCENT_BLUE),
    ("GNN\n观测", ACCENT_TEAL),
    ("Actor\n动作", ACCENT_TEAL),
    ("尺寸场\n排序", ACCENT_WARM),
    ("预算\n选择", ACCENT_WARM),
    ("网格\n细化", ACCENT_GREEN),
    ("FEM\n求解", ACCENT_GREEN),
    ("误差\n估计", ACCENT_GREEN),
    ("Reward\n计算", ACCENT_GREEN),
]

y_center = Inches(2.6)
box_w = Inches(1.1)
box_h = Inches(0.85)
gap = (Inches(12.1) - box_w * 10) / 9

for i, (label, clr) in enumerate(stages):
    x = Inches(0.6) + i * (box_w + gap)
    add_rect(slide, x, y_center, box_w, box_h, fill_color=HIGHLIGHT_BG,
             line_color=clr, line_width=Pt(1.5))
    add_textbox(slide, x, y_center + Inches(0.05), box_w, box_h - Inches(0.1),
                text=label, font_size=Pt(11), font_color=clr, bold=True,
                alignment=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    if i < len(stages) - 1:
        arrow_x = x + box_w
        add_textbox(slide, arrow_x, y_center + Inches(0.22), gap, Inches(0.35),
                    text=">", font_size=Pt(16), font_color=LIGHT_LINE, bold=True,
                    alignment=PP_ALIGN.CENTER)

add_textbox(slide, Inches(6.0), Inches(3.55), Inches(1.5), Inches(0.35),
            text="<-- 下一轮迭代（多步 rollout）", font_size=Pt(10), font_color=ACCENT_TEAL,
            alignment=PP_ALIGN.CENTER)

# RL modeling
add_textbox(slide, Inches(0.6), Inches(4.1), Inches(6.0), Inches(0.35),
            text="RL 问题建模", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)
rl_lines = [
    ("Agent = 每个三角形单元（数量动态变化，约 400 -> 20,000）", False, Pt(11), BODY_GRAY),
    ("State = GNN 节点 12 维特征（几何 + PDE 残差 + 场量 + 物理参数）", False, Pt(11), BODY_GRAY),
    ("Action = 1 维连续标量 x_i -> score = -x_i 排序 -> top-k 选择细化单元", False, Pt(11), BODY_GRAY),
    ("Reward = L2 聚合局部改善 + 全局势函数塑形 - 动作惩罚", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(0.6), Inches(4.5), Inches(6.0), Inches(2.0),
                      rl_lines, line_spacing=1.6)

# PPO training
add_textbox(slide, Inches(7.2), Inches(4.1), Inches(5.5), Inches(0.35),
            text="PPO 训练配置", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)
train_lines = [
    ("双 GNN 架构：Policy / Value 各自独立 MessagePassingBase", False, Pt(11), BODY_GRAY),
    ("2 层消息传递，inner 残差 + LayerNorm，latent_dim=64", False, Pt(11), BODY_GRAY),
    ("DiagGaussian 连续动作分布，log_std 可学习，clamp [-4, -1]", False, Pt(11), BODY_GRAY),
    ("256 步 Rollout，5 Epochs，GAE lambda=0.95，lr=3e-4，梯度裁剪 0.5", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(7.2), Inches(4.5), Inches(5.5), Inches(2.0),
                      train_lines, line_spacing=1.6)

add_takeaway_bar(slide, "闭环 RL 管线：物理求解 -> GNN 感知 -> 策略决策 -> 网格操作 -> 误差反馈 -> 策略更新")
add_slide_number(slide, 4)


# ======================================================================
# SLIDE 5: INNOVATION 1 - Non-dimensionalized residual error
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新 [1]：无量纲化残差误差估计 -- 消除几何尺度偏差")

add_textbox(slide, Inches(0.6), Inches(1.25), Inches(5.8), Inches(0.35),
            text="前序问题：原始残差包含 h_K、h_e 等几何尺度，不同区域不可直接比较", font_size=Pt(13), font_color=ACCENT_WARM)
add_textbox(slide, Inches(0.6), Inches(1.55), Inches(5.8), Inches(0.35),
            text="解决方案：引入局部波数 k_local 做无量纲归一化，反映相位分辨率残差", font_size=Pt(13), font_color=ACCENT_BLUE)

formulas = [
    ("内部残差 r_int",
     "(h_K/k_local) * sqrt(V) * |k^2*eps_r*u + k^2*(eps_r-1)*u_inc|_K",
     "单元内部 PDE 残差；除以 k_local 使大 eps_r 介质区与真空区可比"),
    ("梯度跳变 r_jump",
     "sqrt(1/2 * sum_{e in dK} (h_e/k_local) * |[[grad u * n]]|^2_e)",
     "相邻单元梯度跳变；h_e/k_local 使细化后跳变自然衰减"),
    ("SBC 边界 r_sbc",
     "(h_bnd/k_local) * |du/dn - i*k_local*u|",
     "Sommerfeld 吸收边界残差，仅在边界单元非零"),
]

for i, (name, formula, desc) in enumerate(formulas):
    x = Inches(0.6 + i * 4.1)
    add_rect(slide, x, Inches(2.0), Inches(3.85), Inches(1.65), fill_color=HIGHLIGHT_BG)
    add_textbox(slide, x + Inches(0.15), Inches(2.05), Inches(3.55), Inches(0.3),
                text=name, font_size=Pt(13), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, x + Inches(0.15), Inches(2.35), Inches(3.55), Inches(0.65),
                text=formula, font_size=Pt(11), font_color=BLACK)
    add_textbox(slide, x + Inches(0.15), Inches(3.05), Inches(3.55), Inches(0.5),
                text=desc, font_size=Pt(10), font_color=CAPTION)

add_rect(slide, Inches(0.6), Inches(3.95), Inches(12.1), Inches(0.7), fill_color=None,
         line_color=ACCENT_BLUE, line_width=Pt(1.5))
add_textbox(slide, Inches(0.8), Inches(4.0), Inches(3.5), Inches(0.55),
            text="逐单元误差指示子", font_size=Pt(15), font_color=BLACK, bold=True,
            anchor=MSO_ANCHOR.MIDDLE)
add_textbox(slide, Inches(4.0), Inches(4.0), Inches(3.5), Inches(0.55),
            text="eta_K = sqrt(r_int^2 + r_jump^2 + r_sbc^2)", font_size=Pt(15),
            font_color=ACCENT_BLUE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
add_textbox(slide, Inches(7.5), Inches(4.0), Inches(5.0), Inches(0.55),
            text="三项均严格无量纲\n跨介质、跨频率公平可比", font_size=Pt(13),
            font_color=ACCENT_GREEN, anchor=MSO_ANCHOR.MIDDLE)

add_textbox(slide, Inches(0.6), Inches(4.85), Inches(12.1), Inches(0.3),
            text="量纲分析验证", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)
da_lines = [
    ("k_local ~ [L]^-1, h_e ~ [L], |jump|^2 ~ [L]^-2  =>  h_e/k_local * |jump|^2 ~ [L]^2 * [L]^-2 = 1  严格无量纲", False, Pt(11), BODY_GRAY),
    ("GNN 输入用 log10 压缩的特征；Reward 用原始 eta_K（不经 log 压缩），两者公式一致，物理语义对齐", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(0.6), Inches(5.15), Inches(12.1), Inches(0.8),
                      da_lines, line_spacing=1.5)

add_takeaway_bar(slide, "k_local 归一化使误差指示子反映相位分辨率残差而非网格粗疏程度，为 RL agent 提供物理一致的误差信号")
add_slide_number(slide, 5)


# ======================================================================
# SLIDE 6: INNOVATION 2 - 12D enhanced input features
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新 [2]：12 维增强输入特征 -- 赋予 GNN 几何与物理感知")

add_textbox(slide, Inches(0.6), Inches(1.25), Inches(12.1), Inches(0.35),
            text="前序 11 维 -> 现 12 维，新增 dist_to_interface。全部尺度相关特征均以真空波长 lambda=2*pi/k 无量纲化", font_size=Pt(13), font_color=ACCENT_BLUE)

# Feature table — compact layout to avoid overflow
row_h = Inches(0.30)
table_top = Inches(1.65)
cols = [Inches(0.6), Inches(2.0), Inches(5.5), Inches(9.8)]
col_w = [Inches(1.4), Inches(3.5), Inches(4.3), Inches(3.1)]
headers = ["维度", "特征名称", "物理含义", "归一化"]

for j, (cx, hdr, w) in enumerate(zip(cols, headers, col_w)):
    add_rect(slide, cx, table_top, w, row_h, fill_color=TABLE_HDR)
    add_textbox(slide, cx + Inches(0.06), table_top, w - Inches(0.12), row_h,
                text=hdr, font_size=Pt(9), font_color=BLACK, bold=True,
                anchor=MSO_ANCHOR.MIDDLE)

features = [
    ("volume", "无量纲单元面积", "volume / lambda^2"),
    ("internal_residual", "内部残差（k_local 无量纲化 + log10）", "--"),
    ("gradient_jump", "梯度跳变残差（k_local 无量纲化 + log10）", "--"),
    ("sbc_residual", "SBC 边界残差（k_local 无量纲化 + log10）", "--"),
    ("element_penalty", "单元惩罚系数 lambda", "--"),
    ("timestep", "当前 rollout 步数", "--"),
    ("wave_number", "Helmholtz 波数 k", "--"),
    ("k_local_sqrt_vol", "k_local x sqrt(volume) 已无量纲", "--"),
    ("is_sbc_boundary", "是否与 SBC 边界相邻 (0/1)", "--"),
    ("dist_to_interface", "到介质边界的带符号距离 [新增]", "sign(d)*ln(1+|d|/lambda)"),
    ("epsilon_r", "单元中点介电常数（内=eps_r, 外=1.0）", "--"),
    ("total_solution_magnitude", "散射场复数解的振幅", "--"),
]

for i, (name, meaning, norm) in enumerate(features):
    y = table_top + row_h + i * row_h
    bg = TABLE_ALT if i % 2 == 1 else WHITE
    is_new = "[新增]" in meaning
    cells = [name, meaning, norm]
    for j, (cx, cell_text, w) in enumerate(zip(cols, cells, col_w)):
        add_rect(slide, cx, y, w, row_h, fill_color=bg, line_color=LIGHTER_LINE, line_width=Pt(0.5))
        clr = ACCENT_TEAL if is_new and j == 1 else BODY_GRAY
        bld = is_new and j == 1
        add_textbox(slide, cx + Inches(0.06), y, w - Inches(0.12), row_h,
                    text=cell_text, font_size=Pt(8), font_color=clr, bold=bld,
                    anchor=MSO_ANCHOR.MIDDLE)

# Edge feature note — positioned after table (table bottom = 1.65 + 0.30 + 12*0.30 = 5.55")
add_textbox(slide, Inches(0.6), Inches(5.65), Inches(12.1), Inches(0.25),
            text="边特征 (1 维)：euclidean_distance / lambda -- 相邻单元中点无量纲距离  |  合计：12 (节点) + 1 (边) = 13 维图特征",
            font_size=Pt(9), font_color=BODY_GRAY)

add_takeaway_bar(slide, "全部与尺度相关的特征均以 lambda 做无量纲归一化；dist_to_interface 用 sign·ln(1+|d|) 对数压缩，近场线性、远场自然压缩，与残差 log10 风格统一")
add_slide_number(slide, 6)


# ======================================================================
# SLIDE 7: INNOVATION 3 - Score-based sizing field
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新 [3]：Score-based 连续尺寸场 + 物理预算约束 + 动作掩码")

add_textbox(slide, Inches(0.6), Inches(1.25), Inches(5.7), Inches(0.35),
            text="前序方案：S_i = N_base x Softplus(x_i) / Softplus(0) x median_area", font_size=Pt(12), font_color=ACCENT_WARM)
add_textbox(slide, Inches(0.6), Inches(1.55), Inches(5.7), Inches(0.35),
            text="--> 依赖 median_area 基准，域缩放后语义漂移 (1x1 -> 2x2 基准 x4)", font_size=Pt(10), font_color=CAPTION)
add_textbox(slide, Inches(6.9), Inches(1.25), Inches(5.6), Inches(0.35),
            text="当前方案：score = -x_i 纯排序 + 物理预算约束", font_size=Pt(12), font_color=ACCENT_BLUE)
add_textbox(slide, Inches(6.9), Inches(1.55), Inches(5.6), Inches(0.35),
            text="--> score 排序丢失面积语义，但获得尺度不变性", font_size=Pt(10), font_color=CAPTION)

add_rect(slide, Inches(0.6), Inches(2.1), Inches(12.1), Inches(3.1), fill_color=HIGHLIGHT_BG)
add_textbox(slide, Inches(0.8), Inches(2.15), Inches(5.0), Inches(0.3),
            text="细化选择算法", font_size=Pt(14), font_color=BLACK, bold=True)

algo_steps = [
    ("Step 1: 物理预算",
     "A_budget_i = 1/2 x (lambda_local_i / 6)^2    仅用于 N_budget 计算\nN_budget = max(N_phys, ceil(5 x N_init))    rho_min=5.0，至少 5 倍初始单元数"),
    ("Step 2: Score 排序",
     "score = -x_i  (Actor 输出标量)\nx 越小 -> 优先级越高，纯排序，不设正负门槛"),
    ("Step 3: 双过滤器",
     "eligible = {i | area_i > 0.25 x A_budget_i  AND  eta_i >= 0.05 x eta_P95}\narea_floor: 排除已足够细的单元\nDoerfler-P95: 排除低误差单元 (P95 锚定物理误差尺度)"),
    ("Step 4: Top-k 选择",
     "num = min(|eligible|, N_current//4, remaining//3)    (自适应 cap, 增速 N//4)\nselected = top-k by score -> 1-to-4 切分细化"),
]

for i, (title, content) in enumerate(algo_steps):
    y = Inches(2.55 + i * 0.63)
    add_textbox(slide, Inches(0.9), y, Inches(2.0), Inches(0.55),
                text=title, font_size=Pt(11), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, Inches(2.9), y, Inches(9.5), Inches(0.55),
                text=content, font_size=Pt(10), font_color=BODY_GRAY)

add_rect(slide, Inches(0.6), Inches(5.45), Inches(12.1), Inches(0.95), fill_color=None,
         line_color=ACCENT_BLUE, line_width=Pt(0.5))
add_textbox(slide, Inches(0.8), Inches(5.5), Inches(11.7), Inches(0.85),
            text="为什么用 Doerfler-P95 而非 median/mean？P95 锚定物理误差尺度，免疫远场噪声稀释。远场低 eta 区即使占 90% 的单元，也不会拉低锚点。确保只有误差真正达标的区域才消耗细化预算。",
            font_size=Pt(11), font_color=BODY_GRAY)

add_takeaway_bar(slide, "Score-based 排序 + 物理预算 + Doerfler-P95 掩码：三层保障确保细化资源只投入到物理上需要的地方")
add_slide_number(slide, 7)


# ======================================================================
# SLIDE 8: INNOVATION 4 - L2 aggregation reward
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新 [4]：L2 聚合奖励设计 -- 保证非负，永不惩罚细化")

add_rect(slide, Inches(0.6), Inches(1.25), Inches(12.1), Inches(0.85), fill_color=HIGHLIGHT_BG)
add_textbox(slide, Inches(0.8), Inches(1.3), Inches(11.7), Inches(0.75),
            text="核心洞察：对 1-to-4 切分，用 L2 聚合 sqrt(sum eta_child^2) <= eta_parent 天然成立 -- 因为平方后 int 项 1->1/4 而 jump/sbc 项 1->1。\n如果用 L1 sum，sum eta_child > eta_parent（因 jump/sbc 项不变），会导致「细化=惩罚」。L2 聚合从根本上避免了这一结构性负偏置。",
            font_size=Pt(12), font_color=BLACK)

add_rect(slide, Inches(0.6), Inches(2.35), Inches(7.5), Inches(1.85), fill_color=None,
         line_color=ACCENT_BLUE, line_width=Pt(1.5))
add_textbox(slide, Inches(0.8), Inches(2.4), Inches(7.1), Inches(0.3),
            text="逐步奖励计算", font_size=Pt(14), font_color=BLACK, bold=True)
reward_lines = [
    ("r_local_i = log(eta_old_i + eps) - log( sqrt(sum_{j:M[j]=i} eta_new_j^2) + eps )", True, Pt(13), ACCENT_BLUE),
    ("", False, Pt(4), BODY_GRAY),
    ("- 纯 int 主导区: eta_parent^2 = int^2, sum eta_child^2 = int^2/4 -> r_local = log(2) = +0.69 (强正奖励)", False, Pt(11), BODY_GRAY),
    ("- 纯 jump/sbc 主导区: eta_parent^2 = jump^2, sum eta_child^2 = jump^2 -> r_local = 0 (中性)", False, Pt(11), BODY_GRAY),
    ("- 永不惩罚细化 -- 与 L1 sum 方案根本不同", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(0.8), Inches(2.7), Inches(7.1), Inches(1.4),
                      reward_lines, line_spacing=1.35)

add_rect(slide, Inches(8.5), Inches(2.35), Inches(4.2), Inches(1.85), fill_color=WARN_BG)
add_textbox(slide, Inches(8.7), Inches(2.4), Inches(3.8), Inches(0.3),
            text="epsilon_dynamic 动态截断", font_size=Pt(14), font_color=BLACK, bold=True)
ed_lines = [
    ("eps = max(0.05 x mean(eta_new), 1e-6)", True, Pt(11), ACCENT_WARM),
    ("", False, Pt(4), BODY_GRAY),
    ("自适应钳制，切断远场", False, Pt(11), BODY_GRAY),
    ("低 eta 区的 reward hacking", False, Pt(11), BODY_GRAY),
    ("", False, Pt(4), BODY_GRAY),
    ("防止 log(0) 数值爆炸，", False, Pt(11), BODY_GRAY),
    ("锚定当前误差分布而非", False, Pt(11), BODY_GRAY),
    ("固定阈值", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(8.7), Inches(2.7), Inches(3.8), Inches(1.4),
                      ed_lines, line_spacing=1.2)

add_textbox(slide, Inches(0.6), Inches(4.45), Inches(6.0), Inches(0.3),
            text="动作惩罚与元素上限", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)
pen_lines = [
    ("penalty_i = lambda x (n_i-1) + (lambda_limit/N_old) x 1[达到上限],  lambda=0.06, lambda_limit=10000", False, Pt(12), BODY_GRAY),
    ("lambda 仅为 r_local 均值的约 1/6，轻微抑制网格膨胀，不影响主要学习信号", False, Pt(11), CAPTION),
]
add_multiline_textbox(slide, Inches(0.6), Inches(4.8), Inches(6.0), Inches(0.7),
                      pen_lines, line_spacing=1.5)

add_textbox(slide, Inches(7.2), Inches(4.45), Inches(5.5), Inches(0.3),
            text="全局势函数塑形", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)
glob_lines = [
    ("E_global = sqrt(sum eta_K^2) / ||u_h||_{L2(Omega)}    (无量纲全局误差)", False, Pt(12), BODY_GRAY),
    ("global_bonus = alpha x [log(E_old) - log(E_new)],  alpha = 0.2", False, Pt(12), BODY_GRAY),
    ("仅发给被细化的父单元 -- 避免被未细化单元稀释信号", False, Pt(11), CAPTION),
]
add_multiline_textbox(slide, Inches(7.2), Inches(4.8), Inches(5.5), Inches(0.7),
                      glob_lines, line_spacing=1.5)

add_takeaway_bar(slide, "奖励公式 = L2 聚合局部改善 (>=0) + 全局势函数塑形 (仅细化单元) - 轻微动作惩罚 -> 每个被细化父单元净奖励约 +0.387")
add_slide_number(slide, 8)


# ======================================================================
# SLIDE 9: REWARD CALIBRATION
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "奖励标度校准：随机策略下各分量量级实测")

add_textbox(slide, Inches(0.6), Inches(1.25), Inches(12.1), Inches(0.35),
            text="随机策略下 1,321 个 refined-parent 样本实测（score-based 尺寸场）", font_size=Pt(12), font_color=CAPTION)

kpi_data = [
    ("+0.364", "r_local (L2 聚合)", "局部误差改善，主体信号"),
    ("+0.045", "penalty (lambda=0.02)", "仅占 r_local 的约 1/8"),
    ("+0.069", "alpha x Delta_logE (alpha=0.2)", "全局改善信号，约 r_local/5"),
    ("+0.387", "净奖励 net reward", "r_local >> penalty [check]"),
]

for i, (val, label, desc) in enumerate(kpi_data):
    x = Inches(0.6 + i * 3.1)
    add_rect(slide, x, Inches(1.7), Inches(2.85), Inches(1.2), fill_color=HIGHLIGHT_BG)
    add_textbox(slide, x + Inches(0.1), Inches(1.75), Inches(2.65), Inches(0.4),
                text=val, font_size=Pt(24), font_color=ACCENT_BLUE, bold=True,
                alignment=PP_ALIGN.CENTER)
    add_textbox(slide, x + Inches(0.1), Inches(2.15), Inches(2.65), Inches(0.3),
                text=label, font_size=Pt(11), font_color=BLACK, bold=True,
                alignment=PP_ALIGN.CENTER)
    add_textbox(slide, x + Inches(0.1), Inches(2.45), Inches(2.65), Inches(0.35),
                text=desc, font_size=Pt(9), font_color=CAPTION,
                alignment=PP_ALIGN.CENTER)

add_textbox(slide, Inches(0.6), Inches(3.2), Inches(12.1), Inches(0.3),
            text="设计验证", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

design_checks = [
    ("[OK] r_local >> penalty", "局部 credit assignment 不被惩罚信号淹没，agent 能清晰感知细化 -> 误差下降的因果关系"),
    ("[OK] alpha x Delta_logE = r_local / 5", "全局信号提供趋势引导但不主导局部决策，避免 loss of local credit assignment"),
    ("[OK] r_local >= 0 保证", "L2 聚合天然保证非负，网络永远不会因细化而受到惩罚"),
]
for i, (check, detail) in enumerate(design_checks):
    add_textbox(slide, Inches(0.8), Inches(3.5 + i * 0.45), Inches(2.8), Inches(0.35),
                text=check, font_size=Pt(12), font_color=ACCENT_GREEN, bold=True)
    add_textbox(slide, Inches(3.6), Inches(3.5 + i * 0.45), Inches(9.1), Inches(0.35),
                text=detail, font_size=Pt(11), font_color=BODY_GRAY)

add_textbox(slide, Inches(0.6), Inches(5.1), Inches(12.1), Inches(0.3),
            text="奖励信号链", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

flow_steps = [
    ("FEM 求解", "eta_K per element", ACCENT_BLUE),
    ("L2 聚合", "log(eta_old / sqrt(sum_chi^2))", ACCENT_TEAL),
    ("+ eps_dynamic", "截断保护", ACCENT_WARM),
    ("- penalty", "lambda x (n-1) 防膨胀", ACCENT_WARM),
    ("+ global", "alpha x Delta_logE 仅细化单元", ACCENT_GREEN),
    ("-> r_i", "送入 PPO GAE", ACCENT_BLUE),
]

for i, (step_name, step_desc, clr) in enumerate(flow_steps):
    x = Inches(0.6 + i * 2.05)
    add_rect(slide, x, Inches(5.45), Inches(1.8), Inches(0.7), fill_color=HIGHLIGHT_BG,
             line_color=clr, line_width=Pt(1))
    add_textbox(slide, x + Inches(0.1), Inches(5.48), Inches(1.6), Inches(0.3),
                text=step_name, font_size=Pt(11), font_color=clr, bold=True,
                alignment=PP_ALIGN.CENTER)
    add_textbox(slide, x + Inches(0.1), Inches(5.78), Inches(1.6), Inches(0.3),
                text=step_desc, font_size=Pt(9), font_color=CAPTION,
                alignment=PP_ALIGN.CENTER)
    if i < len(flow_steps) - 1:
        add_textbox(slide, x + Inches(1.8), Inches(5.6), Inches(0.25), Inches(0.3),
                    text=">", font_size=Pt(14), font_color=LIGHT_LINE, bold=True,
                    alignment=PP_ALIGN.CENTER)

add_takeaway_bar(slide, "奖励各分量量级经过标定，满足 r_local >> penalty 且 alpha x Delta_logE 适度，agent 能学到细化 = 有益的信息")
add_slide_number(slide, 9)


# ======================================================================
# SLIDE 10: SCALE INVARIANCE
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新 [5]：尺度不变性架构 -- 从 1x1 到 2x2 的泛化")

add_textbox(slide, Inches(0.6), Inches(1.25), Inches(12.1), Inches(0.4),
            text="问题：1x1 域训练 -> 2x2 域测试时，中心介质处网格未加密，远场误差显著增大", font_size=Pt(14), font_color=ACCENT_WARM)

add_textbox(slide, Inches(0.6), Inches(1.75), Inches(12.1), Inches(0.3),
            text="根因分析（双重漂移）", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

roots = [
    ("N_init 不随 domain area 缩放", "4x 面积用同数量单元 -> h 2x, area 4x", "N_init *= domain_area"),
    ("特征绝对值漂移", "volume/edge/dist 值随 domain 线性或平方放大", "全部用 lambda 无量纲化"),
    ("dist 远场 OOD", "2x2 域远角 dist/lambda 可达训练域 3x", "sign·ln(1+|d|/lambda) 对数压缩"),
]

for i, (problem, cause, fix) in enumerate(roots):
    x = Inches(0.6 + i * 4.1)
    add_rect(slide, x, Inches(2.1), Inches(3.85), Inches(1.3), fill_color=HIGHLIGHT_BG)
    add_textbox(slide, x + Inches(0.1), Inches(2.13), Inches(3.65), Inches(0.3),
                text=problem, font_size=Pt(13), font_color=ACCENT_WARM, bold=True)
    add_textbox(slide, x + Inches(0.1), Inches(2.45), Inches(3.65), Inches(0.4),
                text=f"原因: {cause}", font_size=Pt(10), font_color=BODY_GRAY)
    add_textbox(slide, x + Inches(0.1), Inches(2.85), Inches(3.65), Inches(0.4),
                text=f"--> {fix}", font_size=Pt(11), font_color=ACCENT_GREEN)

add_rect(slide, Inches(0.6), Inches(3.65), Inches(7.5), Inches(2.05), fill_color=None,
         line_color=ACCENT_BLUE, line_width=Pt(1))
add_textbox(slide, Inches(0.8), Inches(3.7), Inches(7.1), Inches(0.3),
            text="四项联动改进 = 完整的尺度不变性", font_size=Pt(14), font_color=BLACK, bold=True)
k_mesh_lines = [
    ("1. N_init = N_base x (k/k_ref)^k_exponent x domain_area   （exponent/k_ref 可配，保证每单位面积密度一致）", False, Pt(12), BODY_GRAY),
    ("2. volume -> volume / lambda^2,  euclidean_distance -> euclidean_distance / lambda", False, Pt(12), BODY_GRAY),
    ("3. dist_to_interface -> sign(d)*ln(1+|d|/lambda)    （近场线性、远场对数压缩，与 log10 残差风格一致）", False, Pt(12), BODY_GRAY),
    ("4. 介质区前渐近区边缘约束: 强制迭代细化至 h <= lambda_d/N (N=1.5)", False, Pt(12), BODY_GRAY),
    ("--> 四项联动：N_init 修 h 漂移 + lambda 归一化修特征绝对值 + tanh 修远场 OOD", False, Pt(11), CAPTION),
]
add_multiline_textbox(slide, Inches(0.8), Inches(4.0), Inches(7.1), Inches(1.5),
                      k_mesh_lines, line_spacing=1.3)

add_textbox(slide, Inches(8.5), Inches(3.7), Inches(4.2), Inches(0.3),
            text="N_init 缩放效果示例", font_size=Pt(13), font_color=BLACK, bold=True)
k_table_lines = [
    ("exponent 可配: ^2 = 理论最优, ^1.5 = 工程折中", False, Pt(10), BODY_GRAY),
    ("N_init 始终 = COMSOL 目标的 30-50%", False, Pt(10), BODY_GRAY),
    ("", False, Pt(4), BODY_GRAY),
    ("改前: 无 domain_area 缩放", True, Pt(10), ACCENT_WARM),
    ("-> 换 domain size 后 N_init 不变", False, Pt(10), CAPTION),
    ("-> h 随 domain 缩放，特征 OOD", False, Pt(10), CAPTION),
]
add_multiline_textbox(slide, Inches(8.5), Inches(4.0), Inches(4.2), Inches(1.7),
                      k_table_lines, line_spacing=1.3)

add_takeaway_bar(slide, "N_init x domain_area + lambda 无量纲化 + ln 对数压缩：三项联动使模型可物理一致地泛化到任意尺寸测试域")
add_slide_number(slide, 10)


# ======================================================================
# SLIDE 11: DUAL GNN ARCHITECTURE
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "双 GNN 架构与 PPO 训练细节")

add_textbox(slide, Inches(0.6), Inches(1.3), Inches(12.1), Inches(0.35),
            text="图观测 -> MessagePassingBase (Policy/Value 各自独立) -> Actor/Critic 头", font_size=Pt(13), font_color=BLACK, bold=True)

add_rect(slide, Inches(0.6), Inches(1.8), Inches(5.8), Inches(3.0), fill_color=HIGHLIGHT_BG)
add_textbox(slide, Inches(0.8), Inches(1.85), Inches(5.4), Inches(0.3),
            text="MessagePassingBase (x2, Policy / Value 各自独立基座)", font_size=Pt(13), font_color=ACCENT_BLUE, bold=True)

gnn_items = [
    ("节点嵌入", "Linear(12 -> 64)"),
    ("边嵌入", "Linear(1 -> 64)"),
    ("MP Step 1", "EdgeModule: MLP([src|dst|edge_attr]) -> 64d"),
    ("", "NodeModule: MLP([node|scatter_mean(入边)]) -> 64d"),
    ("", "+ inner 残差 + LayerNorm"),
    ("MP Step 2", "同 Step 1，堆叠 2 层"),
    ("输出", "节点隐向量 (num_nodes, 64)"),
]

for i, (label, detail) in enumerate(gnn_items):
    y = Inches(2.25 + i * 0.32)
    if label:
        add_textbox(slide, Inches(0.9), y, Inches(1.6), Inches(0.28),
                    text=label, font_size=Pt(10), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, Inches(2.5), y, Inches(3.7), Inches(0.28),
                text=detail, font_size=Pt(10), font_color=BODY_GRAY)

add_rect(slide, Inches(7.0), Inches(1.8), Inches(5.7), Inches(1.4), fill_color=None,
         line_color=ACCENT_TEAL, line_width=Pt(1))
add_textbox(slide, Inches(7.2), Inches(1.85), Inches(5.3), Inches(0.3),
            text="Actor 头（策略网络）", font_size=Pt(13), font_color=ACCENT_TEAL, bold=True)
actor_items = [
    ("MLP: 2 层 Tanh (64 -> 64 -> 64)", False, Pt(11), BODY_GRAY),
    ("Linear(64 -> 1): 输出 x_i (连续标量)", False, Pt(11), BODY_GRAY),
    ("log_std: 可学习参数，初始化 -2.0 (std = 0.135)", False, Pt(11), BODY_GRAY),
    ("DiagGaussian(mu, sigma): 每节点独立动作分布", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(7.2), Inches(2.2), Inches(5.3), Inches(0.9),
                      actor_items, line_spacing=1.3)

add_rect(slide, Inches(7.0), Inches(3.4), Inches(5.7), Inches(1.4), fill_color=None,
         line_color=ACCENT_GREEN, line_width=Pt(1))
add_textbox(slide, Inches(7.2), Inches(3.45), Inches(5.3), Inches(0.3),
            text="Critic 头（价值网络）", font_size=Pt(13), font_color=ACCENT_GREEN, bold=True)
critic_items = [
    ("MLP: 2 层 Tanh (64 -> 64 -> 1)", False, Pt(11), BODY_GRAY),
    ("输出: V_i(s) 逐节点价值 (num_agents, 1)", False, Pt(11), BODY_GRAY),
    ("spatial value function: 不做聚合，保持逐节点", False, Pt(11), BODY_GRAY),
    ("GAE 中用 scatter_add 做子->父投影，处理变长拓扑", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(7.2), Inches(3.75), Inches(5.3), Inches(0.9),
                      critic_items, line_spacing=1.3)

add_textbox(slide, Inches(0.6), Inches(5.1), Inches(12.1), Inches(0.3),
            text="PPO 关键设计", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

ppo_details = [
    ("单路 GAE", "scatter_add 将子单元值聚合回父单元，无需多路 GAE", ACCENT_BLUE),
    ("log_std clamp", "每步 optimizer.step() 后 clamp 到 [-4.0, -1.0]，std in [0.018, 0.368]", ACCENT_TEAL),
    ("熵正则", "entropy_coefficient=0.001，防止 log_std 过早收敛到下限", ACCENT_GREEN),
    ("梯度裁剪", "max_grad_norm=0.5，稳定训练过程", ACCENT_WARM),
]
for i, (tag, desc, clr) in enumerate(ppo_details):
    x = Inches(0.6 + i * 3.1)
    add_rect(slide, x, Inches(5.45), Inches(2.85), Inches(0.85), fill_color=HIGHLIGHT_BG)
    add_textbox(slide, x + Inches(0.1), Inches(5.5), Inches(2.65), Inches(0.3),
                text=tag, font_size=Pt(13), font_color=clr, bold=True, alignment=PP_ALIGN.CENTER)
    add_textbox(slide, x + Inches(0.1), Inches(5.8), Inches(2.65), Inches(0.4),
                text=desc, font_size=Pt(10), font_color=BODY_GRAY, alignment=PP_ALIGN.CENTER)

add_takeaway_bar(slide, "双 GNN 各自独立建模 + DiagGaussian 连续动作 + scatter_add 单路 GAE -> 适合变长 agent 拓扑的 RL 训练框架")
add_slide_number(slide, 11)


# ======================================================================
# SLIDE 12: TRAINING OBSERVATIONS
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "训练观察与诊断：奖励稀疏性与大波数泛化")

add_rect(slide, Inches(0.6), Inches(1.3), Inches(5.8), Inches(2.4), fill_color=WARN_BG)
add_textbox(slide, Inches(0.8), Inches(1.35), Inches(5.4), Inches(0.35),
            text="观察 1: 75% rollout 步骤零 reward", font_size=Pt(14), font_color=ACCENT_WARM, bold=True)
obs1_lines = [
    ("4 步 rollout 中，第 0 步细化后介质区已达标 (h/lambda = 13 > N=15 参考线)", False, Pt(11), BODY_GRAY),
    ("步 1-3 全为零 reward，75% 的 FEM 求解白白浪费", False, Pt(11), BODY_GRAY),
    ("原因: 1-to-4 切分太粗，一步即达标，不存在差一点的中间状态", False, Pt(11), BODY_GRAY),
    ("偶尔的 spike (reward ~60) 来自随机探索中极负的 x_i 触发第二步细化", False, Pt(11), BODY_GRAY),
    ("--> 步 0 的 reward 信号足够训练「在哪里细化」的判断，但多步策略无法学习", False, Pt(11), CAPTION),
]
add_multiline_textbox(slide, Inches(0.8), Inches(1.7), Inches(5.4), Inches(1.85),
                      obs1_lines, line_spacing=1.35)

add_rect(slide, Inches(7.0), Inches(1.3), Inches(5.7), Inches(2.4), fill_color=WARN_BG)
add_textbox(slide, Inches(7.2), Inches(1.35), Inches(5.3), Inches(0.35),
            text="观察 2: 高 k 扇形阴影区网格偏粗", font_size=Pt(14), font_color=ACCENT_WARM, bold=True)
obs2_lines = [
    ("k in [2,20] 训练，小 k 尚可，大 k 效果不佳", False, Pt(11), BODY_GRAY),
    ("介质后方 +x 方向扇形区域网格偏粗，误差较大", False, Pt(11), BODY_GRAY),
    ("根本原因: 污染效应 -> 初始 kh > 0.5 时 FEM 解定性错误 (GIGO)", False, Pt(11), BODY_GRAY),
    ("粗网格 -> 错误解 -> 不可靠 eta -> 垃圾 GNN 特征 -> 垃圾动作", False, Pt(11), BODY_GRAY),
    ("2 层 GNN 感受野仅约 10 个单元，网络不知道自己在介质后方", False, Pt(11), BODY_GRAY),
]
add_multiline_textbox(slide, Inches(7.2), Inches(1.7), Inches(5.3), Inches(1.85),
                      obs2_lines, line_spacing=1.35)

add_textbox(slide, Inches(0.6), Inches(4.0), Inches(12.1), Inches(0.3),
            text="训练日志解读 (k in [2,20], 随机 PDE, 4 步 rollout)", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

log_lines = [
    ("loss ~ 0.10-0.18, explained_var ~ 0.65-0.78", "Critic 对价值函数的解释力中等偏上，尚可但非极强"),
    ("reward 间歇性 spike (0 -> 13 -> 60 -> 0)", "随机探索 + GAE 信度传播，信号稀疏但偶尔强正奖励"),
    ("agent 数量在 100-3500 间大幅波动", "取决于 PDE 随机采样和细化触发情况"),
    ("loss/ev 趋于平台期", "可能是 k^2 与 N=15 互斥的问题（已用 k^1.5 修复）"),
]
for i, (log, interpret) in enumerate(log_lines):
    add_textbox(slide, Inches(0.8), Inches(4.35 + i * 0.42), Inches(4.0), Inches(0.35),
                text=log, font_size=Pt(11), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, Inches(5.0), Inches(4.35 + i * 0.42), Inches(7.7), Inches(0.35),
                text=interpret, font_size=Pt(11), font_color=BODY_GRAY)

add_takeaway_bar(slide, "训练瓶颈非算法设计问题，而是物理前提 (污染效应 GIGO) 和多步细化粒度 (1-to-4 太粗) 的工程限制")
add_slide_number(slide, 12)


# ======================================================================
# SLIDE 13: INNOVATION SUMMARY
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "创新点汇总与可复用价值")

innovations = [
    ("[1]", "无量纲化\n残差误差估计",
     "k_local 归一化三项残差分量\n消除纯几何尺度偏差\nGNN 输入与 Reward 公式物理一致",
     ACCENT_BLUE),
    ("[2]", "Score-based\n连续尺寸场",
     "score = -x_i 纯排序\n物理预算 N_budget 约束\nDoerfler-P95 双过滤器掩码",
     ACCENT_TEAL),
    ("[3]", "L2 聚合\n奖励设计",
     "sqrt(sum eta_child^2) <= eta_parent 天然成立\n永不惩罚细化 (r_local >= 0)\nint 主导区强正奖励约 +0.69",
     ACCENT_GREEN),
    ("[4]", "尺度不变性\n架构",
     "N_init x domain_area 缩放\nlambda 无量纲化全部特征\nsign·ln 对数压缩 + 前渐近区约束",
     ACCENT_WARM),
    ("[5]", "双 GNN +\n变长拓扑 RL",
     "Policy/Value 独立 GNN 基座\nscatter_add 单路 GAE\nDiagGaussian + log_std clamp",
     RGBColor(0x5B, 0x3A, 0x8B)),
]

for i, (num, title, desc, clr) in enumerate(innovations):
    x = Inches(0.6 + i * 2.5)
    add_rect(slide, x, Inches(1.35), Inches(2.3), Inches(3.1), fill_color=HIGHLIGHT_BG)
    add_rect(slide, x, Inches(1.35), Inches(2.3), Pt(3), fill_color=clr)
    add_textbox(slide, x + Inches(0.15), Inches(1.5), Inches(2.0), Inches(0.7),
                text=title, font_size=Pt(13), font_color=clr, bold=True,
                alignment=PP_ALIGN.LEFT, line_spacing=1.2)
    add_textbox(slide, x + Inches(0.15), Inches(2.3), Inches(2.0), Inches(2.0),
                text=desc, font_size=Pt(10), font_color=BODY_GRAY, line_spacing=1.4)

add_textbox(slide, Inches(0.6), Inches(4.7), Inches(12.1), Inches(0.3),
            text="可复用价值（超越本项目的通用方法贡献）", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

reuse_items = [
    ("L2 聚合 + 父子映射", "适用于任何分裂型变长 agent RL 场景（网格细化、树搜索、层次化决策）"),
    ("k_local 无量纲化方法", "适用于具有特征尺度的任何 PDE 问题：跨介质、跨频率、跨几何的统一误差度量"),
    ("Score-based + 预算约束选择", "适用于资源受限的排序-选择问题：传感器部署、计算资源分配、实验设计优化"),
    ("Doerfler-P95 动作掩码", "P95 锚定物理尺度的思想可推广到任何需要排除低信号样本的场景"),
]
for i, (tag, desc) in enumerate(reuse_items):
    add_textbox(slide, Inches(0.8), Inches(5.05 + i * 0.42), Inches(2.8), Inches(0.35),
                text=tag, font_size=Pt(11), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, Inches(3.7), Inches(5.05 + i * 0.42), Inches(9.0), Inches(0.35),
                text=desc, font_size=Pt(11), font_color=BODY_GRAY)

add_slide_number(slide, 13)


# ======================================================================
# SLIDE 14: LIMITATIONS
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_slide_title(slide, "局限性与未解决问题")

limitations = [
    ("污染效应 (GIGO: Garbage-In-Garbage-Out)",
     [
         "高 k 下初始 kh > 0.5 时 FEM 解定性错误，误差指示子 eta_K 完全不可靠",
         "RL 无法在错误解的基础上学到正确策略 -- 这是物理前提而非算法问题",
         "缓解: N_init x domaine_area 使真空始终 >= 12 单元/lambda，但高 k 下余量有限",
     ]),
    ("GNN 感受野受限",
     [
         "2 层消息传递，每个节点感受野仅约 10 个单元，无法感知全局几何结构",
         "介质后方扇形阴影区：GNN 不知道自己在介质背后，小 k 学到的真空不需细化被错误泛化",
         "需要: 更多几何上下文特征（入射波方向、与介质相对位置）或更深的 GNN",
     ]),
    ("1-to-4 切分粒度",
     [
         "一步细化即可达标 (每波长单元数 >= N=15 参考线)，多步 rollout 中 75% 步骤零 reward",
         "高 eps_r 介质区可能需要 2-3 步细化，但 PPO GAE 在 4 步序列中传播稀疏信号效率极低",
         "需要: 更细粒度的切分方案（如 1-to-2 边切分）或递减的 N_per_wavelength 目标",
     ]),
    ("泛化到更多散射体配置",
     [
         "当前仅在单个圆形介质柱上训练；多散射体、非圆形、复杂材料的泛化未经验证",
         "训练波数 [2,20] 覆盖范围有限，更高 k 需要更深的初始网格和更强的特征表达",
         "需要: 更丰富的 PDE 问题分布、课程学习、域随机化策略",
     ]),
]

for i, (title, points) in enumerate(limitations):
    x = Inches(0.6 + (i % 2) * 6.3)
    y = Inches(1.3 + (i // 2) * 2.8)
    add_rect(slide, x, y, Inches(5.9), Inches(2.45), fill_color=None,
             line_color=LIGHT_LINE, line_width=Pt(1))
    add_rect(slide, x + Pt(1), y + Pt(1), Pt(3), Inches(0.35), fill_color=ACCENT_WARM)
    add_textbox(slide, x + Inches(0.2), y + Inches(0.05), Inches(5.3), Inches(0.35),
                text=title, font_size=Pt(14), font_color=ACCENT_WARM, bold=True)
    for j, point in enumerate(points):
        add_textbox(slide, x + Inches(0.2), y + Inches(0.45 + j * 0.45), Inches(5.3), Inches(0.4),
                    text=f"- {point}", font_size=Pt(10), font_color=BODY_GRAY)

add_slide_number(slide, 14)


# ======================================================================
# SLIDE 15: SUMMARY & DISCUSSION
# ======================================================================
slide = add_blank_slide()
set_slide_bg(slide, WHITE)
add_top_bar(slide)
add_rect(slide, Inches(0.6), Inches(2.0), Pt(4), Inches(4.0), fill_color=ACCENT_BLUE)

add_textbox(slide, Inches(0.85), Inches(2.0), Inches(11.5), Inches(1.0),
            text="总  结", font_size=Pt(36), font_color=BLACK, bold=True)

summary_points = [
    "提出了一套完整的 RL 自适应网格细化框架：从物理建模、误差估计、状态表征、动作空间到奖励设计的全链路创新",
    "无量纲化残差误差估计 (k_local 归一化) 使误差指示子具有跨介质、跨频率的物理一致性",
    "Score-based 尺寸场 + 物理预算约束 + Doerfler-P95 掩码实现了资源感知的细化单元选择",
    "L2 聚合奖励设计从数学上保证了细化奖励非负，从根本上避免了 L1 sum 的结构性负偏置",
    "sign(d)*ln(1+|d|/lambda) 对数压缩 + lambda 归一化全部特征实现了域尺寸的尺度不变泛化",
]

for i, point in enumerate(summary_points):
    add_textbox(slide, Inches(0.85), Inches(3.1 + i * 0.42), Inches(0.4), Inches(0.35),
                text=f"{i+1}.", font_size=Pt(14), font_color=ACCENT_BLUE, bold=True)
    add_textbox(slide, Inches(1.25), Inches(3.1 + i * 0.42), Inches(11.2), Inches(0.35),
                text=point, font_size=Pt(13), font_color=BODY_GRAY)

add_rect(slide, Inches(0.85), Inches(5.4), Inches(11.5), Inches(0.05), fill_color=LIGHT_LINE)
add_textbox(slide, Inches(0.85), Inches(5.6), Inches(11.5), Inches(0.4),
            text="讨论与后续方向", font_size=SUBHEAD_SIZE, font_color=BLACK, bold=True)

discussion_points = [
    "如何处理污染效应 (GIGO)？-> 更高阶 FEM (p-refinement) + 显式 kh 特征 + 更深的初始网格",
    "如何提升多步细化效率？-> 递减的 N_per_wavelength 目标 + 更细粒度切分 (1-to-2) + 课程学习",
    "如何拓展到更复杂场景？-> 多散射体、三维 Helmholtz、Maxwell 方程组、时域问题",
]

for i, point in enumerate(discussion_points):
    add_textbox(slide, Inches(0.85), Inches(6.05 + i * 0.35), Inches(0.35), Inches(0.3),
                text=">", font_size=Pt(12), font_color=ACCENT_TEAL, bold=True)
    add_textbox(slide, Inches(1.2), Inches(6.05 + i * 0.35), Inches(11.2), Inches(0.3),
                text=point, font_size=Pt(12), font_color=BODY_GRAY)

add_textbox(slide, Inches(8.5), Inches(7.0), Inches(4.5), Inches(0.4),
            text="谢谢！欢迎讨论。", font_size=Pt(18), font_color=ACCENT_BLUE, bold=True,
            alignment=PP_ALIGN.RIGHT)
add_slide_number(slide, 15)

# Save
output_path = "/public/home/dxw/Codes/afem/output/final_presentation_cn.pptx"
prs.save(output_path)
print(f"PPTX saved to {output_path}")
print(f"Slides: {len(prs.slides)}")
