☰
Bellhop水声传播计算:海底地形起伏下的射线追踪与高斯波束合成
2026/10/24 4:29:49 网站建设 项目流程

简介:这份资源面向水下声学、海洋探测与水下通信方向的研究者与工程人员,提供 Bellhop 在海底地形起伏条件下模拟声波传播特性的 Matlab 源码,适合具备一定射线声学基础、希望深入理解传播路径与衰减机制的中高级学习者。压缩包共 154 个文件,约 33.63MB,涵盖 m 源码脚本、prt 与 env 环境参数文件、bty 海底地形数据、ray 射线结果、shd 声场文件及 tif 图像等,完整覆盖从参数配置到结果输出的计算链路。已有 1418 人学习下载,具备较好的参考基础。读者可借助源码理解输入参数处理、射线追踪、散射计算与输出解析各环节,结合声速剖面与地形数据开展定制化仿真,并通过结果文件对照验证模型设置,为水下通信信道评估、声纳性能预测等研究提供可复用的工具与排错思路。

1. 从一张“声线图”说起:Bellhop 到底能算什么

第一次拿到这份 Bellhop 源码,是在一个水声传播课题的联调现场。对方丢过来一个压缩包,说“跑一下,看看海底有山包的时候声场怎么变”。打开一看,全是.m文件,没有界面、没有按钮,只有一堆函数和几个示例脚本。这正是 Bellhop 的典型形态:它不是一个点开就能用的软件,而是一套基于射线追踪(Ray Tracing)的水声传播计算工具,用 Matlab 写成,输入是环境文件,输出是声线轨迹、传播损失、本征声线等结果。

这份源码针对的场景很具体:海底地形起伏条件下的传播特性。平坦海底的传播模型早就成熟了,但真实海底有海山、有斜坡、有沟壑,声线打上去会反射、会折射、会被遮挡,传播损失曲线会出现明显的“阴影区”和“会聚区”偏移。Bellhop 用高斯波束(Gaussian Beam)方法处理这类问题,比纯射线追踪更平滑,能缓解焦散区能量发散的老毛病。适合谁?做水声建模的研究生、做声呐性能预估的工程师、以及需要把传播模型嵌进更大仿真链路里的开发者。如果你只是想画一张漂亮的声线图交差,它也能干,但那就有点浪费了。

2. 环境文件与运行链路:一个算例从输入到出图

2.1 目录里都有什么

解压后先别急着run,花两分钟把目录摸清楚,后面改参数会省很多事。典型的 Bellhop Matlab 包结构大致是这样(不同版本文件名略有出入,但职责划分一致):

bellhop/ ├── bellhop.m % 主入口,读环境文件、调度计算、写输出 ├── read_env.m % 解析 .env 环境文件 ├── ray2d.m / ray3d.m % 二维/三维射线追踪核心 ├── reflect.m % 海底/海面反射系数计算 ├── ssp.m % 声速剖面插值 ├── Arrival.m % 本征声线/到达结构提取 ├── examples/ % 算例环境文件,.env 为主 │ ├── slope.env % 斜坡海底 │ ├── seamount.env % 海山地形 │ └── ... └── misc/ % 绘图、单位换算等辅助脚本

.env文件是整个计算的“黑匣子入口”,所有物理参数都写在这里。主脚本bellhop.m干的事很线性:读环境 → 建声速场和边界 → 追踪射线 → 算场 → 落盘。理解这条链路,比逐行读函数有用得多。

2.2 环境文件长什么样

Bellhop 的环境文件是纯文本,分块书写,块与块之间用单引号开头的注释行分隔。下面是一个带海底起伏的最小可用示例,我按“能跑通”的标准精简过:

