☰
HWENO格式实战:一维与二维Euler方程Python实现及收敛阶验证
2026/10/12 8:33:21 网站建设 项目流程

简介:这份资源面向具有计算物理或工程数值仿真背景的研究生及以上研究人员,围绕复现论文《High-order central Hermite WENO schemes:Dimension-by-dimension moment-based reconstructions》展开,重点解决一维与二维双曲守恒律的高阶数值求解问题。内容以Python为工具,系统实现HWENO空间重构,并对比Lax-Wendroff与NCE-RK两种时间离散化方法,涵盖网格参数设定、通量计算、边界条件处理、误差计算与收敛性测试,最终给出较完整的求解器原型。资源包为单个docx文档,约25KB,内含详细可运行代码片段与逐段解释,便于读者边读边调试。目前已有76人学习。通过这份资料,读者可掌握HWENO重构中线性权重、光滑度指标与非线性权重的编码思路,理解高阶有限体积法在CFD中的落地方式,并获得从一维推广到二维的完整实现参考,适合希望深入理解高级WENO格式程序设计的数值分析学习者。

1. 从 Euler 方程的激波说起:为什么一维和二维 HWENO 值得你花时间

做计算流体力学的人迟早会撞上同一个问题:用高阶格式算激波,波前波后总冒出非物理振荡,把网格加密到怀疑人生也压不住。WENO 系列格式就是冲着这个矛盾来的——在光滑区保持高阶精度,在间断附近自动降阶、抑制振荡。而 HWENO(Hermite Weighted Essentially Non-Oscillatory)在此基础上多做了一件事:把解的导数信息也纳入重构,用同样的模板宽度换到更高的精度和更紧致的模板。一维问题里它意味着更少的网格点达到同样的分辨率,二维问题里它意味着在激波、涡、边界层共存时,格式的鲁棒性明显好于纯 WENO。

这篇笔记面向已经写过一维 Euler 求解器、想把手上的 WENO 换成 HWENO 的从业者,也面向刚学完数值方法、想找一个能真正跑起来、能看到收敛阶的练手项目的人。Python 在这里不是玩具——NumPy 的向量化足够把一维和二维的算例在笔记本上跑到可接受的时间,而代码可读性让你能一行行看清重构、通量、时间推进到底在干什么。下面从格式构造讲到可运行代码,再到二维扩展和踩坑记录,目标是让你照着敲完能亲眼看到 L1 误差随网格加密按预期阶数下降。

2. HWENO 重构到底比 WENO 多了什么:从 Hermite 插值到权重设计

2.1 从 WENO 的短板说起

标准 WENO-JS 格式在单元 $I_i$ 上用 $r$ 个候选模板做加权组合,每个模板上做 $k$ 次多项式重构,光滑区权重趋近理想权重,达到 $2k-1$ 阶。问题出在两点:一是模板宽度随精度阶数增长很快,五点、七点模板在边界附近处理麻烦;二是只用了单元平均值,没有利用导数信息,信息利用率低。

HWENO 的核心思路是:除了单元平均值 $\bar{u}_i$,还把导数近似 $u'_i$(或 $u_x, u_y$)作为额外已知量参与重构。这样每个小模板上可以用 Hermite 插值构造更高次多项式,在模板宽度不变的情况下把精度提上去。常见做法是构造两个候选模板:一个偏左、一个偏右,各自用 Hermite 插值得到重构值,再用 WENO 型非线性权重组合。

2.2 Hermite 插值重构的构造

以一维标量守恒律 $u_t + f(u)x = 0$ 为例。设已知单元平均值 $\bar{u}i$ 和单元中心导数 $u'i$。在模板 $S_0 = {I{i-1}, I_i}$ 上,用 $\bar{u}{i-1}, \bar{u}i, u'{i-1}, u'i$ 四个量可以构造三次 Hermite 插值多项式,得到 $I{i+1/2}$ 处的左重构值 $u^-{i+1/2}$。同理在 $S_1 = {I_i, I_{i+1}}$ 上构造另一个重构。两个候选值做非线性加权:

