afem/outlook/output/build_pptx.py

1072 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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²·(ε_r1)·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",
"+1teacher 独有(应加密)",
" 0两者一致",
"1physics 独有(不应加密)",
], 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 → 残差 η → 三种标记 → 保存 → 加密",
"每步保存 .npz15-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_sbcSommerfeld 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 115 维特征工程",
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 3CorrectionGNN 架构",
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 broadcastscale 可学习"),
("边特征", "|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.950top-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 overlapGNN/probability top-k 与 teacher_mark 交集",
"AUCROC-AUCGNN vs physics baseline",
"gnn_beats_physics_ratioGNN 优于 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 baselineh/λ_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 4bRollout 评估 — 迭代加密到目标单元数",
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 正确预测的加密单元",
"FPGNN 误判为应加密",
"FNGNN 漏掉的应加密单元",
"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 问题和其他 PDEMaxwell, 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 LearningGNN 学习 teacher_mark 与 physics_mark 之间的差异,"
"推理时无需 FEM solve"),
("数据", "100 样本 × ~12 步 = 1236 个训练样本15-dim 特征 + physics_score"),
("架构", "DensityGNN 骨干 + 二分类头,消息传递 + GVN + edge dropout"),
("结果", "AUC=0.950top-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()