一维无粘 Burgers 方程是研究可压缩流激波捕捉格式时最经典的一维原型问题。本文完整介绍"初值间断如何演化成一条向右传播的激波"这一经典算例(出处:J.D. Anderson《Computational Fluid Dynamics: The Basics with Applications》,第 7 章),内容包括:问题描述、CFD 理论背景、MATLAB 与 C++ 源码、运行结果数据与可视化图像。
1. 问题描述
1.1 控制方程
在一维均匀网格上求解非线性双曲型方程(无黏 Burgers 方程):
∂u/∂t + ∂/∂x (u²/2) = 0, 0 ≤ x ≤ 2
其中 f(u) = u²/2 为通量,未知量 u 可理解为"速度"(无黏 Burgers 方程与可压缩 Euler 方程的非线性结构同源,是后者的标量原型)。
1.2 初始条件(Riemann 问题)
初值在 x = 1 处存在一个间断:
u(x, 0) = 1.0, 0 ≤ x ≤ 1
u(x, 0) = 0.5, 1 < x ≤ 2
1.3 边界条件(Dirichlet)
u(0, t) = 1.0, u(2, t) = 0.5
1.4 理论解:Rankine--Hugoniot 激波位置
左侧高速流体(u = 1)不断追赶右侧低速流体(u = 0.5),初值间断将演化成一条向右传播的激波。由 Rankine--Hugoniot 关系,激波速度为两侧状态的平均特征速度:
s = (a(u_L) + a(u_R)) / 2 = (1 + 0.5) / 2 = 0.75
激波位置随时间线性右移:
x_s(t) = 1 + s·t = 1 + 0.75·t
1.5 数值参数
计算域 L = 2
网格节点 nx = 81,Δx = L/(nx-1) = 0.025
时间步数 nt = 120,Δt = 0.0125,模拟至 t = 1.5 s
CFL 数 ν = max|u|·Δt/Δx = 1×0.0125/0.025 = 0.5(≤ 1,稳定)
2. CFD 理论背景
2.1 无粘 Burgers 方程:最简的非线性双曲守恒律
一维守恒律的标准形式为
∂u/∂t + ∂f(u)/∂x = 0
当通量取 f(u) = u²/2 时即为一维无粘 Burgers 方程。它是非线性 的:即使初值光滑,特征线也会在有限时间内相交,形成梯度趋于无穷的间断(激波,即弱解)。因此它是理解可压缩 Euler 方程中"激波捕捉"问题的最小原型,也是 Anderson 教材里从线性对流方程过渡到非线性方程组(Euler 方程)的"桥梁算例"。
本题初值是典型的 Riemann 问题:左右两个常状态由一个间断隔开。两侧特征速度 a(u) = u 分别为 1 与 0.5(左侧更快),因此该间断必然演化成激波而非稀疏波(若左侧慢于右侧,则退化为稀疏波)。激波传播速度取两侧状态的算术平均:
s = 0.75
2.2 MacCormack 预估--校正格式
MacCormack 格式是一种 显式、二阶精度的时间/空间离散,实现非常简单:先做一次"后差"预估,再做一次"前差"校正并取平均。
记 F = u²/2。预估步(后差通量):
F_i^n = 0.5·(u_i^n)²
ū_i = u_i^n − (Δt/Δx)·(F_i^n − F_{i−1}^n)
F̄_i = 0.5·(ū_i)²
校正步(前差通量):
u_i^{n+1} = 0.5·[ u_i^n + ū_i − (Δt/Δx)·(F̄_{i+1} − F̄_i) ]
在内点区间(i = 2, ..., nx−1 做预估,i = 1, ..., nx−1 做校正)之外,两端节点每步被 Dirichlet 边界值 1.0 / 0.5 重新赋值("钉扎")。整个时间步推进可紧凑地写成整向量形式:
matlab
ubar = [1.0, u(2:80) - (dt/dx)*(F(2:80)-F(1:79)), 0.5]; % 预估(内点 2..80,边界钉扎)
Fbar = 0.5*(ubar.*ubar);
unew = 0.5*(u(1:80) + ubar(1:80) - (dt/dx)*(Fbar(2:81)-Fbar(1:80))); % 校正(内点 1..80)
u = [unew, 0.5];
2.3 CFL 稳定性条件
对显式格式,时间步必须满足 CFL 条件:
ν = max_i |u_i| · Δt / Δx ≤ 1
本题取 Δt = 0.0125、Δx = 0.025,得 ν = 0.5,格式稳定,程序运行全程不发散。
2.4 数值耗散与色散(Gibbs 型振荡/过冲)
MacCormack 格式二阶精度、不含人工耗散 ,在强间断附近会产生典型的小幅 Gibbs 型振荡/过冲 (Anderson 称之为色散误差)。尤其当激波在 t ≈ 1.33 s 后离开右边界,而右边界仍被钉扎在 u = 0.5 时,末段节点会积累较明显的过冲(见第 5 节数据)。这正是后来引入人工黏性、通量限制器(TVD)以及 Riemann 求解器(Godunov 族)等技术的动机。
3. MATLAB 源码
以下给出该算例的完整 MATLAB 实现:网格与参数 → MacCormack 时间推进(并保存 t = 0 / 0.25 / 0.5 / 1.0 / 1.5 五个快照)→ 多时刻剖面绘图 → 激波位置数值检测并与 Rankine--Hugoniot 理论值对比。可直接在 MATLAB / Octave 中运行。
matlab
%% ==========================================================
% 一维无粘 Burgers 方程 ------ 激波形成问题
% MacCormack 预估-校正格式
% 参考: J.D. Anderson, CFD: The Basics with Applications, Ch.7
%% ==========================================================
clear; clc; close all;
%% 1. 网格与时间参数
L = 2; % 计算域长度
nx = 81; % 网格节点数
nt = 120; % 时间步数
dx = L/(nx-1); % 空间步长
dt = 0.0125; % 时间步长
x = linspace(0,L,nx); % 网格坐标
CFL = max(1)*dt/dx; % 初始 CFL 数
fprintf('CFL = %.3f (<=1 即稳定)\n',CFL);
%% 2. 初始条件(Riemann 问题)
u = 0.5*ones(1,nx);
u(x<=1) = 1.0;
% 保存若干时刻用于画图
snap_t = [0 0.25 0.5 1.0 1.5]; % 想要观察的物理时刻
snap_u = cell(numel(snap_t),1);
snap_u{1} = u; % t = 0
isnap = 1; t_now = 0;
%% 3. 时间推进(MacCormack)
for n = 1:nt
F = 0.5*u.^2; % 当前时刻通量
% ---- 预估步:空间后差 ----
ubar = u;
ubar(2:end) = u(2:end) - dt/dx*(F(2:end)-F(1:end-1));
ubar(1) = 1.0; ubar(end) = 0.5; % 边界保持
Fbar = 0.5*ubar.^2; % 预估通量
% ---- 校正步:空间前差 ----
unew = u;
unew(1:end-1) = 0.5*( u(1:end-1) + ubar(1:end-1) ...
- dt/dx*(Fbar(2:end)-Fbar(1:end-1)) );
unew(end) = 0.5;
% 更新
u = unew; t_now = t_now + dt;
% 保存特定时刻的解
if isnap < numel(snap_t) && abs(t_now - snap_t(isnap+1)) < dt/2
isnap = isnap + 1;
snap_u{isnap} = u;
end
end
%% 4. 结果可视化
figure('Color','w','Position',[100 100 700 450]);
hold on;
colors = lines(numel(snap_t));
for k = 1:numel(snap_t)
plot(x, snap_u{k}, 'LineWidth',1.8, 'Color',colors(k,:), ...
'DisplayName',sprintf('t = %.2f',snap_t(k)));
end
grid on; box on;
xlabel('x'); ylabel('u(x,t)');
title('一维 Burgers 方程激波形成');
legend('Location','northeast'); ylim([0.3 1.1]);
%% 5. 激波位置与理论值对比(Rankine--Hugoniot)
xs_num = x( u >= (1.0+0.5)/2 & [diff(u)<0 , 0] ); % 数值激波位置
xs_theory = 1 + 0.75*max(snap_t); % 理论位置
fprintf('\n t=%.2f 时:数值激波位置 x_s=%.3f, 理论值 x_s=%.3f\n', ...
snap_t(end), xs_num(1), xs_theory);
4. C++ 源码
本节给出与上述 MATLAB 数值逻辑完全等价的 C++ 实现,便于工程集成与批量运行。工程由 5 个文件组成:
| 文件 | 作用 |
|---|---|
burgers_vec.h |
入口函数声明 |
burgers_vec.cpp |
数值求解主体(时间推进 + 激波检测) |
main.cpp |
程序入口 main() |
rtwtypes.h |
跨平台基础类型定义 |
CMakeLists.txt |
可选的 CMake 构建脚本 |
以上 5 个文件构成一个完整工程,入口函数为 main(),其输出见第 5 节。
4.1 burgers_vec.h
cpp
/*
* burgers_vec.h
* 一维无粘 Burgers 方程激波形成问题(MacCormack 预估-校正)------头文件
*/
#ifndef BURGERS_VEC_H
#define BURGERS_VEC_H
#include <cmath>
#include <cfloat>
#include <cstddef>
#include <cstdlib>
#include <cstdio>
#include "rtwtypes.h"
#include <vector>
#include <string>
/* 入口函数声明 */
extern void burgers_vec();
#endif
4.2 burgers_vec.cpp
cpp
/*
* burgers_vec.cpp
* 一维无粘 Burgers 方程激波形成问题(MacCormack 预估-校正)------数值实现
* 与第 3 节 MATLAB 实现的数值逻辑一致:
* 预估步后差、校正步前差、端点每步钉扎边界值 1.0 / 0.5
*/
#include "burgers_vec.h"
#include <cmath>
void burgers_vec()
{
double L; /* 计算域长度 */
int nx; /* 网格节点数 */
int nt; /* 时间步数 */
double dx; /* 空间步长 */
double dt; /* 时间步长 */
double x[81]; /* 网格坐标 */
double CFL; /* CFL 数 */
double u[81]; /* 解向量 */
int n; /* 时间步循环 */
double F[81]; /* 当前通量 */
double ubar[81]; /* 预估解 */
double Fbar[81]; /* 预估通量 */
double unew[80]; /* 校正后的内点 */
double xs_num; /* 数值激波位置 */
int found; /* 是否已找到 */
int i;
double xs_theory; /* 理论激波位置 */
L = 2.0;
nx = 81;
nt = 120;
dx = L / (nx - 1); /* = 0.025 */
dt = 0.0125;
double tv1 = (L - 0.0) / (double)(nx - 1); /* linspace(0,L,nx) 步长 */
for (int k = 0; k < 81; k++) {
x[k] = 0.0 + (double)k * tv1;
}
CFL = dt / dx;
printf("CFL = %.3f (<=1 stable)\n", CFL);
/* 初始条件:u = 0.5 全域;x <= 1 处再加 0.5 -> 1.0 */
for (int k = 0; k < 81; k++) {
u[k] = 0.5;
}
for (int k = 0; k < 81; k++) {
u[k] = u[k] + 0.5 * (x[k] <= 1.0);
}
/* MacCormack 时间推进 */
for (n = 1; n <= 120; n++) {
/* 当前通量 F = u.^2 / 2 */
for (int k = 0; k < 81; k++) {
F[k] = 0.5 * (u[k] * u[k]);
}
/* 预估步:后差(内点 k=1..79),两端钉扎 */
ubar[0] = 1.0;
for (int k = 1; k < 80; k++) {
ubar[k] = u[k] - (dt / dx) * (F[k] - F[k - 1]);
}
ubar[80] = 0.5;
/* 预估通量 */
for (int k = 0; k < 81; k++) {
Fbar[k] = 0.5 * (ubar[k] * ubar[k]);
}
/* 校正步:前差(内点 k=0..79),最右端钉扎 */
for (int k = 0; k < 80; k++) {
unew[k] = 0.5 * ((u[k] + ubar[k]) - (dt / dx) * (Fbar[k + 1] - Fbar[k]));
}
for (int k = 0; k < 80; k++) {
u[k] = unew[k];
}
u[80] = 0.5;
}
/* 激波位置数值检测:第一个满足 u(i)>=0.75 且 u(i+1)<u(i) 的网格点 */
xs_num = 0.0;
found = 0;
for (i = 1; i < 81; i++) { /* 内点 1..80 */
if (u[i] >= 0.75) {
if (u[i + 1] < u[i]) {
if (found == 0) {
xs_num = x[i];
found = 1;
}
}
}
}
xs_theory = 1.0 + 0.75 * 1.5; /* x_s(1.5) = 1 + 0.75×1.5 */
printf("t = %.2f: numerical x_s = %.3f, theory x_s = %.3f\n",
1.5, xs_num, xs_theory);
}
说明:C++ 中数组下标为 0-based,对应 MATLAB 的 1-based 节点偏移 1;预估/校正均只在 79~80 个内点上计算,两端节点每步由边界值重写,因此与 MATLAB 切片写法逐点等价。
x <= 1的逻辑判断在 C++ 中返回 0/1,与 MATLAB 中(x<=1)用作数值一样。
4.3 main.cpp
cpp
/* main.cpp ------ 程序入口 */
#include "burgers_vec.h"
int main()
{
burgers_vec();
return 0;
}
4.4 rtwtypes.h
cpp
/* rtwtypes.h ------ 跨平台基础类型定义 */
#ifndef RTWTYPES_H
#define RTWTYPES_H
#include <cstddef>
#include <cstdint>
typedef double real_T;
typedef float real32_T;
typedef int int32_T;
typedef int16_t int16_T;
typedef int8_t int8_T;
typedef uint32_t uint32_T;
typedef uint16_t uint16_T;
typedef uint8_t uint8_T;
typedef uint8_t boolean_T;
typedef char char_T;
#endif /* RTWTYPES_H */
4.5 CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.10)
project(burgers_solver CXX)
set(CMAKE_CXX_STANDARD 14)
add_executable(burgers_app
main.cpp
burgers_vec.cpp
)
target_include_directories(burgers_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
5. 运行结果与数据
5.1 程序输出
运行第 4 节的 C++ 实现(数值格式与第 3 节 MATLAB 实现一致,两者关键数值输出相同)得到:
CFL = 0.500 (<=1 stable)
t = 1.50: numerical x_s = 0.450, theory x_s = 2.125
即:
CFL = dt/dx = 0.0125 / 0.025 = 0.500 显式格式稳定(ν ≤ 1)
理论激波 x_s(t=1.5) = 1 + 0.75×1.5 = 2.125 Rankine--Hugoniot 关系
数值检测 x_s = 0.450 判据第一个命中的网格点(见 5.3 说明)
5.2 各快照时刻剖面数据
各时刻剖面的最小/最大值(max u 已包含 MacCormack 格式的过冲):
| 时刻 t (s) | min u | max u(含过冲) | 形态 |
|---|---|---|---|
| 0.00 | 0.5 | 1.000000 | 阶跃初值,x = 1 处间断(1.0/0.5) |
| 0.25 | 0.5 | 1.157157 | 间断处出现数值抹平与小幅振荡 |
| 0.50 | 0.5 | 1.130594 | 波形左陡右缓、整体向右推进 |
| 1.00 | 0.5 | 1.131811 | 波形逼近右边界 |
| 1.50 | 0.5 | 1.331206 | 激波已离开右边界,末段节点出现明显过冲 |
5.3 结果说明
(1)CFL 与稳定性。 ν = 0.5 ≤ 1 满足显式格式的稳定条件,程序运行全程不发散,解的单调推进行为与理论一致。
(2)激波位置检测为何输出 0.450,而不是理论值 2.125?
理论激波位置 x_s(t) = 1 + 0.75·t 在 t = 1.5 时为 2.125,已大于右边界 x = 2。这意味着激波其实在
t_exit ≈ (2 − 1) / 0.75 ≈ 1.333 s
就已经离开计算域,t = 1.5 时理论上激波位于域外。
而程序中的检测判据只能返回网格坐标(最大 2.0);同时,t = 1.5 时 u = 1 的高位平台上布满约 1e-12 量级的浮点噪声,扫描会先在这些噪声处命中 u(i+1) < u(i),于是输出了第一个命中点 x = 0.450(第 19 号节点)。也就是说:激波位置判据本身受"网格坐标上限 + 平台噪声"影响,输出的 0.450 不能直接当作物理激波位置 ;物理上激波以 s = 0.75 匀速右移并在约 1.33 s 时离开计算域。若需在激波出域后继续跟踪,应把判据限定在波形前缘(靠近右端)附近,或改为记录每个时间步的传播前沿。
(3)MacCormack 格式的过冲。 t = 1.5 时末段节点最大过冲 u_max − 1 ≈ 0.331。这是二阶无耗散格式在"激波推出边界 + Dirichlet 钉扎"条件下的典型色散振荡,与 2.4 节的理论背景吻合;在更长时间积分或更粗网格下会变得更明显,工程中常配合人工黏性 / 通量限制器使用。
6. 可视化结果
以下图像由运行数据绘制:左图为 t = 0 / 0.25 / 0.5 / 1.0 / 1.5 五个时刻的整场剖面,右图为 t = 1.5 时刻最终剖面与激波检测结果。
图 1:不同时刻的速度剖面(激波的形成与传播)

图 2:t = 1.5 s 最终剖面与激波检测

图中可见完整物理过程:初始阶跃 → 间断处轻微的 Gibbs 型振荡 → 波形整体向右推进并逐渐逼近右边界;t = 1.5 时高位平台几乎贯穿全域,理论激波位置(2.125)已超出右边界(图中以竖线标出),离散判据先命中了平台噪声点(x = 0.450)。
7. 小结
- 该算例完整展示了非线性双曲守恒律从初值间断 → 激波形成 → 传播/离开计算域的物理过程,是理解可压流激波捕捉格式(TVD、WENO、Godunov 等)最直接的入门原型。
- MacCormack 格式实现简洁、二阶精度,但本身不含耗散,在强间断处会产生振荡/过冲------这正是后续引入人工黏性、通量限制器或 Riemann 求解器的动机。
- 通过 MATLAB 与 C++ 两套等价实现,可以清晰对照向量化切片写法与逐点循环写法的对应关系;两套代码的关键数值输出完全一致,结果可复现、可审计。
8. 参考资料
- J.D. Anderson, Computational Fluid Dynamics: The Basics with Applications, McGraw-Hill, 1995(第 7 章:激波捕捉与 Burgers 方程算例)。
- R.J. LeVeque, Numerical Methods for Conservation Laws, Birkhäuser, 1992(守恒律数值方法与 Riemann 问题)。