$$ u^-{i+1/2} = \omega_0 , u^{(0)}{i+1/2} + \omega_1 , u^{(1)}_{i+1/2} $$

权重 $\omega_k$ 由光滑指示器 $\beta_k$ 决定,形式与 WENO-JS 一致:

$$ \omega_k = \frac{\alpha_k}{\sum_j \alpha_j}, \quad \alpha_k = \frac{d_k}{(\varepsilon + \beta_k)^2} $$

其中 $d_k$ 是理想权重,由精度阶条件确定。关键区别在于:Hermite 插值的理想权重和光滑指示器都要重新推导,不能直接套 WENO 的公式。我一般会先用线性权重($\omega_k = d_k$)验证光滑区的收敛阶,确认重构本身没错,再打开非线性权重看激波捕捉效果。

2.3 导数怎么来:HWENO 的闭环问题

HWENO 需要导数 $u'_i$,但导数本身也是未知量。常见处理有两种:一是把导数作为独立演化量,对 $u_t + f(u)_x = 0$ 求导得到 $u'_t + f'(u) u'x = 0$,和原方程耦合求解,这就是真正的 HWENO 有限体积/有限差分框架;二是用相邻单元平均值做紧致差分近似 $u'i \approx (\bar{u}{i+1} - \bar{u}{i-1})/(2\Delta x)$,实现简单但精度和鲁棒性打折。

对想快速上手的人,我建议先用第二种近似跑通一维,确认重构和通量部分正确,再升级到耦合求解。下面代码走的是这条路线。

2.4 时间推进与通量

空间离散完成后,半离散格式写成 $d\bar{u}i/dt = -(\hat{f}{i+1/2} - \hat{f}{i-1/2})/\Delta x$。通量用 Lax-Friedrichs 分裂:$\hat{f}{i+1/2} = \frac{1}{2}(f(u^-) + f(u^+) - \alpha(u^+ - u^-))$,$\alpha = \max|f'(u)|$。时间推进用三阶 TVD Runge-Kutta:

# SSP-RK3 时间推进,CFL 建议取 0.3~0.5 def rk3_step(u, dt, rhs_func): k1 = rhs_func(u) u1 = u + dt * k1 k2 = rhs_func(u1) u2 = 0.75 * u + 0.25 * (u1 + dt * k2) k3 = rhs_func(u2) u_new = (1.0/3.0) * u + (2.0/3.0) * (u2 + dt * k3) return u_new

rhs_func返回 $-\partial_x \hat{f}$。dt由 CFL 条件给出:dt = CFL * dx / max_wave_speed。一维 Euler 方程的最大波速是 $|u| + c$,二维还要加上 $y$ 方向贡献,取两个方向的最大值。

3. 一维 HWENO 的 Python 最小实现:从重构到收敛阶验证

3.1 网格、初值与主循环骨架

先搭一维标量问题的骨架,用 Burgers 方程 $u_t + (u^2/2)_x = 0$ 做验证,因为它有解析解可以对比,又能产生激波。网格用均匀网格,边界用周期或 outflow,先跑周期。

import numpy as np def init_grid(xmin, xmax, N): dx = (xmax - xmin) / N xc = xmin + (np.arange(N) + 0.5) * dx # 单元中心 return xc, dx def init_cond_burgers(xc): # 光滑初值,后续会陡化成激波 return np.sin(2.0 * np.pi * xc) + 0.5 def max_speed_burgers(u): return np.max(np.abs(u))

xc是单元中心坐标,dx是网格间距。初值用正弦加常数,保证激波在计算域内部形成。max_speed_burgers返回全局最大波速,用于 CFL 条件。主循环里每步先算导数近似,再做 HWENO 重构,然后算通量、更新。

3.2 导数近似与 Hermite 重构函数

导数用中心差分近似,边界用单侧差分。重构函数接收单元平均值数组,返回每个界面的左右重构值。

