一维无粘 Burgers 方程的激波形成问题:MacCormack 格式求解

一维无粘 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·tt = 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. 参考资料

  1. J.D. Anderson, Computational Fluid Dynamics: The Basics with Applications, McGraw-Hill, 1995(第 7 章:激波捕捉与 Burgers 方程算例)。
  2. R.J. LeVeque, Numerical Methods for Conservation Laws, Birkhäuser, 1992(守恒律数值方法与 Riemann 问题)。
相关推荐
HugoStudio_SWAN1 小时前
洛谷 B4500 / B4449 / B3843 凯撒密码、密码强度与密码合规——加密与安全的三道门
c++·学习·程序人生·算法·安全
雾里看山2 小时前
源码目录、构建目录与 out-of-source build
c++·软件构建·cmake
枫叶林FYL2 小时前
【群体智能集群控制工程实践】第3章 一致性协同算法
算法
禹凕2 小时前
滑动窗口算法实战指南
python·算法
DisonTangor2 小时前
【腾讯混元雪耻归来】 Hy4 preview:770B 参数 MoE 旗舰模型,1M 上下文全面开源
人工智能·算法·开源·aigc·腾讯云·腾讯云ai代码助手
明月_清风4 小时前
从二叉树到 B+ 树:一文搞懂工程中「树」的演化之道
数据结构·算法·go
渡我白衣4 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
如意猴4 小时前
【C++】001--C++入门(1)
开发语言·c++
hetao17338374 小时前
2026-09-01~09-04 hetao1733837 的刷题记录
c++·算法