'Seamount test' ! 标题 50.0 ! 频率 Hz 1 ! 声源数 0.0 100.0 ! 声源深度范围 m 1 ! 接收器数 0.0 200.0 ! 接收深度范围 m 1 ! 接收距离数 0.0 5000.0 ! 接收距离范围 m 'CVW' ! 边界条件:C=海面真空, V=海底真空, W=水柱 0.0 1500.0 0.0 ! 海面:深度 声速 密度 100.0 1500.0 1.0 ! 水柱分层 200.0 1520.0 1.5 ! 海底界面起始 'ALT' ! 海底地形插值方式:A=解析, L=线性, T=样条 0.0 200.0 ! 地形点:距离 深度 1000.0 180.0 2000.0 120.0 ! 海山隆起 3000.0 160.0 5000.0 200.0 'A' ! 声速剖面类型:A=解析 0.0 1500.0 0.0 ! 剖面参数

几个参数值得单独说。第一行的频率决定波长,波长又决定射线密度够不够,频率越高需要的射线数越多,否则会聚区算不准。'CVW'里的W表示水柱内是流体,海底用V表示真空边界只是简化,真实算例一般会换成A(声学半空间)并给出海底声速和衰减。地形块的'ALT'是插值方式,T(样条)在起伏剧烈时容易过冲,我一般先用L线性插值验证,确认地形没读错再换样条。

2.3 跑起来:三条命令和一次检查

Matlab 里进入包目录,设置路径,然后调用主函数:

% 添加路径,注意 genpath 会把 examples 也加进来 addpath(genpath(pwd)); % 指定环境文件(不带扩展名),输出前缀同名 bellhop('examples/seamount'); % 结果默认写到 seamount.shd(传播损失)和 seamount.ray(声线)

bellhop的第一个参数是环境文件名,不带.env后缀;第二个可选参数是输出文件名前缀,不传就沿用输入名。跑完先别急着画图,用dir看一眼生成了哪些文件。常见输出有.shd(复数声压场,二进制)、.ray(射线坐标)、.arr(到达结构)。如果只生成了.ray没有.shd,多半是环境文件里接收器块写错了,或者'CVW'那行的边界类型和后面的海底参数对不上。

画传播损失最省事的办法是调包里的绘图脚本,或者自己读.shd:

% 读取 shd 文件,返回声压矩阵和距离/深度网格 [pressure, pos] = read_shd('seamount.shd'); tl = -20*log10(abs(pressure) + eps); % 传播损失 dB figure; pcolor(pos.r, pos.z, tl); shading interp; set(gca, 'YDir', 'reverse'); % 深度向下 xlabel('距离 m'); ylabel('深度 m'); title('海底起伏条件下的传播损失');

read_shd的返回维度通常是“深度 × 距离”,pos.r和pos.z是对应的坐标向量。eps是为了防止对零取对数,这个细节不加的话图上会出现-Inf白条,第一次跑的人容易被吓到。

3. 核心模块拆解:射线追踪、反射与声场合成

3.1 射线追踪主循环

Bellhop 的射线追踪本质是解一组射线方程,用龙格库塔类方法步进。核心逻辑在ray2d.m里,简化后的骨架如下:

function ray = trace_ray2d(env, theta0) % theta0: 初始掠射角,单位弧度 % env: 已解析的环境结构体(声速场、地形、边界) step = env.step; % 步长,默认与波长相关 r = 0; z = env.zs; % 起点:距离 0,深度为声源深度 phi = theta0; % 当前射线方向角 ray = [r, z]; % 轨迹缓存 while r < env.rmax && z > env.zmin && z < env.zmax % 当前点的声速与声速梯度 c = interp_ssp(env.ssp, z); dc = gradient_ssp(env.ssp, z); % 射线方程:dr/ds = cos(phi)/c, dphi/ds = -dc/dz * cos(phi)/c dr = step * cos(phi) / c; dphi = -step * dc * cos(phi) / c; r = r + dr; phi = phi + dphi; z = z + step * sin(phi); % 边界处理:触底或触面则反射 if z >= interp_bottom(env.bathy, r) [phi, z] = reflect_bottom(env, r, z, phi); elseif z <= 0 phi = -phi; z = 0; % 海面理想反射 end ray = [ray; r, z]; end end