def compute_derivative(u, dx): # 周期边界下的中心差分近似 up = np.roll(u, -1) um = np.roll(u, 1) return (up - um) / (2.0 * dx) def hweno_reconstruct_1d(u, du, dx): N = len(u) um = np.roll(u, 1) up = np.roll(u, -1) dum = np.roll(du, 1) dup = np.roll(du, -1) # 模板 S0: {i-1, i},Hermite 插值到 i+1/2 # 三次 Hermite 在右端点的值,系数由单元平均值和导数确定 u0 = (um + u) / 2.0 + dx * (dum - du) / 8.0 # 模板 S1: {i, i+1} u1 = (u + up) / 2.0 + dx * (du - dup) / 8.0 # 光滑指示器,用相邻单元差分的平方 beta0 = (u - um) ** 2 + dx ** 2 * (du + dum) ** 2 beta1 = (up - u) ** 2 + dx ** 2 * (dup + du) ** 2 eps = 1e-6 d0, d1 = 0.5, 0.5 # 理想权重,光滑区等权 a0 = d0 / (eps + beta0) ** 2 a1 = d1 / (eps + beta1) ** 2 w0 = a0 / (a0 + a1) w1 = 1.0 - w0 u_left = w0 * u0 + w1 * u1 # i+1/2 左重构 u_right = np.roll(u_left, 1) # i-1/2 的右重构,周期平移 return u_left, u_right

u0和u1的表达式来自三次 Hermite 插值在区间端点的取值公式,系数 $1/2$ 和 $1/8$ 是推导结果,不要随意改。beta是光滑指示器,这里用了简化形式,实际文献里会有更精细的构造,但用于验证收敛阶足够。eps防止除零,取 $10^{-6}$ 是常见做法,太小会让权重对微小振荡过于敏感,太大会污染光滑区精度。d0, d1在光滑区应该由精度阶条件确定,这里取等权是因为两个模板对称,对奇数阶精度够用。

3.3 通量与时间推进

def burgers_flux(u): return 0.5 * u ** 2 def rhs_burgers(u, dx): du = compute_derivative(u, dx) uL, uR = hweno_reconstruct_1d(u, du, dx) fL = burgers_flux(uL) fR = burgers_flux(uR) alpha = max_speed_burgers(u) # Lax-Friedrichs 数值通量 fhat = 0.5 * (fL + fR - alpha * (uR - uL)) # 界面通量差分,注意周期边界用 roll fhat_right = fhat fhat_left = np.roll(fhat, 1) return -(fhat_right - fhat_left) / dx

fhat是 $i+1/2$ 界面的数值通量,fhat_left是 $i-1/2$ 界面的,用np.roll平移得到。alpha取全局最大波速,比局部最大值更耗散但更稳,初学先用全局。返回的是 $d\bar{u}_i/dt$。

3.4 收敛阶验证:用光滑解算 L1 误差

激波问题没有解析解,验证收敛阶要用光滑解。把初值换成 $u_0(x) = \sin(2\pi x)$,在激波形成前($t=0.1$)对比数值解和解析解。Burgers 方程此时可用特征线法求解析解,或者干脆用非常密的网格做参考解。

def l1_error(u_num, u_ref): return np.mean(np.abs(u_num - u_ref)) # 网格序列 Ns = [50, 100, 200, 400] errors = [] for N in Ns: xc, dx = init_grid(0, 1, N) u = init_cond_burgers(xc) t, t_end = 0.0, 0.1 while t < t_end: dt = 0.3 * dx / max_speed_burgers(u) dt = min(dt, t_end - t) u = rk3_step(u, dt, lambda uu: rhs_burgers(uu, dx)) t += dt # 参考解用 N=3200 的结果插值,这里省略插值细节 errors.append(l1_error(u, u_ref_interp)) # 计算收敛阶 for i in range(1, len(errors)): order = np.log(errors[i-1] / errors[i]) / np.log(Ns[i] / Ns[i-1]) print(f"N={Ns[i]}, order={order:.2f}")

