4. 波动方程初值问题

任务目标

使用物理信息神经网络(PINN)求解一维波动方程初值问题,模拟弦的自由振动,验证网络对双曲型偏微分方程的求解能力,并与解析解进行定量对比。

问题定义

求解如下一维波动方程初值问题:

∂²u/∂t² = c² · ∂²u/∂x²,  x ∈ [0, 1], t ∈ [0, 1]

初始条件:u(x, 0) = sin(πx),∂u/∂t(x, 0) = 0
边界条件:u(0, t) = u(1, t) = 0
波速:c = 1

该问题的解析解为 u(x, t) = sin(πx)cos(πt),用于验证PINN精度。

实现要点

PINN损失函数包含三部分:方程残差(内部配点满足波动方程)、初始位移残差、初始速度残差(对t求偏导为0)。边界条件通过强制构造满足:令网络输出乘以 x(1-x) 因子,自动满足两端零边界。网络输入为 (x, t),输出为位移 u。

完整代码

import torch
import torch.nn as nn
import numpy as np

# ---------- 网络定义 ----------
class PINN(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2, 64), nn.Tanh(),
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 64), nn.Tanh(),
            nn.Linear(64, 1)
        )
    def forward(self, x, t):
        # 强制边界条件 u(0,t)=u(1,t)=0
        u = self.net(torch.cat([x, t], dim=1))
        return u * x * (1 - x)

# ---------- 损失函数 ----------
def loss_fn(model, x_eq, t_eq, x_ic, t_ic, x_ic_v, t_ic_v):
    x_eq.requires_grad_(True)
    t_eq.requires_grad_(True)
    
    # 方程残差: u_tt - u_xx = 0
    u = model(x_eq, t_eq)
    u_t = torch.autograd.grad(u, t_eq, grad_outputs=torch.ones_like(u), create_graph=True)[0]
    u_tt = torch.autograd.grad(u_t, t_eq, grad_outputs=torch.ones_like(u_t), create_graph=True)[0]
    u_x = torch.autograd.grad(u, x_eq, grad_outputs=torch.ones_like(u), create_graph=True)[0]
    u_xx = torch.autograd.grad(u_x, x_eq, grad_outputs=torch.ones_like(u_x), create_graph=True)[0]
    loss_eq = torch.mean((u_tt - u_xx) ** 2)
    
    # 初始位移: u(x,0) = sin(πx)
    u_ic = model(x_ic, t_ic)
    loss_ic = torch.mean((u_ic - torch.sin(np.pi * x_ic)) ** 2)
    
    # 初始速度: u_t(x,0) = 0
    u_v = model(x_ic_v, t_ic_v)
    u_v_t = torch.autograd.grad(u_v, t_ic_v, grad_outputs=torch.ones_like(u_v), create_graph=True)[0]
    loss_iv = torch.mean(u_v_t ** 2)
    
    return loss_eq + loss_ic + loss_iv

# ---------- 训练数据 ----------
torch.manual_seed(42)
N_eq = 2000   # 内部配点
N_ic = 200    # 初始条件点

# 方程配点: 拉丁超立方采样
x_eq = torch.rand(N_eq, 1)
t_eq = torch.rand(N_eq, 1) * 1.0

# 初始位移点 t=0
x_ic = torch.rand(N_ic, 1)
t_ic = torch.zeros(N_ic, 1)

# 初始速度点 t=0 (需要二阶导,单独一组)
x_ic_v = torch.rand(N_ic, 1)
t_ic_v = torch.zeros(N_ic, 1)
t_ic_v.requires_grad_(True)

# ---------- 训练 ----------
model = PINN()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for epoch in range(5000):
    optimizer.zero_grad()
    loss = loss_fn(model, x_eq, t_eq, x_ic, t_ic, x_ic_v, t_ic_v)
    loss.backward()
    optimizer.step()
    if epoch % 500 == 0:
        print(f'Epoch {epoch:5d}, Loss: {loss.item():.2e}')

# ---------- 验证 ----------
# 在网格上对比解析解
x_test = torch.linspace(0, 1, 101).unsqueeze(1)
t_test = torch.linspace(0, 1, 101).unsqueeze(1)
X, T = torch.meshgrid(x_test.squeeze(), t_test.squeeze(), indexing='ij')
Xf = X.reshape(-1, 1)
Tf = T.reshape(-1, 1)

u_pred = model(Xf, Tf).detach().reshape(101, 101)
u_exact = torch.sin(np.pi * X) * torch.cos(np.pi * T)

# 相对L2误差
err = torch.norm(u_pred - u_exact) / torch.norm(u_exact)
print(f'相对L2误差: {err.item():.4f}')

# 保存预测结果供可视化
np.savez('wave_pred.npz', x=x_test.numpy(), t=t_test.numpy(), 
         u_pred=u_pred.numpy(), u_exact=u_exact.numpy())

运行方式

需要安装 torch 和 numpy。直接运行脚本即可,训练约5000轮(CPU约2~3分钟)。程序会打印每500轮的损失值和最终相对L2误差。

验证方法与判定标准

运行结束后观察输出:

  • 损失收敛:训练末期损失应降至1e-4以下(理想情况可达1e-5~1e-6);
  • 相对L2误差:与解析解 u(x,t)=sin(πx)cos(πt) 对比,误差小于0.01即认为PINN成功求解该波动方程;
  • 物理合理性:可绘制 t=0, 0.25, 0.5 时刻的波形,应分别对应 sin(πx)、0.707·sin(πx)、0,体现驻波特征。

常见问题

  • 初始速度条件难以收敛:若初始速度损失下降缓慢,可适当增大其权重(如乘以10),或增加 t=0 附近的配点密度。
  • 长时间域发散:本案例 t∈[0,1] 较短,若扩展时间域需增加配点数量或采用分段训练。
  • 边界强制技巧:乘以 x(1-x) 因子使网络输出在 x=0 和 x=1 处恒为零,避免了边界损失项的调参,但会略微增加训练难度,可配合适当增大网络宽度缓解。
PINN求解波动方程验证流程 输入 (x, t) 配点采样 PINN 网络 4层×64神经元 边界强制 u × x(1-x) 损失函数 L = L_eq + L_ic + L_iv 方程残差 | 初始位移 | 初始速度 u_tt - u_xx = 0 | u(x,0)=sin(πx) | u_t(x,0)=0 Adam优化 反向传播更新 验证:对比解析解 u = sin(πx)cos(πt),计算相对L2误差