这段代码有三个关键点。step不是越小越好,太小会让长距离算例慢到不可接受,太大则在地形陡变处直接穿模;经验值是取波长的若干分之一,同时在地形梯度大的区域自适应减半。dphi那一项里的dc是声速对深度的梯度,正是它让声线在跃层附近弯折,这也是声速剖面必须给准的原因。边界判断里interp_bottom对地形做插值,如果地形点太稀疏,射线会在两个采样点之间“跳”过海山,表现为声线莫名其妙穿进海底——这是起伏地形算例最常见的翻车点。

3.2 反射系数与海底建模

射线打到海底不是简单镜面反射,反射系数随掠射角变化,还和海底声速、密度、衰减有关。reflect.m里通常用瑞利反射公式:

function R = reflect_coeff(theta, c1, rho1, c2, rho2, alpha) % theta: 掠射角(相对界面) % c1/rho1: 水介质声速/密度;c2/rho2: 海底声速/密度 % alpha: 海底衰减 dB/波长 % 复声速,把衰减折进去 k2 = 2*pi*f / (c2 * (1 + 1i*alpha/(40*pi*log10(exp(1))))); p = sin(theta) * c1 / c2; % 斯涅尔定律 if p >= 1 R = 1; % 全内反射 return; end cos2 = sqrt(1 - p.^2); Z1 = rho1 * c1 / sin(theta); Z2 = rho2 * c2 / cos2; R = (Z2 - Z1) / (Z2 + Z1); end

alpha的单位容易搞混,有的文献用 dB/λ,有的用 dB/m,源码里一般按 dB/λ 处理,换算时乘频率。p >= 1的分支对应全内反射,此时反射系数模为 1,相位仍会变,如果直接返回实数 1 会丢掉相位信息,导致干涉条纹算错。这个细节在平坦海底算例里不明显,一到起伏地形、多次反射叠加时就暴露出来。

3.3 高斯波束合成声场

纯射线追踪在焦散区能量趋于无穷,Bellhop 用高斯波束把每条射线的能量“抹开”再叠加,得到平滑的声压场。合成逻辑大致是:对每条射线,沿轨迹计算波束宽度和曲率,在接收点附近做高斯加权求和。

function pressure = beam_sum(rays, receivers, freq) % rays: 所有射线的轨迹与相位 % receivers: 接收点网格 [r, z] pressure = zeros(size(receivers, 1), 1); k = 2*pi*freq / 1500; % 参考波数 for i = 1:numel(rays) ray = rays(i); % 每条射线对每个接收点的贡献 for j = 1:size(receivers, 1) dr = receivers(j,1) - ray.r(end); dz = receivers(j,2) - ray.z(end); % 高斯窗:宽度由波束参数控制 w = ray.beam_width; amp = ray.amplitude * exp(-(dr^2 + dz^2) / (2*w^2)); pressure(j) = pressure(j) + amp * exp(1i*k*ray.phase); end end end

beam_width是波束宽度,它和频率、射线管面积有关,源码里由射线间距和曲率推导。这个参数调小了,声场退化成离散亮点;调大了,会聚区和阴影区的边界被抹平,传播损失曲线失去细节。实际调参时我一般先固定射线数(比如 2000 条),扫一遍波束宽度,看会聚区位置是否稳定,稳定了再增加射线数提高精度。

4. 避坑与排查:起伏地形算例最容易栽的五个地方

4.1 声线穿进海底

现象:声线图上射线直接穿过海底地形线,继续在水下“飞行”。原因:地形插值点太稀疏,或者插值方式选了样条导致过冲,射线步进时跨过了界面。解决:把地形采样加密到与射线步长同量级,插值方式先用L线性;在ray2d.m的边界判断里加一步“半步回退”,检测到越界就把步长减半重算。

4.2 传播损失出现大片空白

现象:pcolor图上某些区域是白色或-Inf。原因:该区域没有射线到达(阴影区),声压为零,取对数后变-Inf;也可能是接收网格没覆盖到。解决:加eps防零是治标;治本是增加射线数或放宽初始角度范围,让波束能覆盖阴影区边缘。如果物理上确实是声影区,空白就是正确结果,别硬填。