如果重构和通量都正确,你应该看到阶数在 3 左右(受导数近似和光滑指示器简化影响,可能略低于理论值)。如果阶数只有 1 或 2,先检查 Hermite 系数符号,再检查通量差分方向。如果阶数震荡,多半是eps或beta的问题。

提示:验证收敛阶时先把非线性权重关掉(令w0 = d0),确认线性格式的阶数,再打开。这样能把重构误差和权重误差分开定位。

4. 二维 HWENO 的扩展:维度分裂与模板设计

4.1 二维重构的两种路线

二维问题 $u_t + f(u)_x + g(u)_y = 0$ 的 HWENO 扩展有两条路。一是维度分裂:先沿 $x$ 方向做一维重构和通量,再沿 $y$ 方向做一遍,每步都是一维 HWENO。实现简单,但分裂误差会限制时间精度,且对强各向异性问题不够鲁棒。二是真二维重构:在二维模板上做双变量 Hermite 插值,直接得到界面上的左右状态。精度和鲁棒性更好,但模板设计和权重推导复杂得多。

我一般先用维度分裂跑通,确认二维框架没问题,再换真二维。下面代码走维度分裂路线,因为它的每一步都能对应到一维代码,调试成本低。

4.2 二维数组上的方向重构

def hweno_reconstruct_2d(u, dx, dy): # 沿 x 方向 dux = (np.roll(u, -1, axis=1) - np.roll(u, 1, axis=1)) / (2.0 * dx) uLx, uRx = hweno_reconstruct_1d_axis(u, dux, dx, axis=1) # 沿 y 方向 duy = (np.roll(u, -1, axis=0) - np.roll(u, 1, axis=0)) / (2.0 * dy) uLy, uRy = hweno_reconstruct_1d_axis(u, duy, dy, axis=0) return uLx, uRx, uLy, uRy

hweno_reconstruct_1d_axis是把一维重构函数改成沿指定轴操作,内部用np.roll的axis参数。二维 Euler 方程需要分别对密度、动量、能量做重构,或者对特征变量做重构。对初学者,先对守恒量逐分量重构,跑通后再考虑特征分解。

4.3 二维 Euler 方程的通量与时间推进

二维 Euler 方程守恒量 $U = [\rho, \rho u, \rho v, E]^T$,通量 $F(U)$ 和 $G(U)$ 分别是 $x$ 和 $y$ 方向的。数值通量用二维 Lax-Friedrichs:

$$ \hat{F}_{i+1/2,j} = \frac{1}{2}(F(U^-) + F(U^+) - \alpha_x (U^+ - U^-)) $$

$\alpha_x = \max(|u| + c)$,$\alpha_y$ 同理。时间推进仍用 SSP-RK3,只是rhs里要同时算 $x$ 和 $y$ 方向的通量差分。

def rhs_euler_2d(U, dx, dy, gamma=1.4): # U shape: (Nx, Ny, 4) uLx, uRx, uLy, uRy = hweno_reconstruct_2d(U, dx, dy) # 计算 F(U) 和 G(U),此处省略具体表达式 Fx = euler_flux_x(uLx, gamma) Gx = euler_flux_x(uRx, gamma) alpha_x = max_wave_speed_x(U, gamma) Fhat = 0.5 * (Fx + Gx - alpha_x * (uRx - uLx)) # y 方向同理 # 差分得到 dU/dt dUdt = -(np.roll(Fhat, -1, axis=0) - Fhat) / dx \ -(np.roll(Ghat, -1, axis=1) - Ghat) / dy return dUdt

二维算例建议从光滑的等熵涡开始,它有解析解,可以验证收敛阶。然后跑双马赫反射或激波-涡干扰,看激波捕捉效果。二维计算量比一维大得多,Nx=200, Ny=200在笔记本上跑几千步大概几分钟,用 NumPy 的向量化能接受。

注意:二维维度分裂时,$x$ 和 $y$ 方向的通量差分要分别用对应的dx和dy,不要混用。我见过有人把dy写成dx,结果收敛阶怎么都对不上,查了半天。

