【学习记录】k-Wave 时间步长、CFL 稳定性与参数实验全解析

【学习记录】k-Wave 时间步长、CFL 稳定性与参数实验全解析

跑 k-Wave 仿真时,dt 该取多大?Nt 该设多少?alpha_power 到底是什么?改声速、改源、改传感器会发生什么?本文系统整理 k-Wave 中与时间步长和稳定性相关的全部核心知识点:CFL 条件(1D/2D/3D 的理论极限与推荐值 0.3)、dt 的计算公式、Nt 的选取逻辑、setTime 的作用、kspaceFirstOrder2D 的内部流程、幂律吸收模型 alpha_power 的物理含义,以及四个参数实验(改声速、改传感器、改源、故意超标 CFL)的预期现象与原因。保留全部知识点,附完整代码。


📌 目录

  1. [CFL 稳定性条件](#CFL 稳定性条件)
  2. [k-Wave 里怎么算 dt](#k-Wave 里怎么算 dt)
  3. [为什么 sqrt(3) 是 3D 的因子](#为什么 sqrt(3) 是 3D 的因子)
  4. [Nt 的选取逻辑](#Nt 的选取逻辑)
  5. [setTime 的作用](#setTime 的作用)
  6. [kspaceFirstOrder2D 内部做了什么](#kspaceFirstOrder2D 内部做了什么)
  7. 背后的物理/数学
  8. [alpha_power 详解](#alpha_power 详解)
  9. 四个参数实验
  10. 总结

一、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]。

内部流程:

  1. 检查输入参数(CFL 是否满足、PML 位置、尺寸是否匹配)。
  2. 初始化声场(source.p0 作为初始压力)。
  3. 时间循环:每一步用 k-space 伪谱法推进声场。
  4. 每一步在 sensor.mask 位置记录声压。
  5. 返回记录的数据。

七、背后的物理/数学

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 #声学仿真 #学习记录

相关推荐
心祭丫丫3 小时前
【每天一个 Python 小案例17】Python 实现通讯录
开发语言·人工智能·笔记·python·学习
Nerd(BUG版)6 小时前
Flutter学习day5--事件手势与命中测试
学习·flutter
Nerd(BUG版)7 小时前
KMP学习day4--Kotlin空安全与泛型
学习·安全·kotlin
東隅已逝,桑榆非晚7 小时前
算法6. 双指针法-查找总价格为目标的两个商品
c++·学习·算法
wdyan2978 小时前
带着小白学习-微服务-5 实战:配置中心-数据库连接
数据库·学习·微服务
叼烟扛炮9 小时前
Linux 学习笔记(五十):网络层——IP 协议、子网划分与 NAT 魔法
linux·笔记·学习
叼烟扛炮9 小时前
Linux 学习笔记(六十):单线程的逆袭——epoll 高并发服务器实战
linux·服务器·网络·笔记·学习·算法
Rabitebla10 小时前
【Linux操作系统】进程篇(十一):进程程序替换——从底层原理到 exec 函数族
linux·服务器·c++·笔记·学习·算法
叼烟扛炮11 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain