#!/usr/bin/env python3 """ Build GNN-AMR correction presentation PPTX from README content. Nature-style academic presentation, simplified Chinese. """ import json from pathlib import Path import numpy as np from pptx import Presentation from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.shapes import MSO_SHAPE # ── Paths ── ROOT = Path(__file__).resolve().parent.parent.parent # afem/ OUTLOOK = ROOT / "outlook" OUTPUT = OUTLOOK / "output" ASSETS = OUTPUT / "assets" / "figures" # ── Colors ── WHITE = RGBColor(0xFF, 0xFF, 0xFF) BG_LIGHT = RGBColor(0xF8, 0xF9, 0xFA) TEXT_DARK = RGBColor(0x1A, 0x1A, 0x2E) TEXT_MID = RGBColor(0x4A, 0x4A, 0x5A) TEXT_LIGHT = RGBColor(0x8A, 0x8A, 0x9A) ACCENT_BLUE = RGBColor(0x21, 0x96, 0xF3) ACCENT_RED = RGBColor(0xE9, 0x1E, 0x63) ACCENT_GREEN = RGBColor(0x4C, 0xAF, 0x50) ACCENT_ORANGE = RGBColor(0xFF, 0x98, 0x00) BORDER_LIGHT = RGBColor(0xE0, 0xE0, 0xE0) # ── Fonts ── FONT_CN = "Microsoft YaHei" FONT_EN = "Calibri" # ── Slide dimensions (16:9) ── SLIDE_W = Inches(13.333) SLIDE_H = Inches(7.5) def set_slide_bg(slide, color=BG_LIGHT): """Set slide background color.""" bg = slide.background fill = bg.fill fill.solid() fill.fore_color.rgb = color def add_shape_fill(slide, left, top, width, height, color, alpha=None): """Add a colored rectangle shape.""" shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height) shape.fill.solid() shape.fill.fore_color.rgb = color shape.line.fill.background() if alpha is not None: shape.fill.fore_color.brightness = alpha return shape def add_text_box(slide, left, top, width, height, text, font_size=14, color=TEXT_DARK, bold=False, align=PP_ALIGN.LEFT, font_name=FONT_CN, line_spacing=1.3): """Add a text box with formatted text.""" txBox = slide.shapes.add_textbox(left, top, width, height) tf = txBox.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = text p.font.size = Pt(font_size) p.font.color.rgb = color p.font.bold = bold p.font.name = font_name p.alignment = align p.space_after = Pt(2) if line_spacing: p.line_spacing = Pt(font_size * line_spacing) return txBox def add_bullet_list(slide, left, top, width, height, items, font_size=14, color=TEXT_DARK, font_name=FONT_CN, bullet_char="•"): """Add a bulleted list.""" txBox = slide.shapes.add_textbox(left, top, width, height) tf = txBox.text_frame tf.word_wrap = True for i, item in enumerate(items): if i == 0: p = tf.paragraphs[0] else: p = tf.add_paragraph() p.text = f"{bullet_char} {item}" p.font.size = Pt(font_size) p.font.color.rgb = color p.font.name = font_name p.space_after = Pt(6) p.line_spacing = Pt(font_size * 1.5) return txBox def add_caption(slide, left, top, width, text, font_size=9, color=TEXT_LIGHT): """Add a small caption/source line.""" return add_text_box(slide, left, top, width, Inches(0.3), text, font_size=font_size, color=color) def add_takeaway_strip(slide, text, top=None): """Add a bottom takeaway strip.""" if top is None: top = SLIDE_H - Inches(0.6) strip = add_shape_fill(slide, Inches(0), top, SLIDE_W, Inches(0.5), RGBColor(0xE8, 0xF0, 0xFE)) add_text_box(slide, Inches(0.5), top + Inches(0.05), SLIDE_W - Inches(1), Inches(0.4), f"✦ {text}", font_size=12, color=ACCENT_BLUE, bold=True, align=PP_ALIGN.LEFT) def add_source_label(slide, text): """Add source label at bottom-right.""" add_text_box(slide, SLIDE_W - Inches(4), SLIDE_H - Inches(0.35), Inches(3.5), Inches(0.25), text, font_size=8, color=TEXT_LIGHT, align=PP_ALIGN.RIGHT) def add_image_safe(slide, img_path, left, top, width, height): """Add image if file exists, otherwise add placeholder.""" p = Path(img_path) if p.exists() and p.stat().st_size > 0: slide.shapes.add_picture(str(p), left, top, width, height) else: shape = add_shape_fill(slide, left, top, width, height, BORDER_LIGHT) add_text_box(slide, left, top + height // 2 - Inches(0.2), width, Inches(0.4), f"[{p.name}]", font_size=10, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) # ============================================================================ # Slide builders # ============================================================================ def slide_01_title(prs): """Title slide.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank set_slide_bg(slide, WHITE) # Top accent bar add_shape_fill(slide, Inches(0), Inches(0), SLIDE_W, Inches(0.08), ACCENT_BLUE) # Title add_text_box(slide, Inches(1), Inches(1.8), Inches(11), Inches(1.2), "GNN 引导的自适应网格加密", font_size=36, color=TEXT_DARK, bold=True, align=PP_ALIGN.CENTER) # Subtitle add_text_box(slide, Inches(1), Inches(3.1), Inches(11), Inches(0.8), "用于 2D Helmholtz 散射问题的图神经网络修正学习", font_size=20, color=TEXT_MID, align=PP_ALIGN.CENTER) # Separator line add_shape_fill(slide, Inches(5), Inches(4.2), Inches(3), Inches(0.03), ACCENT_BLUE) # Meta add_text_box(slide, Inches(1), Inches(4.6), Inches(11), Inches(0.5), "Problem-to-Solution: 用 GNN 学习残差驱动 AMR 的修正信号", font_size=14, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) add_text_box(slide, Inches(1), Inches(5.5), Inches(11), Inches(0.5), "AFEM Research Group | 2026", font_size=12, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) def slide_02_background(prs): """Research background.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "研究背景:为什么这个问题重要", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_BLUE) # Left column: physics problem add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "物理问题:2D 介质圆柱电磁散射", font_size=18, color=ACCENT_BLUE, bold=True) add_bullet_list(slide, Inches(0.6), Inches(2.1), Inches(5.5), Inches(3.5), [ "求解散射场公式:∇²u + k²·ε_r·u = −k²·(ε_r−1)·u_inc", "入射波 u_inc = exp(i·k·x),Sommerfeld 辐射 BC", "介质圆柱参数:k ∈ [3,15],ε_r ∈ [2,8],r ∈ [0.05,0.25]", "FEM 求解器:P1 三角元 + Sommerfeld BC", "参考解:Mie 解析解(精确对比基准)", ], font_size=13) # Right column: why it matters add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "核心挑战:网格预算有限下的误差最小化", font_size=18, color=ACCENT_RED, bold=True) add_bullet_list(slide, Inches(6.8), Inches(2.1), Inches(5.8), Inches(3.5), [ "精细网格 → 高精度,但计算代价高", "粗网格 → 快速,但误差大", "目标:在给定网格预算 N 下,最小化 FEM 解误差", "传统 AMR 需要逐步求解 FEM → 计算开销大", "能否用数据驱动方法替代?", ], font_size=13) # Bottom box: parameter space box = add_shape_fill(slide, Inches(0.6), Inches(5.6), Inches(12.1), Inches(1.2), RGBColor(0xF0, 0xF4, 0xF8)) add_text_box(slide, Inches(0.8), Inches(5.7), Inches(11.5), Inches(0.4), "参数空间", font_size=14, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(0.8), Inches(6.1), Inches(11.5), Inches(0.5), "k ∈ [3,15] | ε_r ∈ [2,8] | cx, cy ∈ [0.2,0.8] | radius ∈ [0.05,0.25] | " "统一初始网格 32×32 = 2048 单元", font_size=12, color=TEXT_MID) def slide_03_gap(prs): """Knowledge gap / bottleneck.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "技术瓶颈:传统残差驱动 AMR 的局限", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_RED) # Left: traditional AMR add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "传统残差驱动 AMR 流程", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(0.6), Inches(2.1), Inches(5.5), Inches(3.0), [ "每步:FEM 求解 → 计算残差估计器 η → 标记 top-k → 加密", "残差 η = √(r_int² + r_jump² + r_sbc²)", "需要每步都做 FEM solve → 计算量大", "η 是局部最优,不能利用全局几何/物理信息", "无法离线推理:必须在线计算残差", ], font_size=13) # Right: what we want add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "我们希望实现", font_size=18, color=ACCENT_GREEN, bold=True) add_bullet_list(slide, Inches(6.8), Inches(2.1), Inches(5.8), Inches(3.0), [ "用 GNN 学习 \"哪些单元应被加密\"", "推理时只需几何/物理特征 → 无需 FEM solve", "一次前向传播 → 全图单元标记", "可离线部署,计算代价大幅降低", "超越纯物理先验的标记策略", ], font_size=13) # Bottom: key insight box = add_shape_fill(slide, Inches(0.6), Inches(5.3), Inches(12.1), Inches(1.5), RGBColor(0xFE, 0xF3, 0xE2)) add_text_box(slide, Inches(0.8), Inches(5.4), Inches(11.5), Inches(0.4), "核心洞察", font_size=14, color=ACCENT_ORANGE, bold=True) add_text_box(slide, Inches(0.8), Inches(5.85), Inches(11.5), Inches(0.8), "物理先验 physics_score = h/λ_eff 可以粗略判断分辨率不足的单元,但它忽略了散射体几何、" "波场相位、材料界面等关键信息。GNN 可以学习这些物理先验无法捕捉的修正信号。", font_size=13, color=TEXT_MID) def slide_04_approach(prs): """Core approach: correction learning.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "核心思路:GNN 学习修正信号", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_GREEN) # Three mark schemes add_text_box(slide, Inches(0.6), Inches(1.5), Inches(12), Inches(0.5), "三种标记方案的对比", font_size=18, color=TEXT_MID, bold=True) # Three boxes box_w = Inches(3.7) box_h = Inches(2.8) gap = Inches(0.35) x0 = Inches(0.6) y_top = Inches(2.2) # Box 1: teacher b1 = add_shape_fill(slide, x0, y_top, box_w, box_h, RGBColor(0xE8, 0xF5, 0xE9)) add_text_box(slide, x0 + Inches(0.15), y_top + Inches(0.1), box_w - Inches(0.3), Inches(0.4), "teacher_mark (η 信号)", font_size=14, color=ACCENT_GREEN, bold=True) add_bullet_list(slide, x0 + Inches(0.15), y_top + Inches(0.55), box_w - Inches(0.3), Inches(2.0), [ "基于残差估计器 η 排序", "取 top 3% 单元标记为 1", "需要 FEM solve → 计算代价高", "代表 \"最优\" 加密决策", ], font_size=12, color=TEXT_MID, bullet_char="▸") # Box 2: physics b2 = add_shape_fill(slide, x0 + box_w + gap, y_top, box_w, box_h, RGBColor(0xE3, 0xF2, 0xFD)) add_text_box(slide, x0 + box_w + gap + Inches(0.15), y_top + Inches(0.1), box_w - Inches(0.3), Inches(0.4), "physics_score (物理先验)", font_size=14, color=ACCENT_BLUE, bold=True) add_bullet_list(slide, x0 + box_w + gap + Inches(0.15), y_top + Inches(0.55), box_w - Inches(0.3), Inches(2.0), [ "h / λ_eff,纯几何计算", "无需 FEM solve", "忽略波场、几何、材料信息", "基线对比方法", ], font_size=12, color=TEXT_MID, bullet_char="▸") # Box 3: correction b3 = add_shape_fill(slide, x0 + 2 * (box_w + gap), y_top, box_w, box_h, RGBColor(0xFC, 0xE4, 0xEC)) add_text_box(slide, x0 + 2 * (box_w + gap) + Inches(0.15), y_top + Inches(0.1), box_w - Inches(0.3), Inches(0.4), "correction_label (GNN 目标)", font_size=14, color=ACCENT_RED, bold=True) add_bullet_list(slide, x0 + 2 * (box_w + gap) + Inches(0.15), y_top + Inches(0.55), box_w - Inches(0.3), Inches(2.0), [ "= teacher_mark − physics_mark", "+1:teacher 独有(应加密)", " 0:两者一致", "−1:physics 独有(不应加密)", ], font_size=12, color=TEXT_MID, bullet_char="▸") # Bottom takeaway add_takeaway_strip(slide, "GNN 的训练目标:学习 teacher 与 physics 之间的差异,使标记策略超越纯物理先验") def slide_05_pipeline(prs): """Full pipeline overview.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "全流程概览:5 步流水线", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_BLUE) # Pipeline steps as connected boxes steps = [ ("Step 1", "数据生成", "gen.py", "残差驱动 AMR\n保存逐步数据 + 标签", ACCENT_BLUE), ("Step 2", "数据校验", "check.py", "字段完整性\n统计趋势\n空间可视化", ACCENT_GREEN), ("Step 3", "训练", "train.py", "features + physics_score\n→ GNN → sigmoid\n→ 二分类", ACCENT_RED), ("Step 4", "评估", "test/eval.py", "离线指标 + Rollout\n三种方法对比", ACCENT_ORANGE), ("Step 5", "可视化", "viz.py", "端到端 AMR\n单步对比", RGBColor(0x9C, 0x27, 0xB0)), ] box_w = Inches(2.2) box_h = Inches(3.2) x_start = Inches(0.5) y_top = Inches(1.6) gap = Inches(0.3) for i, (step_num, title, script, desc, color) in enumerate(steps): x = x_start + i * (box_w + gap) # Step number circle circle = slide.shapes.add_shape(MSO_SHAPE.OVAL, x + Inches(0.8), y_top, Inches(0.6), Inches(0.6)) circle.fill.solid() circle.fill.fore_color.rgb = color circle.line.fill.background() tf = circle.text_frame tf.paragraphs[0].text = str(i + 1) tf.paragraphs[0].font.size = Pt(18) tf.paragraphs[0].font.color.rgb = WHITE tf.paragraphs[0].font.bold = True tf.paragraphs[0].alignment = PP_ALIGN.CENTER tf.word_wrap = False # Title add_text_box(slide, x, y_top + Inches(0.7), box_w, Inches(0.4), title, font_size=16, color=color, bold=True, align=PP_ALIGN.CENTER) # Script name add_text_box(slide, x, y_top + Inches(1.1), box_w, Inches(0.3), script, font_size=10, color=TEXT_LIGHT, align=PP_ALIGN.CENTER, font_name=FONT_EN) # Description add_text_box(slide, x + Inches(0.1), y_top + Inches(1.5), box_w - Inches(0.2), Inches(1.8), desc, font_size=11, color=TEXT_MID, align=PP_ALIGN.CENTER) # Arrow between steps if i < len(steps) - 1: arrow_x = x + box_w + Inches(0.05) arrow_y = y_top + Inches(0.25) add_text_box(slide, arrow_x, arrow_y, Inches(0.2), Inches(0.3), "→", font_size=20, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) # Bottom: key data flow box = add_shape_fill(slide, Inches(0.5), Inches(5.3), Inches(12.3), Inches(1.5), RGBColor(0xF5, 0xF5, 0xF5)) add_text_box(slide, Inches(0.7), Inches(5.4), Inches(11.8), Inches(0.4), "数据流", font_size=14, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(0.7), Inches(5.85), Inches(11.8), Inches(0.8), "PDE 参数采样 → 初始网格 → AMR 循环(FEM solve → η → marks → save → refine)→ " "训练数据 (.npz) → GNN 训练 → checkpoint → 离线推理 / Rollout 评估 → 可视化", font_size=12, color=TEXT_MID) def slide_06_data(prs): """Data generation and marking strategy.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 1:数据生成与标记策略", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_BLUE) # Left: data generation flow add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "残差驱动 AMR 循环", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(0.6), Inches(2.1), Inches(5.5), Inches(2.5), [ "100 个 PDE 样本,统一初始网格 32×32", "每步:FEM solve → 残差 η → 三种标记 → 保存 → 加密", "每步保存 .npz:15-dim 特征 + 边索引 + 标记", "共 1236 个 step 文件,平均 ~12 步/样本", "加密时使用安全过滤:面积 + 反向 Dörfler", ], font_size=13) # Right: residual estimator add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "残差估计器 η(Teacher Signal)", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(6.8), Inches(2.1), Inches(5.8), Inches(2.5), [ "η = √(r_int² + r_jump² + r_sbc²)", "r_int:内部残差(PDE 残差 × h/k)", "r_jump:梯度跳变(相邻单元边界)", "r_sbc:Sommerfeld BC 边界残差", "使用 k₀ 归一化,介质内短波残差自然放大", ], font_size=13) # Bottom: data check visualization add_image_safe(slide, OUTLOOK / "data_correction_check" / "sample0011_step002.png", Inches(0.6), Inches(4.6), Inches(12.1), Inches(2.4)) add_source_label(slide, "Source: check_correction_data.py — 4 面板空间可视化") def slide_07_features(prs): """Feature engineering.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 1:15 维特征工程", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_BLUE) # Feature table features = [ ("0, 1", "x, y", "单元中点坐标"), ("2", "area", "单元面积"), ("3", "dist_to_center", "到圆柱中心距离"), ("4", "signed_dist", "dist − radius(负值=介质内)"), ("5", "inside", "是否在圆柱内 (0/1)"), ("6", "k", "波数(全局常量)"), ("7", "eps_r", "介电常数(全局常量)"), ("8", "radius", "半径(全局常量)"), ("9, 10", "cx, cy", "圆柱中心(全局常量)"), ("11", "k_h", "k × √area(网格-波数耦合)"), ("12", "k_eps_h", "k × √eps_r × √area(介质内分辨率)"), ("13", "sin(k·x)", "入射波相位 sin 分量"), ("14", "cos(k·x)", "入射波相位 cos 分量"), ] # Table header y_start = Inches(1.5) row_h = Inches(0.32) col_widths = [Inches(0.8), Inches(2.0), Inches(5.5)] x_start = Inches(0.6) # Header header_bg = add_shape_fill(slide, x_start, y_start, sum(w for w in col_widths), row_h, ACCENT_BLUE) headers = ["维度", "特征名", "说明"] x = x_start for j, (hdr, w) in enumerate(zip(headers, col_widths)): add_text_box(slide, x, y_start + Inches(0.02), w, row_h, hdr, font_size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER) x += w # Rows for i, (dim, name, desc) in enumerate(features): y = y_start + (i + 1) * row_h bg_color = RGBColor(0xF8, 0xF9, 0xFA) if i % 2 == 0 else WHITE add_shape_fill(slide, x_start, y, sum(w for w in col_widths), row_h, bg_color) x = x_start vals = [dim, name, desc] for j, (val, w) in enumerate(zip(vals, col_widths)): font_name = FONT_EN if j < 2 else FONT_CN add_text_box(slide, x, y + Inches(0.02), w, row_h, val, font_size=10, color=TEXT_DARK, align=PP_ALIGN.CENTER if j < 2 else PP_ALIGN.LEFT, font_name=font_name) x += w # Right: physics score box_x = Inches(9.2) box = add_shape_fill(slide, box_x, Inches(1.5), Inches(3.5), Inches(3.5), RGBColor(0xE8, 0xF0, 0xFE)) add_text_box(slide, box_x + Inches(0.15), Inches(1.6), Inches(3.2), Inches(0.4), "Physics Score(第 16 维输入)", font_size=13, color=ACCENT_BLUE, bold=True) add_text_box(slide, box_x + Inches(0.15), Inches(2.1), Inches(3.2), Inches(2.5), "λ_eff = 2π/(k·√ε_r) 介质内\n" " = 2π/k 介质外\n\n" "h = √(2·area) 特征长度\n\n" "physics_score = h / λ_eff\n\n" "> 1 表示分辨率不足\n" "→ 应优先加密", font_size=11, color=TEXT_MID, font_name=FONT_EN) # Bottom: normalization add_takeaway_strip(slide, "训练时 15-dim 特征 + physics_score = 16-dim 输入,z-score 归一化") def slide_08_gnn(prs): """GNN architecture.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 3:CorrectionGNN 架构", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_RED) # Architecture diagram as text boxes # Embedding layer add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "模型结构", font_size=18, color=TEXT_MID, bold=True) arch_lines = [ "CorrectionGNN(基于 DensityGNN 骨干)", "├── node_embedding: Linear(16 → latent_dim)", "├── edge_embedding: Linear(16 → latent_dim)", "├── mp_steps × 3:", "│ ├── EdgeModule: MLP([src|dst|edge]) → latent_dim", "│ ├── NodeModule: MLP([node|scatter_mean]) → latent_dim", "│ └── LayerNorm + Residual", "├── GlobalVirtualNode: mean_pool → attn_gate → broadcast", "└── head: Linear(64→64) → ReLU → Linear(64→1)", ] y = Inches(2.1) for line in arch_lines: add_text_box(slide, Inches(0.6), y, Inches(5.5), Inches(0.3), line, font_size=11, color=TEXT_DARK, font_name="Consolas") y += Inches(0.28) # Right: key design choices add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "关键设计", font_size=18, color=TEXT_MID, bold=True) designs = [ ("消息传递", "每步含边更新 + 节点更新,2 层 MLP + LayerNorm + 残差"), ("GVN", "mean pooling → attention-gated broadcast,scale 可学习"), ("边特征", "|x[src] − x[dst]|(16 维绝对差),输入和 latent 各计算一次"), ("边丢弃", "训练时 edge_dropout=0.1,推理时 0.0"), ("输出头", "单 logit → BCEWithLogitsLoss 训练"), ] y = Inches(2.1) for title, desc in designs: add_text_box(slide, Inches(6.8), y, Inches(5.8), Inches(0.3), f"▸ {title}", font_size=13, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(7.1), y + Inches(0.32), Inches(5.5), Inches(0.4), desc, font_size=11, color=TEXT_MID) y += Inches(0.8) # Bottom: training config table box = add_shape_fill(slide, Inches(0.6), Inches(5.0), Inches(12.1), Inches(2.0), RGBColor(0xF5, 0xF5, 0xF5)) add_text_box(slide, Inches(0.8), Inches(5.1), Inches(11.5), Inches(0.4), "训练配置", font_size=14, color=ACCENT_RED, bold=True) configs = [ "latent_dim=64", "mp_steps=3", "head_hidden=64", "edge_dropout=0.1", "lr=1e-3", "weight_decay=1e-4", "batch_size=1", "epochs=100", "optimizer=Adam", "scheduler=ReduceLROnPlateau", "AMP (GPU)", ] add_text_box(slide, Inches(0.8), Inches(5.5), Inches(11.5), Inches(0.4), " | ".join(configs), font_size=10, color=TEXT_MID, font_name=FONT_EN) # Loss function add_text_box(slide, Inches(0.8), Inches(6.0), Inches(11.5), Inches(0.8), "损失函数:BCEWithLogitsLoss + per-graph pos_weight = neg_count/pos_count," "解决正负样本不平衡。向量化实现,无 Python 循环。", font_size=12, color=TEXT_MID) def slide_09_training(prs): """Training results.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 3:训练结果 — AUC 0.950,top-k 远超物理先验", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_GREEN) # Key metrics as big numbers metrics = [ ("AUC", "0.950", "二分类判别能力"), ("GNN top-k", "46.6%", "与 η 标记的重叠率"), ("Physics top-k", "15.3%", "物理先验基线"), ("GNN/Physics", "3.0×", "GNN 优势倍数"), ] box_w = Inches(2.8) box_h = Inches(1.8) x_start = Inches(0.6) y_top = Inches(1.6) gap = Inches(0.25) for i, (label, value, desc) in enumerate(metrics): x = x_start + i * (box_w + gap) color = ACCENT_BLUE if i < 2 else (ACCENT_GREEN if i == 3 else TEXT_MID) bg = RGBColor(0xE8, 0xF0, 0xFE) if i < 2 else ( RGBColor(0xE8, 0xF5, 0xE9) if i == 3 else RGBColor(0xF5, 0xF5, 0xF5)) add_shape_fill(slide, x, y_top, box_w, box_h, bg) add_text_box(slide, x, y_top + Inches(0.15), box_w, Inches(0.35), label, font_size=13, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) add_text_box(slide, x, y_top + Inches(0.5), box_w, Inches(0.8), value, font_size=32, color=color, bold=True, align=PP_ALIGN.CENTER, font_name=FONT_EN) add_text_box(slide, x, y_top + Inches(1.3), box_w, Inches(0.35), desc, font_size=10, color=TEXT_LIGHT, align=PP_ALIGN.CENTER) # Training curve placeholder add_text_box(slide, Inches(0.6), Inches(3.8), Inches(5.5), Inches(0.5), "训练过程", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(0.6), Inches(4.3), Inches(5.5), Inches(2.5), [ "100 epochs,最佳 epoch=40", "train_loss: 0.435 → val_loss: 0.533", "val_topk_overlap: 0.466 (vs physics 0.153)", "GNN 在验证集上 100% 样本优于 physics", "使用 AMP 混合精度加速 GPU 训练", ], font_size=13) # Right: what this means add_text_box(slide, Inches(6.8), Inches(3.8), Inches(5.8), Inches(0.5), "结果解读", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(6.8), Inches(4.3), Inches(5.8), Inches(2.5), [ "AUC=0.95 表明 GNN 能很好地区分应加密/不应加密单元", "top-k 46.6% 意味着 GNN 预测的 top-k 中近半与 η 一致", "物理先验仅 15.3% → GNN 学到了大量额外信息", "GNN 从几何+波场相位中捕捉了 η 的关键模式", ], font_size=13) add_takeaway_strip(slide, "GNN 不仅记住了 η 的模式,还学到了从几何/物理特征推断加密优先级的能力") def slide_10_eval(prs): """Offline evaluation.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 4a:离线评估 — GNN vs Physics 对比", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_ORANGE) # Test visualization image add_image_safe(slide, OUTLOOK / "result" / "correction" / "vis" / "sample0000_step004_vis.png", Inches(0.6), Inches(1.5), Inches(12.1), Inches(3.5)) add_source_label(slide, "Source: test_correction.py — 验证集逐图可视化 (2×2: teacher/GNN/physics/TP·FP·FN·TN)") # Bottom: what the panels show add_text_box(slide, Inches(0.6), Inches(5.2), Inches(5.5), Inches(0.5), "评估指标", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(0.6), Inches(5.7), Inches(5.5), Inches(1.5), [ "top-k overlap:GNN/probability top-k 与 teacher_mark 交集", "AUC:ROC-AUC(GNN vs physics baseline)", "gnn_beats_physics_ratio:GNN 优于 physics 的样本比例", ], font_size=12) add_text_box(slide, Inches(6.8), Inches(5.2), Inches(5.8), Inches(0.5), "解读", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(6.8), Inches(5.7), Inches(5.8), Inches(1.5), [ "左上:True teacher_mark(η top-3% 标记)", "右上:GNN 预测(top-k selection)", "左下:Physics baseline(h/λ_eff top-k)", "右下:TP/FP/FN/TN 误差分类图", ], font_size=12) def slide_11_rollout(prs): """Rollout evaluation results.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 4b:Rollout 评估 — 迭代加密到目标单元数", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_ORANGE) # Rollout curve add_image_safe(slide, OUTLOOK / "result" / "correction" / "rollout" / "aw_rel_vs_elements.png", Inches(0.6), Inches(1.5), Inches(7), Inches(4.0)) add_source_label(slide, "Source: eval_correction.py — aw_rel vs actual elements") # Right: three methods comparison add_text_box(slide, Inches(8.0), Inches(1.5), Inches(4.8), Inches(0.5), "三种评估方法", font_size=16, color=TEXT_MID, bold=True) methods_data = [ ("physics", "h/λ_eff", "纯物理先验基线"), ("neural", "GNN sigmoid", "纯 GNN 概率"), ("hybrid", "α·z(physics)+β·z(neural)", "z-score 混合"), ] y = Inches(2.1) for method, formula, desc in methods_data: add_text_box(slide, Inches(8.0), y, Inches(4.8), Inches(0.3), f"▸ {method}: {formula}", font_size=12, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(8.3), y + Inches(0.3), Inches(4.5), Inches(0.3), desc, font_size=11, color=TEXT_MID) y += Inches(0.7) # Bottom: results table box = add_shape_fill(slide, Inches(0.6), Inches(5.6), Inches(12.1), Inches(1.5), RGBColor(0xF5, 0xF5, 0xF5)) add_text_box(slide, Inches(0.8), Inches(5.7), Inches(11.5), Inches(0.4), "Rollout 结果(aw_rel,面积加权相对误差)", font_size=13, color=ACCENT_ORANGE, bold=True) add_text_box(slide, Inches(0.8), Inches(6.1), Inches(11.5), Inches(0.9), "target=4000: physics 14.1% → neural 14.0% → hybrid 13.8% (↓1.9%)\n" "target=8000: physics 13.3% → neural 13.0% → hybrid 13.0% (↓1.8%)\n" "target=12000: physics 12.9% → neural 12.8% → hybrid 12.8% (↓0.2%)", font_size=11, color=TEXT_MID, font_name=FONT_EN) def slide_12_viz(prs): """Visualization: AMR results.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "Step 5:可视化 — 端到端 AMR 与单步对比", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), RGBColor(0x9C, 0x27, 0xB0)) # AMR overview add_image_safe(slide, OUTLOOK / "result" / "correction" / "viz" / "amr_overview.png", Inches(0.6), Inches(1.5), Inches(12.1), Inches(2.8)) add_source_label(slide, "Source: viz_correction.py — GNN 驱动 AMR 全流程:网格 + 场图总览") # Bottom: two modes add_text_box(slide, Inches(0.6), Inches(4.6), Inches(5.5), Inches(0.5), "模式 1:端到端 AMR", font_size=16, color=RGBColor(0x9C, 0x27, 0xB0), bold=True) add_bullet_list(slide, Inches(0.6), Inches(5.1), Inches(5.5), Inches(1.8), [ "GNN 驱动完整细化循环", "每步做 FEM solve,展示网格和场的演变", "输出:amr_overview.png + amr_steps/stepXX.png", "支持自定义物理参数(--k, --eps-r 等)", ], font_size=12) add_text_box(slide, Inches(6.8), Inches(4.6), Inches(5.8), Inches(0.5), "模式 2:单步对比", font_size=16, color=RGBColor(0x9C, 0x27, 0xB0), bold=True) add_bullet_list(slide, Inches(6.8), Inches(5.1), Inches(5.8), Inches(1.8), [ "重建指定 AMR 步的 mesh", "对比 GNN / teacher / physics 三种标记", "2×2 对比图 + 3 面板场图 + 误差图", "支持 Physics vs GNN vs Eta 三方对比", ], font_size=12) def slide_13_viz_detail(prs): """Visualization detail: marks comparison.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "单步对比:GNN vs Teacher vs Physics 标记", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), RGBColor(0x9C, 0x27, 0xB0)) # Marks comparison image add_image_safe(slide, OUTLOOK / "result" / "correction" / "viz" / "marks_sample0005_step010.png", Inches(0.6), Inches(1.5), Inches(7.5), Inches(5.5)) add_source_label(slide, "Source: viz_correction.py — marks_sample0005_step010.png") # Right: interpretation add_text_box(slide, Inches(8.5), Inches(1.5), Inches(4.3), Inches(0.5), "解读", font_size=18, color=TEXT_MID, bold=True) add_bullet_list(slide, Inches(8.5), Inches(2.1), Inches(4.3), Inches(4.5), [ "左上:True teacher_mark(η top-3%)", "右上:GNN 预测标记", "左下:Physics baseline 标记", "右下:误差分类", "", "TP(绿):GNN 正确预测的加密单元", "FP(蓝):GNN 误判为应加密", "FN(红):GNN 漏掉的应加密单元", "TN(灰):正确预测的非加密单元", ], font_size=12) add_takeaway_strip(slide, "GNN 能有效捕捉散射体界面和波前附近的高残差区域") def slide_14_robustness(prs): """Validation and robustness.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "验证与稳健性:数据质量保障", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_GREEN) # Five checks add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "数据校验 5 项检查", font_size=18, color=TEXT_MID, bold=True) checks = [ ("1", "字段完整性", "每个 .npz 包含所有必需字段、维度/类型正确"), ("2", "全局统计", "teacher/physics mark 比例、IoU、correction 正负比"), ("3", "逐步趋势", "correction 信号随 AMR 步数合理衰减"), ("4", "空间可视化", "随机 5 样本 4 面板图(score/eta/mark/correction)"), ("5", "最终判定", "PASS / WARNING / FAIL"), ] y = Inches(2.1) for num, title, desc in checks: circle = slide.shapes.add_shape(MSO_SHAPE.OVAL, Inches(0.6), y, Inches(0.4), Inches(0.4)) circle.fill.solid() circle.fill.fore_color.rgb = ACCENT_GREEN circle.line.fill.background() tf = circle.text_frame tf.paragraphs[0].text = num tf.paragraphs[0].font.size = Pt(12) tf.paragraphs[0].font.color.rgb = WHITE tf.paragraphs[0].font.bold = True tf.paragraphs[0].alignment = PP_ALIGN.CENTER add_text_box(slide, Inches(1.1), y, Inches(1.5), Inches(0.35), title, font_size=13, color=TEXT_DARK, bold=True) add_text_box(slide, Inches(2.6), y, Inches(4), Inches(0.35), desc, font_size=11, color=TEXT_MID) y += Inches(0.5) # Right: comparison visualization add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "三方对比可视化", font_size=18, color=TEXT_MID, bold=True) add_image_safe(slide, OUTLOOK / "result" / "correction" / "viz" / "compare_sample0005_step010.png", Inches(6.8), Inches(2.1), Inches(5.8), Inches(3.0)) add_source_label(slide, "Source: viz_correction.py — Physics vs GNN vs Eta (2×3 field + error)") # Bottom: robustness box = add_shape_fill(slide, Inches(0.6), Inches(5.5), Inches(12.1), Inches(1.3), RGBColor(0xE8, 0xF5, 0xE9)) add_text_box(slide, Inches(0.8), Inches(5.6), Inches(11.5), Inches(0.4), "稳健性保障", font_size=14, color=ACCENT_GREEN, bold=True) add_bullet_list(slide, Inches(0.8), Inches(6.0), Inches(11.5), Inches(0.7), [ "训练/验证按 sample ID 划分(无数据泄漏) | 统一初始网格消除网格差异 | " "安全过滤防止退化单元 | 多目标单元数评估(2k/4k/8k/12k)", ], font_size=11, color=TEXT_MID) def slide_15_innovation(prs): """Innovation and value.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "创新点与可复用价值", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_BLUE) innovations = [ ("Correction Learning 框架", "不直接预测 η,而是学习 teacher 与 physics 之间的差异。" "这使得 GNN 专注于物理先验无法捕捉的信息,而非重复已有知识。"), ("无需 FEM 的推理", "推理时只需几何/物理特征(15-dim + physics_score)," "无需 FEM solve,可离线部署,计算代价从 O(N_steps × FEM) 降至 O(N_steps × GNN forward)。"), ("三标记对比设计", "teacher_mark / physics_mark / correction_label 的三重对比," "清晰量化了 GNN 相对于物理先验的增量价值。"), ("安全过滤机制", "面积过滤 + 反向 Dörfler 过滤的两层安全机制," "防止对退化单元或误差极小单元做无意义加密。"), ("统一评估体系", "离线指标(top-k/AUC)+ Rollout 评估(三种方法 × 多目标单元数)" "+ 端到端可视化,全方位验证 GNN 的有效性。"), ] y = Inches(1.5) for i, (title, desc) in enumerate(innovations): # Number add_text_box(slide, Inches(0.6), y, Inches(0.5), Inches(0.4), f"{i+1}.", font_size=16, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(1.1), y, Inches(4.5), Inches(0.4), title, font_size=15, color=TEXT_DARK, bold=True) add_text_box(slide, Inches(1.1), y + Inches(0.4), Inches(11), Inches(0.5), desc, font_size=12, color=TEXT_MID) y += Inches(1.0) add_takeaway_strip(slide, "Correction learning 框架可推广到其他 PDE 问题和 AMR 策略") def slide_16_limitations(prs): """Limitations and open questions.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12), Inches(0.7), "局限性与未解决问题", font_size=28, color=TEXT_DARK, bold=True) add_shape_fill(slide, Inches(0.6), Inches(1.1), Inches(2), Inches(0.04), ACCENT_ORANGE) # Current limitations add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.5), "当前局限", font_size=18, color=ACCENT_RED, bold=True) add_bullet_list(slide, Inches(0.6), Inches(2.1), Inches(5.5), Inches(3.5), [ "仅验证 2D Helmholtz 介质圆柱散射问题", "参数空间有限:k∈[3,15],ε_r∈[2,8]", "统一初始网格(32×32),未测试非均匀初始网格", "Rollout 改善幅度有限(~2%),高预算下差异缩小", "correction_label 依赖 η 的质量(teacher 信号的上限)", "batch_size=1 的图级训练,大规模数据效率低", ], font_size=13) # Open questions add_text_box(slide, Inches(6.8), Inches(1.5), Inches(5.8), Inches(0.5), "开放问题", font_size=18, color=ACCENT_BLUE, bold=True) add_bullet_list(slide, Inches(6.8), Inches(2.1), Inches(5.8), Inches(3.5), [ "能否推广到 3D 问题和其他 PDE(Maxwell, elasticity)?", "如何处理更复杂的散射体(多体、非圆柱)?", "是否可以用 RL 替代 supervised correction learning?", "如何自适应调整 mark_fraction?", "GNN 推理速度 vs FEM solve 的实际加速比?", "能否与 hp-AMR 或 spectral 方法结合?", ], font_size=13) # Bottom: boundary conditions box = add_shape_fill(slide, Inches(0.6), Inches(5.5), Inches(12.1), Inches(1.3), RGBColor(0xFE, 0xF3, 0xE2)) add_text_box(slide, Inches(0.8), Inches(5.6), Inches(11.5), Inches(0.4), "应用边界", font_size=14, color=ACCENT_ORANGE, bold=True) add_text_box(slide, Inches(0.8), Inches(6.0), Inches(11.5), Inches(0.7), "本方法的核心假设:(1) 物理先验 physics_score 提供了合理的基线;" "(2) 残差估计器 η 是可信的 teacher signal;" "(3) 网格拓扑变化可以通过图结构捕捉。" "当这些假设不成立时(如高频率问题、复杂几何),方法的有效性需要重新验证。", font_size=12, color=TEXT_MID) def slide_17_summary(prs): """Summary.""" slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, WHITE) # Top accent bar add_shape_fill(slide, Inches(0), Inches(0), SLIDE_W, Inches(0.08), ACCENT_BLUE) add_text_box(slide, Inches(0.6), Inches(0.6), Inches(12), Inches(0.7), "总结", font_size=32, color=TEXT_DARK, bold=True, align=PP_ALIGN.CENTER) # Summary points as cards cards = [ ("问题", "2D Helmholtz 散射的 FEM 求解需要自适应网格加密," "但传统残差驱动 AMR 计算代价高"), ("方法", "Correction Learning:GNN 学习 teacher_mark 与 physics_mark 之间的差异," "推理时无需 FEM solve"), ("数据", "100 样本 × ~12 步 = 1236 个训练样本,15-dim 特征 + physics_score"), ("架构", "DensityGNN 骨干 + 二分类头,消息传递 + GVN + edge dropout"), ("结果", "AUC=0.950,top-k 46.6%(vs physics 15.3%)," "Rollout aw_rel 改善 ~2%"), ("价值", "Correction learning 框架可推广到其他 PDE 问题," "推理代价从 FEM solve 降至 GNN forward"), ] y = Inches(1.6) for i, (label, text) in enumerate(cards): bg = RGBColor(0xE8, 0xF0, 0xFE) if i % 2 == 0 else RGBColor(0xF5, 0xF5, 0xF5) add_shape_fill(slide, Inches(1), y, Inches(11.3), Inches(0.7), bg) add_text_box(slide, Inches(1.2), y + Inches(0.05), Inches(1.5), Inches(0.6), label, font_size=14, color=ACCENT_BLUE, bold=True) add_text_box(slide, Inches(2.8), y + Inches(0.05), Inches(9.2), Inches(0.6), text, font_size=13, color=TEXT_MID) y += Inches(0.8) # Bottom add_text_box(slide, Inches(1), Inches(6.5), Inches(11.3), Inches(0.5), "谢谢!欢迎提问与讨论", font_size=20, color=TEXT_DARK, bold=True, align=PP_ALIGN.CENTER) # ============================================================================ # Main # ============================================================================ def main(): prs = Presentation() prs.slide_width = SLIDE_W prs.slide_height = SLIDE_H # Build all slides slide_01_title(prs) slide_02_background(prs) slide_03_gap(prs) slide_04_approach(prs) slide_05_pipeline(prs) slide_06_data(prs) slide_07_features(prs) slide_08_gnn(prs) slide_09_training(prs) slide_10_eval(prs) slide_11_rollout(prs) slide_12_viz(prs) slide_13_viz_detail(prs) slide_14_robustness(prs) slide_15_innovation(prs) slide_16_limitations(prs) slide_17_summary(prs) # Save out_path = OUTPUT / "final_presentation_cn.pptx" prs.save(str(out_path)) print(f"✓ Saved: {out_path}") print(f" Slides: {len(prs.slides)}") # Count images img_count = 0 for slide in prs.slides: for shape in slide.shapes: if shape.shape_type == 13: # picture img_count += 1 print(f" Images: {img_count}") return out_path if __name__ == "__main__": main()