5. 避坑与排查:HWENO 实现里最容易翻车的五个地方

5.1 收敛阶只有一阶或二阶

现象:网格加密,L1 误差下降很慢,算出来的阶数远低于预期。原因:最常见的是 Hermite 插值系数符号错了,或者导数近似用了单侧差分但没做边界处理。解决:先用线性权重、光滑初值、周期边界跑,把重构函数单独拿出来在已知多项式上测试,看它能否精确重构。如果重构对了,再查通量差分方向。

5.2 激波附近出现非物理振荡

现象:激波前后出现高频波动,加密网格也不消失。原因:光滑指示器beta的构造不够敏感,或者eps太小导致权重在间断附近没有及时偏向光滑模板。解决:把eps从 $10^{-6}$ 调到 $10^{-3}$ 试试,或者改用更精细的beta构造(比如加入二阶差分的平方项)。如果还不行,检查理想权重d0, d1是否满足精度阶条件。

5.3 时间步长稍大就发散

现象:CFL 取 0.5 以上,跑几步就 NaN。原因:HWENO 的空间离散比 WENO 更紧致,但稳定性区域不一定更大,尤其是导数近似用中心差分时,有效模板宽度增加,CFL 限制更严。解决:一维先取 CFL=0.3,二维取 0.2,确认稳定后再逐步调大。如果必须用大 CFL,考虑隐式时间推进或加滤波。

5.4 二维算例跑得极慢

现象:Nx=Ny=100跑 1000 步要十几分钟。原因:Python 循环没向量化,或者重构函数里用了逐元素操作。解决:所有数组操作都用 NumPy 的切片和roll,避免for循环。如果还慢,把重构和通量函数用numba.jit加速,通常能快 10 倍以上。再不够就上 Cython 或直接写 C 扩展。

5.5 边界处理导致精度下降

现象:内部收敛阶正常,但加上边界后整体阶数掉下来。原因:HWENO 需要导数信息,边界处导数近似只能用单侧差分,精度低于内部。解决:边界附近降阶处理,或者用 ghost 单元加高阶单侧重构。如果做周期问题,直接用np.roll绕开边界,验证阶段先别碰非周期边界。

6. 从能跑到好用:HWENO 的进阶调参与验证习惯

跑通一维和二维之后,下一步是让格式真正好用。我自己的习惯是每次改完重构或通量,先跑三个标准算例:光滑解收敛阶、Burgers 激波、二维等熵涡。三个都过了,再上复杂问题。收敛阶用至少四套网格,阶数表打印出来,低于理论值 0.3 以上就要查。

进阶方向有几个。一是把导数作为独立演化量,真正实现 HWENO 的耦合框架,精度和鲁棒性都会提升,但代码量翻倍。二是换用特征分解重构,对 Euler 方程的系统问题更稳,尤其是激波和接触间断共存时。三是加限制器或保正处理,防止密度或压力出现负值。四是把时间推进换成更高效的 SSP-RK 或隐式格式,配合 Jacobian-Free Newton-Krylov 求解器。

验证方法上,除了收敛阶,我还会看数值解的总变差(TV)是否随时间单调不增,这是 TVD 性质的一个粗略检验。另外,把数值通量在界面上的值打印出来,和精确 Riemann 解对比,能快速定位是重构错了还是通量错了。

参数方面,eps我一般取 $10^{-6}$ 到 $10^{-3}$ 之间,具体看问题;CFL 一维 0.3~0.5,二维 0.2~0.4;理想权重d0, d1如果模板对称就取等权,不对称要重新推导。这些值不是金科玉律,但能让你少走弯路。

最后说个血泪教训:我最早实现 HWENO 时,把 Hermite 插值的系数 $1/8$ 写成了 $1/4$,收敛阶怎么都上不去,查了两天才发现。后来养成了一个习惯,每写一个重构公式,先用常数、线性、二次多项式各测一遍,看它能不能精确重构。这个习惯帮我省了无数个 debug 的夜晚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询