XIAN-FEM-2026June/物理神经网络耦合图_PPT素材/generate_physics_nn_diagram.py

660 lines
24 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.

# -*- coding: utf-8 -*-
"""
生成「物理模块 + 神经网络层 深度耦合」架构图的可编辑 PPT 素材。
每个子元素单独一页,文字均为可编辑文本框;另含一页完整拼装参考。
运行: python generate_physics_nn_diagram.py
"""
from __future__ import annotations
import math
import random
from pathlib import Path
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.enum.shapes import MSO_CONNECTOR, MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt
OUT_DIR = Path(__file__).resolve().parent
OUT_PPTX = OUT_DIR / "物理神经网络耦合图_可编辑素材.pptx"
# 配色(贴近原图)
C_BG = RGBColor(0x0A, 0x0A, 0x0A)
C_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
C_BLACK = RGBColor(0x00, 0x00, 0x00)
C_BLUE = RGBColor(0x4A, 0x86, 0xC8)
C_BLUE_LT = RGBColor(0x6D, 0xA3, 0xE8)
C_TEAL = RGBColor(0x3D, 0xA8, 0x9A)
C_GREY = RGBColor(0xB0, 0xB0, 0xB0)
C_GREY_DK = RGBColor(0x55, 0x55, 0x55)
C_GREY_LT = RGBColor(0xD9, 0xD9, 0xD9)
C_LINE = RGBColor(0x88, 0x88, 0x88)
FONT = "Microsoft YaHei"
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
def new_prs() -> Presentation:
prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
return prs
def blank_slide(prs: Presentation, dark: bool = False):
slide = prs.slides.add_slide(prs.slide_layouts[6])
if dark:
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = C_BG
return slide
def add_label(slide, left, top, width, height, text, size=14, bold=False,
color=C_BLACK, align=PP_ALIGN.CENTER, wrap=True):
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.word_wrap = wrap
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
p = tf.paragraphs[0]
p.text = text
p.alignment = align
p.font.name = FONT
p.font.size = Pt(size)
p.font.bold = bold
p.font.color.rgb = color
return box
def add_round_box(slide, left, top, width, height, fill=C_WHITE, line=C_BLACK,
line_w=1.0, radius_hint=0.08):
shape = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height
)
shape.fill.solid()
shape.fill.fore_color.rgb = fill
shape.line.color.rgb = line
shape.line.width = Pt(line_w)
# 圆角
if hasattr(shape, "adjustments") and shape.adjustments:
shape.adjustments[0] = radius_hint
return shape
def add_oval(slide, left, top, size, fill, line=None, line_w=1.0):
shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, size, size)
shape.fill.solid()
shape.fill.fore_color.rgb = fill
if line:
shape.line.color.rgb = line
shape.line.width = Pt(line_w)
else:
shape.line.fill.background()
return shape
def add_line(slide, x1, y1, x2, y2, color=C_LINE, width=0.75, dash=None):
conn = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
conn.line.color.rgb = color
conn.line.width = Pt(width)
if dash:
conn.line.dash_style = dash
return conn
def add_arrow_down(slide, cx, y1, y2, color=C_WHITE, dash=False):
add_line(slide, cx, y1, cx, y2 - Inches(0.08), color, 1.2,
MSO_LINE_DASH_STYLE.DASH if dash else None)
tri = slide.shapes.add_shape(
MSO_SHAPE.ISOSCELES_TRIANGLE,
cx - Inches(0.06), y2 - Inches(0.12), Inches(0.12), Inches(0.12)
)
tri.rotation = 180
tri.fill.solid()
tri.fill.fore_color.rgb = color
tri.line.fill.background()
return tri
def add_arrow_up(slide, cx, y1, y2, color=C_WHITE):
add_line(slide, cx, y1 + Inches(0.08), cx, y2, color, 1.2)
tri = slide.shapes.add_shape(
MSO_SHAPE.ISOSCELES_TRIANGLE,
cx - Inches(0.06), y1, Inches(0.12), Inches(0.12)
)
tri.fill.solid()
tri.fill.fore_color.rgb = color
tri.line.fill.background()
def add_arrow_right(slide, x1, y, x2, color=C_WHITE):
add_line(slide, x1, y, x2 - Inches(0.08), y, color, 1.5)
tri = slide.shapes.add_shape(
MSO_SHAPE.ISOSCELES_TRIANGLE,
x2 - Inches(0.12), y - Inches(0.06), Inches(0.12), Inches(0.12)
)
tri.rotation = 90
tri.fill.solid()
tri.fill.fore_color.rgb = color
tri.line.fill.background()
def add_double_arrow_h(slide, x1, y1, x2, y2=None, color=C_WHITE):
if y2 is None:
y2 = y1
add_line(slide, x1 + Inches(0.1), y1, x2 - Inches(0.1), y2, color, 1.5)
for x, rot in ((x1, 270), (x2 - Inches(0.12), 90)):
tri = slide.shapes.add_shape(
MSO_SHAPE.ISOSCELES_TRIANGLE,
x, y1 - Inches(0.06), Inches(0.12), Inches(0.12)
)
tri.rotation = rot
tri.fill.solid()
tri.fill.fore_color.rgb = color
tri.line.fill.background()
def draw_mesh_icon(slide, cx, cy, w, h, color=C_GREY):
"""简化 3D 网格图标"""
pts = [
(cx - w * 0.35, cy + h * 0.2),
(cx, cy - h * 0.35),
(cx + w * 0.35, cy + h * 0.2),
(cx, cy + h * 0.35),
]
for i in range(4):
x1, y1 = pts[i]
x2, y2 = pts[(i + 1) % 4]
add_line(slide, x1, y1, x2, y2, color, 1.0)
add_line(slide, pts[0][0], pts[0][1], pts[2][0], pts[2][1], color, 0.8)
add_line(slide, pts[1][0], pts[1][1], pts[3][0], pts[3][1], color, 0.8)
for i in range(3):
t = (i + 1) / 4
mx = pts[0][0] + t * (pts[2][0] - pts[0][0])
my = pts[0][1] + t * (pts[2][1] - pts[0][1])
add_line(slide, mx, my, pts[1][0], pts[1][1], color, 0.6)
def draw_radar_icon(slide, cx, cy, size, color=C_GREY):
base = slide.shapes.add_shape(
MSO_SHAPE.OVAL, cx - size * 0.15, cy + size * 0.05,
size * 0.3, size * 0.15
)
base.fill.solid()
base.fill.fore_color.rgb = color
base.line.fill.background()
dish = slide.shapes.add_shape(
MSO_SHAPE.OVAL, cx - size * 0.35, cy - size * 0.25,
size * 0.7, size * 0.35
)
dish.fill.background()
dish.line.color.rgb = color
dish.line.width = Pt(1.2)
for ang in (-25, 0, 25):
rad = math.radians(ang - 90)
add_line(
slide, cx, cy,
cx + math.cos(rad) * size * 0.55,
cy + math.sin(rad) * size * 0.55,
color, 0.8, MSO_LINE_DASH_STYLE.DASH
)
def draw_field_icon(slide, cx, cy, w, h, color=C_GREY):
rect = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE, cx - w / 2, cy - h / 2, w, h
)
rect.fill.background()
rect.line.color.rgb = color
rect.line.width = Pt(1.0)
for i in range(4):
y = cy - h / 2 + h * (i + 1) / 5
add_line(slide, cx - w / 2, y, cx + w / 2, y, color, 0.5)
for i in range(4):
x = cx - w / 2 + w * (i + 1) / 5
add_line(slide, x, cy - h / 2, x, cy + h / 2, color, 0.5)
peak = add_oval(slide, cx - Inches(0.08), cy - Inches(0.08), Inches(0.16), C_GREY_DK)
def draw_wave_icon(slide, cx, cy, w, color=C_GREY):
for i, phase in enumerate((0, 0.4, 0.8)):
pts = []
steps = 20
for s in range(steps + 1):
t = s / steps
x = cx - w / 2 + w * t
y = cy + math.sin(t * math.pi * 2 + phase) * Inches(0.12)
pts.append((x, y))
for j in range(len(pts) - 1):
add_line(slide, pts[j][0], pts[j][1], pts[j + 1][0], pts[j + 1][1],
color, 1.0 if i == 1 else 0.7)
def draw_beam_icon(slide, cx, cy, size, color=C_GREY):
add_line(slide, cx - size * 0.3, cy, cx, cy, color, 1.2)
for ang in (-35, -15, 15, 35):
rad = math.radians(ang)
add_line(slide, cx, cy, cx + math.cos(rad) * size * 0.45,
cy + math.sin(rad) * size * 0.45, color, 1.0)
def draw_boundary_icon(slide, cx, cy, size, color=C_GREY):
add_line(slide, cx, cy - size * 0.35, cx, cy + size * 0.35, color, 1.2,
MSO_LINE_DASH_STYLE.DASH)
add_line(slide, cx - size * 0.35, cy, cx + size * 0.1, cy - size * 0.15, color, 1.2)
tri = slide.shapes.add_shape(
MSO_SHAPE.ISOSCELES_TRIANGLE,
cx + size * 0.05, cy - size * 0.22, Inches(0.1), Inches(0.1)
)
tri.rotation = 120
tri.fill.solid()
tri.fill.fore_color.rgb = color
tri.line.fill.background()
def draw_scale_icon(slide, cx, cy, w, color=C_GREY):
add_line(slide, cx, cy + Inches(0.15), cx, cy - Inches(0.25), color, 1.5)
add_line(slide, cx - w / 2, cy - Inches(0.25), cx + w / 2, cy - Inches(0.25), color, 1.5)
for dx in (-w / 2, w / 2):
pan = slide.shapes.add_shape(
MSO_SHAPE.OVAL, cx + dx - Inches(0.12), cy - Inches(0.05),
Inches(0.24), Inches(0.12)
)
pan.fill.background()
pan.line.color.rgb = color
pan.line.width = Pt(1.0)
def slide_title_bar(slide, title: str):
add_label(slide, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.45),
title, size=18, bold=True, color=C_GREY_DK, align=PP_ALIGN.LEFT)
# ---------- 各子元素 ----------
def make_top_box_1(slide):
slide_title_bar(slide, "01 · 顶部算子框 — 连续方程算子")
w, h = Inches(2.6), Inches(1.35)
x, y = Inches(5.4), Inches(2.8)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
"连续方程算子", size=16, bold=True)
add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
"∇·(ρv) + ∂ρ/∂t = 0", size=13, color=C_GREY_DK)
add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)
def make_top_box_2(slide):
slide_title_bar(slide, "02 · 顶部算子框 — 边界条件")
w, h = Inches(2.6), Inches(1.35)
x, y = Inches(5.4), Inches(2.8)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
"边界条件", size=16, bold=True)
add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
"B(u) = g 或 n×E = 0", size=13, color=C_GREY_DK)
add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)
def make_top_box_3(slide):
slide_title_bar(slide, "03 · 顶部算子框 — 传播/散射算子")
w, h = Inches(2.6), Inches(1.35)
x, y = Inches(5.4), Inches(2.8)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
"传播/散射算子", size=16, bold=True)
add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
"∇²E + k²E = 0", size=13, color=C_GREY_DK)
add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)
def make_top_box_4(slide):
slide_title_bar(slide, "04 · 顶部算子框 — 守恒约束")
w, h = Inches(2.6), Inches(1.35)
x, y = Inches(5.4), Inches(2.8)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
"守恒约束", size=16, bold=True)
add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
"能量 / 动量 / 质量守恒", size=13, color=C_GREY_DK)
add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)
def make_left_geometry(slide):
slide_title_bar(slide, "05 · 左侧 — 几何网格 / 材料分布")
w, h = Inches(1.55), Inches(3.2)
x, y = Inches(5.0), Inches(2.0)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.08), w, Inches(0.35),
"几何规则约束", size=11, bold=True)
draw_mesh_icon(slide, x + w / 2, y + h * 0.45, w, h * 0.55)
add_label(slide, x, y + h - Inches(0.55), w, Inches(0.45),
"几何网格\n材料分布", size=12, bold=True)
add_double_arrow_h(slide, x + w, y + h / 2, x + w + Inches(0.55), y + h / 2)
def make_neural_network(slide):
slide_title_bar(slide, "06 · 中部 — 神经网络层6层")
layer_colors = [C_BLUE, C_GREY_DK, C_TEAL, C_WHITE, C_GREY_DK, C_BLUE_LT]
neuron_colors = [C_BLUE, C_GREY, C_TEAL, C_GREY_LT, C_GREY, C_BLUE_LT]
n_layers = 6
n_neurons = 7
layer_w = Inches(0.55)
gap = Inches(0.18)
total_w = n_layers * layer_w + (n_layers - 1) * gap
x0 = (SLIDE_W - total_w) / 2
y0 = Inches(1.8)
layer_h = Inches(3.6)
neuron_r = Inches(0.11)
neuron_gap = (layer_h - Inches(0.5)) / (n_neurons - 1)
layer_centers = []
neuron_positions = []
for li in range(n_layers):
lx = x0 + li * (layer_w + gap)
add_round_box(slide, lx, y0, layer_w, layer_h,
fill=RGBColor(0xFF, 0xFF, 0xFF), line=layer_colors[li], line_w=1.5)
cx = lx + layer_w / 2
layer_centers.append(cx)
positions = []
ny = y0 + Inches(0.25)
for ni in range(n_neurons):
if ni == n_neurons // 2:
add_label(slide, cx - Inches(0.08), ny - Inches(0.05),
Inches(0.16), Inches(0.2), "", size=14, color=C_GREY_DK)
ny += neuron_gap
continue
add_oval(slide, cx - neuron_r, ny - neuron_r, neuron_r * 2,
neuron_colors[li], line=layer_colors[li], line_w=0.8)
positions.append((cx, ny))
ny += neuron_gap
neuron_positions.append(positions)
random.seed(42)
for li in range(n_layers - 1):
for (x1, y1) in neuron_positions[li]:
for (x2, y2) in neuron_positions[li + 1]:
if random.random() < 0.55:
add_line(slide, x1, y1, x2, y2, C_LINE, 0.4)
def make_right_decoder(slide):
slide_title_bar(slide, "07 · 右侧 — 物理解码器(算子输出)")
w, h = Inches(1.55), Inches(3.2)
x, y = Inches(6.8), Inches(2.0)
add_round_box(slide, x, y, w, h)
add_label(slide, x, y + Inches(0.06), w, Inches(0.55),
"物理解码器\n(算子输出)", size=11, bold=True)
draw_radar_icon(slide, x + w / 2, y + Inches(1.05), Inches(0.9))
draw_field_icon(slide, x + w / 2, y + Inches(2.0), Inches(1.0), Inches(0.75))
add_label(slide, x, y + h - Inches(0.45), w, Inches(0.35),
"... 多源输出", size=11, color=C_GREY_DK)
add_arrow_right(slide, x - Inches(0.55), y + h / 2, x)
def make_bottom_box_1(slide):
slide_title_bar(slide, "08 · 底部算子 — 载荷频率算子")
w, h = Inches(1.7), Inches(1.15)
x, y = Inches(5.8), Inches(3.0)
add_round_box(slide, x, y, w, h)
draw_beam_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.7))
add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
"载荷频率\n算子", size=12, bold=True)
add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))
def make_bottom_box_2(slide):
slide_title_bar(slide, "09 · 底部算子 — 边界条件")
w, h = Inches(1.7), Inches(1.15)
x, y = Inches(5.8), Inches(3.0)
add_round_box(slide, x, y, w, h)
draw_boundary_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.75))
add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
"边界条件", size=12, bold=True)
add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))
def make_bottom_box_3(slide):
slide_title_bar(slide, "10 · 底部算子 — 传播/散射算子")
w, h = Inches(1.7), Inches(1.15)
x, y = Inches(5.8), Inches(3.0)
add_round_box(slide, x, y, w, h)
draw_wave_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.85))
add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
"传播/散射\n算子", size=12, bold=True)
add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))
def make_bottom_box_4(slide):
slide_title_bar(slide, "11 · 底部算子 — 守恒约束")
w, h = Inches(1.7), Inches(1.15)
x, y = Inches(5.8), Inches(3.0)
add_round_box(slide, x, y, w, h)
draw_scale_icon(slide, x + w / 2, y + Inches(0.38), Inches(0.75))
add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
"守恒约束", size=12, bold=True)
add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))
def make_footer_title(slide):
slide_title_bar(slide, "12 · 底部标题条")
w, h = Inches(8.5), Inches(0.75)
x, y = Inches(2.4), Inches(3.2)
box = add_round_box(slide, x, y, w, h, fill=C_WHITE, line=C_TEAL, line_w=2.0)
add_label(slide, x, y, w, h,
"物理模块 + 神经网络层 深度耦合", size=22, bold=True, color=C_TEAL)
def make_connection_lines(slide):
slide_title_bar(slide, "13 · 连接线与反馈回路")
y = Inches(3.5)
add_line(slide, Inches(1.5), y, Inches(11.8), y, C_WHITE, 1.2,
MSO_LINE_DASH_STYLE.DASH)
add_arrow_down(slide, Inches(3.5), Inches(2.5), Inches(3.5), dash=False)
add_arrow_down(slide, Inches(6.5), Inches(2.5), Inches(6.5), dash=False)
add_arrow_down(slide, Inches(9.5), Inches(2.5), Inches(9.5), dash=False)
add_label(slide, Inches(1.0), Inches(4.0), Inches(11.0), Inches(0.5),
"(虚线底栏 + 各层虚线箭头,可按需复制到拼装页)",
size=12, color=C_GREY_DK)
def make_full_assembly(slide):
"""完整拼装参考页(深色背景)"""
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = C_BG
add_label(slide, Inches(0.3), Inches(0.1), Inches(12.7), Inches(0.4),
"完整拼装参考(所有元素可从此页复制,或从前页分别插入)",
size=14, bold=True, color=C_WHITE, align=PP_ALIGN.LEFT)
# 顶部四框
top_y = Inches(0.55)
top_specs = [
("连续方程算子", "∇·(ρv)+∂ρ/∂t=0"),
("边界条件", "B(u)=g"),
("传播/散射算子", "∇²E+k²E=0"),
("守恒约束", "能量/动量/质量守恒"),
]
tw, th = Inches(2.35), Inches(0.95)
tx0 = Inches(0.55)
tg = Inches(0.28)
top_xs = []
for i, (title, formula) in enumerate(top_specs):
tx = tx0 + i * (tw + tg)
top_xs.append(tx + tw / 2)
add_round_box(slide, tx, top_y, tw, th, fill=C_WHITE)
add_label(slide, tx, top_y + Inches(0.04), tw, Inches(0.32),
title, size=10, bold=True, color=C_BLACK)
add_label(slide, tx, top_y + Inches(0.36), tw, Inches(0.5),
formula, size=9, color=C_GREY_DK)
# 神经网络区域
nn_y0 = Inches(1.65)
nn_h = Inches(2.55)
layer_colors = [C_BLUE, C_GREY_DK, C_TEAL, C_WHITE, C_GREY_DK, C_BLUE_LT]
neuron_colors = [C_BLUE, C_GREY, C_TEAL, C_GREY_LT, C_GREY, C_BLUE_LT]
n_layers, n_neurons = 6, 5
layer_w = Inches(0.42)
gap = Inches(0.12)
nn_x0 = Inches(3.05)
neuron_positions = []
layer_centers = []
for li in range(n_layers):
lx = nn_x0 + li * (layer_w + gap)
add_round_box(slide, lx, nn_y0, layer_w, nn_h,
fill=RGBColor(0x18, 0x18, 0x18), line=layer_colors[li], line_w=1.2)
cx = lx + layer_w / 2
layer_centers.append(cx)
positions = []
ny = nn_y0 + Inches(0.18)
ngap = (nn_h - Inches(0.36)) / (n_neurons - 1)
nr = Inches(0.07)
for ni in range(n_neurons):
if ni == n_neurons // 2:
add_label(slide, cx - Inches(0.06), ny - Inches(0.04),
Inches(0.12), Inches(0.15), "", size=10, color=C_GREY)
ny += ngap
continue
add_oval(slide, cx - nr, ny - nr, nr * 2, neuron_colors[li],
line=layer_colors[li], line_w=0.6)
positions.append((cx, ny))
ny += ngap
neuron_positions.append(positions)
random.seed(7)
for li in range(n_layers - 1):
for p1 in neuron_positions[li]:
for p2 in neuron_positions[li + 1]:
if random.random() < 0.5:
add_line(slide, p1[0], p1[1], p2[0], p2[1], C_LINE, 0.35)
# 顶部虚线箭头
for i, cx in enumerate([layer_centers[0], layer_centers[1],
layer_centers[3], layer_centers[5]]):
add_arrow_down(slide, cx, top_y + th, nn_y0, color=C_WHITE, dash=True)
# 左侧
lw, lh = Inches(1.15), Inches(2.55)
lx, ly = Inches(1.55), nn_y0
add_round_box(slide, lx, ly, lw, lh, fill=RGBColor(0x18, 0x18, 0x18), line=C_WHITE)
add_label(slide, lx, ly + Inches(0.04), lw, Inches(0.3),
"几何规则约束", size=8, bold=True, color=C_WHITE)
draw_mesh_icon(slide, lx + lw / 2, ly + lh * 0.45, lw, lh * 0.5, C_GREY)
add_label(slide, lx, ly + lh - Inches(0.42), lw, Inches(0.38),
"几何网格\n材料分布", size=9, bold=True, color=C_WHITE)
add_double_arrow_h(slide, lx + lw, ly + lh / 2, nn_x0, ly + lh / 2)
# 右侧
rx = Inches(9.65)
add_round_box(slide, rx, ly, lw, lh, fill=RGBColor(0x18, 0x18, 0x18), line=C_WHITE)
add_label(slide, rx, ly + Inches(0.04), lw, Inches(0.42),
"物理解码器\n(算子输出)", size=8, bold=True, color=C_WHITE)
draw_radar_icon(slide, rx + lw / 2, ly + Inches(0.95), Inches(0.65), C_GREY)
draw_field_icon(slide, rx + lw / 2, ly + Inches(1.75), Inches(0.7), Inches(0.5), C_GREY)
add_label(slide, rx, ly + lh - Inches(0.32), lw, Inches(0.28),
"... 多源输出", size=8, color=C_GREY)
add_arrow_right(slide, layer_centers[-1] + layer_w / 2 + Inches(0.05),
ly + lh / 2, rx)
# 底部四框
bot_y = Inches(4.55)
bw, bh = Inches(1.45), Inches(0.85)
bot_specs = [
("载荷频率\n算子", draw_beam_icon),
("边界条件", draw_boundary_icon),
("传播/散射\n算子", draw_wave_icon),
("守恒约束", draw_scale_icon),
]
bx0 = Inches(2.15)
bg = Inches(0.22)
bot_centers = []
for i, (txt, icon_fn) in enumerate(bot_specs):
bx = bx0 + i * (bw + bg)
bot_centers.append(bx + bw / 2)
add_round_box(slide, bx, bot_y, bw, bh, fill=C_WHITE)
if icon_fn == draw_wave_icon:
icon_fn(slide, bx + bw / 2, bot_y + Inches(0.28), Inches(0.65))
elif icon_fn == draw_scale_icon:
icon_fn(slide, bx + bw / 2, bot_y + Inches(0.3), Inches(0.6))
else:
icon_fn(slide, bx + bw / 2, bot_y + Inches(0.28), Inches(0.6))
add_label(slide, bx, bot_y + Inches(0.42), bw, Inches(0.4),
txt, size=9, bold=True, color=C_BLACK)
for cx, li in zip(bot_centers, [0, 1, 3, 5]):
add_arrow_up(slide, cx, bot_y, nn_y0 + nn_h, color=C_WHITE)
# 底栏虚线
dash_y = Inches(5.55)
add_line(slide, Inches(1.3), dash_y, Inches(12.0), dash_y, C_WHITE, 1.0,
MSO_LINE_DASH_STYLE.DASH)
# 底部标题
fw, fh = Inches(7.5), Inches(0.6)
fx = (SLIDE_W - fw) / 2
fy = Inches(6.15)
add_round_box(slide, fx, fy, fw, fh, fill=RGBColor(0x18, 0x18, 0x18),
line=C_TEAL, line_w=2.0)
add_label(slide, fx, fy, fw, fh,
"物理模块 + 神经网络层 深度耦合", size=18, bold=True, color=C_TEAL)
def make_usage_slide(slide):
slide_title_bar(slide, "使用说明")
items = [
"本文件共 15 页:第 113 页为独立子元素,第 14 页为完整拼装参考,第 15 页为说明。",
"所有中文、公式均为可编辑文本框,双击即可修改字体与内容。",
"建议做法:打开对应页 → Ctrl+A 全选 → 复制 → 粘贴到目标 PPT。",
"图标由 PPT 基本形状组合而成,可在「选择窗格」中单独调整。",
"若需公式编辑器格式,可选中文本后 插入 → 公式 重新录入。",
"神经网络连线为独立线条,可在选择窗格中批量删除或重画。",
]
y = Inches(1.2)
for item in items:
add_label(slide, Inches(0.8), y, Inches(11.8), Inches(0.55),
"" + item, size=14, color=C_GREY_DK, align=PP_ALIGN.LEFT)
y += Inches(0.62)
def build():
prs = new_prs()
makers = [
make_top_box_1,
make_top_box_2,
make_top_box_3,
make_top_box_4,
make_left_geometry,
make_neural_network,
make_right_decoder,
make_bottom_box_1,
make_bottom_box_2,
make_bottom_box_3,
make_bottom_box_4,
make_footer_title,
make_connection_lines,
]
for fn in makers:
slide = blank_slide(prs, dark=False)
fn(slide)
full = blank_slide(prs, dark=True)
make_full_assembly(full)
usage = blank_slide(prs, dark=False)
make_usage_slide(usage)
prs.save(str(OUT_PPTX))
print(f"已生成: {OUT_PPTX}")
print(f"{len(prs.slides)} 页幻灯片")
if __name__ == "__main__":
build()