【学习记录】k-Wave 时间步长、CFL 稳定性与参数实验全解析
跑 k-Wave 仿真时,
dt该取多大?Nt该设多少?alpha_power到底是什么?改声速、改源、改传感器会发生什么?本文系统整理 k-Wave 中与时间步长和稳定性相关的全部核心知识点:CFL 条件(1D/2D/3D 的理论极限与推荐值 0.3)、dt的计算公式、Nt的选取逻辑、setTime的作用、kspaceFirstOrder2D的内部流程、幂律吸收模型alpha_power的物理含义,以及四个参数实验(改声速、改传感器、改源、故意超标 CFL)的预期现象与原因。保留全部知识点,附完整代码。
📌 目录
- [CFL 稳定性条件](#CFL 稳定性条件)
- [k-Wave 里怎么算 dt](#k-Wave 里怎么算 dt)
- [为什么 sqrt(3) 是 3D 的因子](#为什么 sqrt(3) 是 3D 的因子)
- [Nt 的选取逻辑](#Nt 的选取逻辑)
- [setTime 的作用](#setTime 的作用)
- [kspaceFirstOrder2D 内部做了什么](#kspaceFirstOrder2D 内部做了什么)
- 背后的物理/数学
- [alpha_power 详解](#alpha_power 详解)
- 四个参数实验
- 总结
一、CFL 稳定性条件
CFL 全称 Courant--Friedrichs--Lewy 条件,是数值求解波动方程时的一个稳定性约束。
一句话:在一个时间步内,声波传播的距离不能超过一个网格间距。
如果波在一个时间步里跨过了好几个网格,数值格式就"追不上"波的传播,计算会发散。
CFL 定义:
CFL = c × d t d x \text{CFL} = \frac{c \times dt}{dx} CFL=dxc×dt
稳定性要求:
| 维度 | 理论极限 | 说明 |
|---|---|---|
| 1D | CFL ≤ 1 | 1D 理论极限 |
| 2D | CFL ≤ 1/√2 | 2D 理论极限 |
| 3D | CFL ≤ 1/√3 | 3D 理论极限 |
💡 实际使用中,k-Wave 推荐 CFL = 0.3,留足安全余量。
二、k-Wave 里怎么算 dt
公式:
d t ≤ CFL × d x c × 3 dt \leq \frac{\text{CFL} \times dx}{c \times \sqrt{3}} dt≤c×3 CFL×dx
参数含义:
| 参数 | 含义 | 值 |
|---|---|---|
| CFL | 库朗数,一般取 0.3 | 0.3 |
| dx | 最小网格间距 | 0.1e-3 m |
| c | 最大声速 | 1500 m/s |
| sqrt(3) | 3D 对角线因子(2D 用 sqrt(2)) | --- |
计算示例:
d t max = 0.3 × 0.1 × 10 − 3 1500 × 1.732 ≈ 1.15 × 10 − 8 s = 0.0115 µs dt_{\max} = \frac{0.3 \times 0.1 \times 10^{-3}}{1500 \times 1.732} \approx 1.15 \times 10^{-8} \text{ s} = 0.0115 \text{ µs} dtmax=1500×1.7320.3×0.1×10−3≈1.15×10−8 s=0.0115 µs
三、为什么 sqrt(3) 是 3D 的因子
波在 3D 网格里可以沿对角线传播 。对角线长度是 dx × sqrt(3)。为了保证波在一个时间步内传播不超过一个对角线网格,dt 要除以 sqrt(3)。
| 维度 | 公式 |
|---|---|
| 1D | dt ≤ CFL × dx / c |
| 2D | dt ≤ CFL × dx / (c × sqrt(2)) |
| 3D | dt ≤ CFL × dx / (c × sqrt(3)) |
💡 注意 :如果用的是 2D,严格来说应该用
sqrt(2)。但用sqrt(3)更保守,也能跑。
四、Nt 的选取逻辑
matlab
Nt_A = 2000; % 设定时间步数为 2000 步
含义:设定时间步数为 2000 步。
为什么需要 :k-Wave 是时域仿真,需要知道仿真跑多少步。Nt 越大,仿真时间越长,计算越久。
怎么选 Nt :Nt 由总仿真时长和时间步长共同决定:
N t = t end d t Nt = \frac{t_{\text{end}}}{dt} Nt=dttend
t_end:希望仿真持续多久(物理时间)。dt:每一步的时间步长。
示例计算:
dt_stable 由 CFL 条件算出(约 1.15e-8 s),Nt_A = 2000,所以总仿真时长是:
t end = 2000 × 1.15 × 10 − 8 ≈ 2.3 × 10 − 5 s = 23 µs t_{\text{end}} = 2000 \times 1.15 \times 10^{-8} \approx 2.3 \times 10^{-5} \text{ s} = 23 \text{ µs} tend=2000×1.15×10−8≈2.3×10−5 s=23 µs
够不够用? 取决于声波需要多久到达传感器。
源在 (30, 64),传感器在 (90, 64),距离 60 × 0.1 mm = 6 mm。声速 1500 m/s 时,传播时间约:
6 × 10 − 3 1500 = 4 µs \frac{6 \times 10^{-3}}{1500} = 4 \text{ µs} 15006×10−3=4 µs
23 µs 足够。
五、setTime 的作用
matlab
kgrid.setTime(Nt_A, dt_stable);
含义 :把时间步数和时间步长写入 kgrid 对象。
为什么需要 :kWaveGrid 对象默认只有空间信息(Nx、dx 等),没有时间信息。setTime 是给网格"挂上"时间轴。
⚠️ 不调用会怎样 :如果不调用
setTime,k-Wave 会在kspaceFirstOrder2D内部自动算一个dt和Nt。自动算的dt满足 CFL,但你可能无法控制总时长,也不方便做"固定dt对比不同声速"的实验。
dt_stable 从哪来:
matlab
dt_stable = cfl_stable * dx_min / (c * sqrt(3));
这是 CFL 条件算出的稳定时间步。setTime 把它写进 kgrid,后续仿真就用这个 dt。
💡 关键点 :
setTime只设置参数,不跑仿真。它只是告诉kgrid:"时间轴长这样"。
六、kspaceFirstOrder2D 内部做了什么
matlab
sensor_data_A = kspaceFirstOrder2D(kgrid, medium, source, sensor);
含义:调用 k-Wave 的 2D 求解器,跑仿真,返回传感器记录的数据。
四个输入参数:
| 参数 | 含义 |
|---|---|
kgrid |
计算网格(含空间和时间信息) |
medium |
传播介质(声速、密度、衰减) |
source |
声源(初始压力或边界驱动) |
sensor |
传感器(记录位置和记录内容) |
返回值 :sensor_data_A 是传感器记录的数据。尺寸是 [传感器点数, Nt]。
💡 场景示例 :
sensor.mask(90, 64) = 1,只有 1 个传感器点,所以sensor_data_A尺寸是[1, 2000]。
内部流程:
- 检查输入参数(CFL 是否满足、PML 位置、尺寸是否匹配)。
- 初始化声场(
source.p0作为初始压力)。 - 时间循环:每一步用 k-space 伪谱法推进声场。
- 每一步在
sensor.mask位置记录声压。 - 返回记录的数据。
七、背后的物理/数学
7.1 k-space 伪谱法
k-Wave 的核心求解器使用 k-space 伪谱法 。名字里的 "k-space" 指的是波数空间。k-Wave 在波数空间里计算空间导数,用 FFT 实现,比有限差分精度高。
求解的耦合一阶声学方程:
∂ p ∂ t = − ρ c 2 ∇ ⋅ v \frac{\partial p}{\partial t} = -\rho c^2 \nabla \cdot \mathbf{v} ∂t∂p=−ρc2∇⋅v
∂ v ∂ t = − 1 ρ ∇ p \frac{\partial \mathbf{v}}{\partial t} = -\frac{1}{\rho} \nabla p ∂t∂v=−ρ1∇p
其中 p 是声压,v 是质点速度,ρ 是密度,c 是声速。
伪谱法的做法 :在每一步时间推进中,用 FFT 把 p 和 v 变换到波数空间,在那里计算空间导数,再变换回来。
7.2 初始值问题 vs 边界驱动问题
| 类型 | 源定义 | 物理含义 |
|---|---|---|
| 初始值问题(IVP) | source.p0 |
定义初始压力分布,仿真从 t=0 开始让压力自然扩散。本章示例属于这一类 |
| 边界驱动问题 | source.p |
定义边界上的压力或速度驱动,模拟换能器持续发射。经颅仿真属于这一类 |
💡 两者的区别 :IVP 的声源只在
t=0存在,之后只有传播;边界驱动的声源在每个时间步都注入能量。
八、alpha_power 详解
alpha_power 是 k-Wave 里描述声吸收如何随频率变化的参数。它决定了介质对不同频率声波的"吸收力度"差异。
8.1 它出现在哪
k-Wave 里描述吸收需要两个参数:
matlab
medium.alpha_coeff = 0.75; % 吸收系数
medium.alpha_power = 1.5; % 吸收随频率的幂次
两者一起决定某个频率下的吸收值:
α ( f ) = alpha_coeff × ( f 1 MHz ) alpha_power \alpha(f) = \text{alpha\_coeff} \times \left(\frac{f}{1 \text{ MHz}}\right)^{\text{alpha\_power}} α(f)=alpha_coeff×(1 MHzf)alpha_power
单位是 dB/(MHz^y cm),其中 y = alpha_power。
8.2 alpha_power 的物理含义
alpha_power 是吸收与频率的幂律指数 。它描述:频率翻倍时,吸收增加多少倍。
| alpha_power | 物理含义 | 典型介质 |
|---|---|---|
| 1.0 | 吸收与频率线性正比 | 部分软组织 |
| 1.1--1.5 | 吸收随频率次线性增长 | 多数软组织、颅骨 |
| 1.5--2.0 | 吸收随频率超线性增长 | 水(约 2) |
举例 :alpha_power = 1.5,alpha_coeff = 0.75:
| 频率 | 吸收值 |
|---|---|
| 1 MHz | α = 0.75 × (1)^1.5 = 0.75 dB/cm |
| 2 MHz | α = 0.75 × (2)^1.5 ≈ 2.12 dB/cm |
| 4 MHz | α = 0.75 × (4)^1.5 ≈ 6.0 dB/cm |
💡 频率翻倍,吸收增加约
2^1.5 ≈ 2.83倍。
8.3 为什么 k-Wave 要求它是全局标量
k-Wave 的 alpha_power 只能是一个数,不能是逐点变化的矩阵。
原因 :k-Wave 求解吸收项时,用的是幂律吸收模型。这个模型的数学推导假设 alpha_power 在整个介质里是同一个值。如果逐点变化,方程不再成立,k-Wave 无法求解。
💡 实操建议 :多种介质一起仿真时选择一个折中值,通过
alpha_coeff调节到目标频率下的正确吸收。
九、四个参数实验
实验 1:改声速,看斜线斜率
操作 :把 medium.sound_speed 从 1500 改成 3000,重新跑。
预期:图 2 的斜线变陡。因为声速翻倍,声波到达传感器的时间减半,斜线斜率变陡。
为什么 :时空图的横轴是时间,纵轴是传感器位置。斜线斜率 = 传播距离 / 传播时间 = 声速。
matlab
clearvars;
%% ===== 1. Grid =====
Nx = 128; Ny = 128;
dx = 0.1e-3; dy = 0.1e-3;
kgrid = kWaveGrid(Nx, dx, Ny, dy);
%% ===== 2. Medium =====
medium.sound_speed = 1500;
medium.alpha_coeff = 0.75;
medium.alpha_power = 1.5;
%% ===== 3. Source =====
disc_1 = 5 * makeDisc(Nx, Ny, 50, 50, 8);
disc_2 = 3 * makeDisc(Nx, Ny, 80, 60, 5);
source.p0 = disc_1 + disc_2;
%% ===== 4. Sensor =====
sensor_radius = 4e-3;
num_sensor_points = 50;
sensor.mask = makeCartCircle(sensor_radius, num_sensor_points);
%% ===== 5. 跑仿真 =====
sensor_data = kspaceFirstOrder2D(kgrid, medium, source, sensor);
% 检查数值
fprintf('sensor_data: max=%.3e, min=%.3e, size=[%d, %d]\n', ...
max(sensor_data(:)), min(sensor_data(:)), size(sensor_data));
%% ===== 6. 可视化 =====
% 图 1:初始压力和传感器分布
figure;
imagesc(kgrid.y_vec * 1e3, kgrid.x_vec * 1e3, ...
source.p0 + cart2grid(kgrid, sensor.mask), [-1, 1]);
colormap(getColorMap);
ylabel('x-position [mm]');
xlabel('y-position [mm]');
axis image;
% 图 2:传感器时空图
figure;
imagesc(sensor_data, [-1, 1]);
colormap(getColorMap);
ylabel('Sensor Position');
xlabel('Time Step');
colorbar;
% 图 3:时域波形(从 kgrid 取 Nt 和 dt)
t_phys = (0:kgrid.Nt-1) * kgrid.dt * 1e6; % 物理时间 [µs]
mean_waveform = mean(sensor_data, 1); % 50 个点的平均
figure;
plot(t_phys, mean_waveform / max(abs(mean_waveform)), 'b', 'LineWidth', 1.5);
xlabel('Time [\mus]');
ylabel('Normalized Pressure');
title('Mean Waveform over 50 Sensor Points (c = 1500 m/s)');
grid on;
xlim([0, t_phys(end)]);


实验 2:改传感器为二值矩阵
操作 :把 sensor.mask = makeCartCircle(...) 改成:
matlab
sensor.mask = zeros(Nx, Ny);
sensor.mask(64, 64) = 1; % 网格中心放一个点
重新跑 kspaceFirstOrder2DG。
matlab
clearvars;
%% ===== 1. Grid =====
Nx = 128; Ny = 128;
dx = 0.1e-3; dy = 0.1e-3;
kgrid = kWaveGrid(Nx, dx, Ny, dy);
%% ===== 2. Medium =====
medium.alpha_coeff = 0.75;
medium.alpha_power = 1.5;
%% ===== 3. Source =====
source.p0 = zeros(Nx, Ny);
source.p0(30, 64) = 1;
%% ===== 4. Sensor =====
sensor.mask = zeros(Nx, Ny);
sensor.mask(90, 64) = 1;
%% ===== 5. 时间步:满足 CFL =====
c_max = 3000;
cfl = 0.3;
dt = cfl * dx / (c_max * sqrt(3)); % ≈ 5.8e-9 s
Nt = 2000; % 总时长 ≈ 11.6 µs
kgrid.setTime(Nt, dt);
fprintf('dt = %.2e s, Nt = %d, tEnd = %.2f us\n', dt, Nt, Nt*dt*1e6);
%% ===== 6. 跑两种情况 =====
medium.sound_speed = 1500;
sensor_data_A = kspaceFirstOrder2D(kgrid, medium, source, sensor);
medium.sound_speed = 3000;
sensor_data_B = kspaceFirstOrder2D(kgrid, medium, source, sensor);
%% ===== 7. 检查数值 =====
fprintf('A: max=%.3e, min=%.3e\n', max(sensor_data_A(:)), min(sensor_data_A(:)));
fprintf('B: max=%.3e, min=%.3e\n', max(sensor_data_B(:)), min(sensor_data_B(:)));
%% ===== 8. 画图 =====
t_phys = (0:Nt-1) * dt * 1e6;
figure;
plot(t_phys, sensor_data_A(1,:) / max(abs(sensor_data_A(1,:))), 'b', 'LineWidth', 1.5);
hold on;
plot(t_phys, sensor_data_B(1,:) / max(abs(sensor_data_B(1,:))), 'r', 'LineWidth', 1.5);
xlabel('Time [\mus]');
ylabel('Normalized Pressure');
legend('c = 1500 m/s', 'c = 3000 m/s');
grid on;
xlim([0, 12]);

实验 3:改源为单个圆盘
操作 :把 source.p0 = disc_1 + disc_2 改成 source.p0 = disc_1,重新跑。
预期 :图 2 的斜线变成一组对称的曲线,对应单个圆盘向四周扩散的波前。
为什么:单个圆盘的波前是圆形,到达环形传感器各点的时间不同,形成曲线轨迹。

实验 4:CFL 条件对仿真稳定性的影响(dt 太大,仿真发散)
matlab
clearvars;
%% ===== 1. Grid =====
Nx = 128; Ny = 128;
dx = 0.1e-3; dy = 0.1e-3;
kgrid = kWaveGrid(Nx, dx, Ny, dy);
%% ===== 2. Medium =====
medium.sound_speed = 1500; % 用水的声速,简单
medium.alpha_coeff = 0.75;
medium.alpha_power = 1.5;
%% ===== 3. Source =====
source.p0 = zeros(Nx, Ny);
source.p0(30, 64) = 1;
%% ===== 4. Sensor =====
sensor.mask = zeros(Nx, Ny);
sensor.mask(90, 64) = 1;
%% ===== 5. 对比两种 dt =====
c = medium.sound_speed;
dx_min = dx;
cfl_stable = 0.3;
dt_stable = cfl_stable * dx_min / (c * sqrt(3)); % 满足 CFL
dt_unstable = 10 * dt_stable; % 故意超标 10 倍
fprintf('稳定 dt = %.2e s (CFL=%.2f)\n', dt_stable, cfl_stable);
fprintf('不稳定 dt = %.2e s (CFL=%.2f)\n', dt_unstable, dt_unstable*c*sqrt(3)/dx_min);
%% ===== 6. 情况 A:稳定 dt =====
Nt_A = 2000;
kgrid.setTime(Nt_A, dt_stable);
sensor_data_A = kspaceFirstOrder2D(kgrid, medium, source, sensor);
fprintf('A (稳定): max=%.3e, min=%.3e\n', max(sensor_data_A(:)), min(sensor_data_A(:)));
%% ===== 7. 情况 B:不稳定 dt =====
Nt_B = 200;
kgrid.setTime(Nt_B, dt_unstable);
sensor_data_B = kspaceFirstOrder2D(kgrid, medium, source, sensor);
fprintf('B (不稳定): max=%.3e, min=%.3e\n', max(sensor_data_B(:)), min(sensor_data_B(:)));
%% ===== 8. 画图对比 =====
t_A = (0:Nt_A-1) * dt_stable * 1e6;
t_B = (0:Nt_B-1) * dt_unstable * 1e6;
figure;
subplot(2,1,1);
plot(t_A, sensor_data_A(1,:), 'b', 'LineWidth', 1.5);
xlabel('Time [\mus]'); ylabel('Pressure [Pa]');
title(sprintf('Stable: dt = %.2e s (CFL = %.2f)', dt_stable, cfl_stable));
grid on; xlim([0, t_A(end)]);
subplot(2,1,2);
plot(t_B, sensor_data_B(1,:), 'r', 'LineWidth', 1.5);
xlabel('Time [\mus]'); ylabel('Pressure [Pa]');
title(sprintf('Unstable: dt = %.2e s (CFL = %.2f)', dt_unstable, dt_unstable*c*sqrt(3)/dx_min));
grid on; xlim([0, t_B(end)]);

运行后应该看到什么:
| 情况 | 现象 |
|---|---|
| A(稳定) | max 和 min 是 1 Pa 量级;波形平滑,能看到一个清晰的波峰 |
| B(不稳定) | max 和 min 是 1e12 甚至 1e30 量级;波形振荡发散,看不出物理意义 |
十、总结
| 知识点 | 核心要点 |
|---|---|
| CFL 条件 | 一个时间步内波传播不超过一个网格间距;推荐 CFL = 0.3 |
| dt 公式 | dt ≤ CFL × dx / (c × sqrt(3))(3D),2D 用 sqrt(2) |
| Nt 选取 | Nt = t_end / dt,取决于声波传播到传感器所需时间 |
| setTime | 给 kgrid 挂上时间轴,只设参数不跑仿真 |
| kspaceFirstOrder2D | 检查参数 → 初始化声场 → 时间循环推进 → 记录传感器数据 |
| 伪谱法 | 用 FFT 在波数空间计算空间导数,精度高于有限差分 |
| IVP vs 边界驱动 | source.p0 只在 t=0 存在;source.p 每个时间步注入能量 |
| alpha_power | 吸收随频率的幂律指数,频率翻倍吸收增加 2^alpha_power 倍;必须是全局标量 |
| 实验1 | 改声速 → 时空图斜线斜率变陡 |
| 实验2 | 改传感器为二值矩阵 → 单点记录 |
| 实验3 | 改单圆盘源 → 时空图变成对称曲线 |
| 实验4 | dt 超标 10 倍 → 数值发散,达到 1e12 量级 |
一句话总结 :CFL 条件保证数值稳定,推荐取 0.3;dt 由 dt ≤ CFL × dx / (c × sqrt(3)) 算出;Nt 由总仿真时长决定;setTime 给网格挂时间轴;kspaceFirstOrder2D 内部用 k-space 伪谱法逐步推进声场。alpha_power 是吸收随频率的幂律指数,必须是全局标量。四个实验分别验证了声速影响斜率、传感器模式、源形状和 CFL 稳定性------dt 超标 10 倍,仿真直接发散到 1e12 量级。
#k-Wave #CFL #时间步长 #alpha_power #声学仿真 #学习记录