4.3 会聚区位置对不上文献

现象:算出来的会聚区距离比预期偏近或偏远。原因:声速剖面单位或深度方向搞反了;海底声速给成了淡水值;频率和射线数不匹配。解决:先画声速剖面确认深度向下递增;检查.env里声速块的第一列是不是深度;射线数按频率 × 距离 / 声速的量级估算,不够就加。

4.4 程序跑完没有任何输出文件

现象:命令窗口没报错,目录里也没有.shd。原因:环境文件名传错(带了.env后缀);输出路径没有写权限;接收器块的距离范围写成了 0。解决:bellhop的参数不带后缀;用绝对路径试一次;接收距离范围至少给一个非零值。

4.5 三维算例内存爆掉

现象:跑ray3d时 Matlab 卡死或报内存不足。原因:三维射线数按角度平方增长,接收网格也是二维,内存是二维算例的几十倍。解决:先降射线数跑通流程,确认环境文件无误再逐步加;接收网格用稀疏采样,出图时再插值;必要时分距离段分批计算。

5. 二次开发:把 Bellhop 嵌进自己的仿真链路

5.1 改造思路:从“跑脚本”到“调函数”

原始包是脚本式用法,每次改参数都要动.env文件,批量扫参很痛苦。二次开发第一步是把环境解析和计算拆开,让参数从 Matlab 结构体传入,而不是从文件读。做法是保留read_env.m作为文件入口,另写一个env_from_struct.m把结构体转成内部格式,主计算函数只认内部格式。这样批量跑 100 组海底坡度时,不用生成 100 个.env文件。

% 批量扫参示例:海底坡度从 2° 到 10° slopes = 2:2:10; results = cell(numel(slopes), 1); for i = 1:numel(slopes) env = default_env(); % 默认参数结构体 env.bathy = make_slope(slopes(i)); % 生成对应地形 env.freq = 50; results{i} = bellhop_core(env); % 纯计算,不落盘 end

bellhop_core是我建议新增的中间层:输入结构体,输出声压矩阵和射线,不碰文件系统。落盘和绘图留给外层脚本。这样单元测试也好写,给定一组已知解析解的平坦海底参数,断言传播损失在容差内。

5.2 验证方法:拿平坦海底当“后悔药”

改完核心代码最怕的是“看起来能跑,其实算错了”。我的习惯是保留一个平坦海底的基准算例,它的传播损失有近似解析解(柱面扩展加海底反射修正),每次改动后先跑基准,误差超过 1 dB 就回滚。起伏地形没有解析解,就用“收敛性验证”:射线数翻倍,看会聚区位置和传播损失曲线是否稳定,稳定了才认为结果可信。

验证项方法通过标准
平坦海底对比解析近似传播损失误差 < 1 dB
射线收敛射线数 1000→2000→4000会聚区距离变化 < 1%
能量守恒全角度积分声强与源级偏差 < 0.5 dB
地形连续性地形加密一倍声线轨迹不跳变

5.3 一个具体技巧:把波束宽度做成自适应

固定波束宽度在起伏地形下很别扭:平坦段够用,海山附近要么太粗要么太细。我一般按局部射线管面积做自适应,射线管被地形压缩时减小波束宽度,避免能量“糊”到阴影区里。实现上在beam_sum里把w从标量改成随射线索引变化的向量,由射线间距和曲率实时算。改完之后会聚区边界明显更锐利,代价是计算量增加约两成,换来的是地形遮挡效应算得准。

从那以后我每次改 Bellhop 的核心函数,都强制先跑一遍平坦海底基准再碰起伏算例,这个习惯帮我挡掉过好几次“参数单位写错却看起来正常”的坑。这份源码不大,但把环境文件、射线追踪、反射和波束合成这几块吃透,再按上面的思路拆出可调用的中间层,它就能从一个算例脚本变成你仿真链路里的一块积木。希望帮到你。

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

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

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

立即咨询