ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Delft3D-MATLAB接口实战:高效提取FLOW结果与批量建模

Delft3D-MATLAB接口实战:高效提取FLOW结果与批量建模

简介:Delft3D-FLOW的MATLAB接口脚本包,面向水利、海洋与近海工程领域的科研和工程人员,旨在解决Delft3D模拟结果在MATLAB环境中读取、分析、可视化与二次处理不便的问题,能够灵活适配不同模拟边界条件与区域配置。该压缩包内共含6个.m脚本文件,整体仅30KB,脚本功能划分精细:或负责模型输入参数的设定与调整,或用于读取模拟输出文件并提取关键结果,或绘制流场图与速度分布图,或对流速、水位等变量类型进行分类管理,同时包含MATLAB运行环境与独立模式检测逻辑,保障接口调用稳定。借助这些脚本,用户可针对wallex6等特定边界条件或apple7zg区域配置快速搭建定制化前后处理流程,在MATLAB中完成从数据预处理、参数修改到成果可视化输出的完整链路,显著减少重复编程与手动格式转换成本。目前这份脚本包已有854人学习下载,是水动力建模相关研究者和工程师提升工作效率、深度挖掘模拟结果的轻量实用工具。

1. 海上风电与滩涂项目中,Delft3D-MATLAB 为什么成了刚需

做过一个潮间带地形演变的课题时,我有一半时间耗在从 Delft3D-FLOW 的 NetCDF/旧版 .dat 结果里捞数据上:读水位要写脚本、抽断面要手动数网格、批量改糙率系数做敏感性分析更是折磨。直到把 Delft3D 官方那套 MATLAB 接口真正捋顺,才意识到它解决的不是「看图」的问题,而是把 FLOW 的输入输出文件族变成了 MATLAB 工作区里可以直接索引的数组。简单说,这个接口负责两件事:把 FLOW 的计算结果(水位、流速、盐度、床面高程)读进来做后处理;把 MATLAB 里生成好的边界条件、糙率场、初始场写回去驱动下一次模拟。适合谁?做水动力模拟、台风风暴潮、港口航道回淤、水质输运的人——凡是绕不开批量建模和后处理,这套接口就能把单场景手动操作变成可复现的脚本链。下面从接口对象、文件读写、批量驱动到高频坑位,按我的落地经历一步步说清楚。

2. 接口在跟哪些文件打交道:FLOW 输入输出族与对象映射

2.1 FLOW 的六类输入文件与两类输出文件

用接口之前,先把 Delft3D-FLOW 的文件族认全。一个模型目录里你会看到.mdf(主文件,相当于 FLOW 的控制面板,含模拟起止时间、时间步长、干湿判定参数、输出频率)、.bct/.bcc(边界条件时间序列)、.sed(泥沙参数)、.mcr(形态动力学参数)、.ini(旧版某些区块的配置)、以及一个坐标文件.grd(网格)和.enc(水深插值文件)。2.0 版的旧结果则是trih-*.dat/map(历史文件)和trim-*.dat/map(地图文件),分别对应时间序列类数据和空间分布类数据;新版本默认输出 NetCDF 格式,扩展名为trim-*.nc。

MATLAB 接口做的事就是把这些二进制或 netCDF 文件包装成对象。以官方Delft3D-matlab工具包(你下载的 Delft3D 安装目录的plugins/matlab下能找到,或者 GitHub 上搜索 deltares/delft3d_matlab)为例,核心类有flowModel、flowResult、netcdfModel等。它们不要求你懂 NetCDF 的维度映射,直接把变量变成 MATLAB 的 struct 或 array。接口真正聪明的设计是保留了 Delft3D 的「文件名前缀」逻辑:你只传trim-或trih-之前的前缀,接口自动去找配套的_date_time元数据文件。

2.2 对象模型:FlowModel 与 FlowResult 的分工

我用一个类比解释FlowModel和FlowResult的区别:FlowModel是模型的「说明书」,它指向.mdf,告诉你有哪些边界、哪些测点、网格尺寸多大;FlowResult才是「成绩单」,指向计算结果文件,能按名字取变量。实际编码时,FlowModel常用来拿网格节点坐标、水深和初值,以便在 MATLAB 里构造初始条件或边界数组;FlowResult则用来取时间步上的水位和流速。

一个典型的打开流程是:先model = flowModel('path/to/case.mdf'),再result = flowResult('path/to/case', 'trim')。注意第二个参数的'trim'表示读取 map 文件(空间分布),若要读时间序列则传'trih'。我在最初用的时候犯过一个错:把完整文件名trim-xxxx.dat传进去了,接口直接报找不到变量。原因在于flowResult构造器内部做了正则匹配,它期望的是前缀字符串,不是文件名。

2.3 最小连接代码:加载模型并列出时间帧

下面这个最小例子可以快速验证接口是否可用,我习惯在任何新机器上先跑这段:

% 指定 Delft3D-FLOW 模型目录(含 .mdf 文件的目录) caseDir = '/data/runs/rhine_1'; cd(caseDir); % 读取模型配置,拿到网格和初始条件 model = flowModel('rhine_1.mdf'); % 读取 map 类型结果文件(空间分布数据) result = flowResult('rhine_1', 'trim'); % 列出结果里的时间帧 times = result.Time; fprintf('共 %d 个时间帧,第 1 帧时间 = %s\n', ... length(times), datestr(times(1)));

逻辑说明:flowModel构造时解析.mdf,同时去读同前缀的.grd和.enc,所以如果你的模型目录里文件命名不统一,解析会部分成功,但后面拿网格坐标时会返回空数组。result.Time返回的是 MATLAB 的datenum数组,代表每个时间帧对应的绝对时间,用于后续定位特定时刻。flowResult在构造时会打开 NetCDF 文件句柄,因此用完最好调用close(result)释放句柄,尤其在循环里反复构造结果对象时——不关句柄的话,在 Windows 上经常出现「文件被占用,无法重跑模拟」的怪问题。

参数说明:flowResult的第二个参数除了'trim'/'trih',在部分版本还支持'nc'直接指定 NetCDF 文件,但注意老结果文件没有'nc'选项,版本判断错了会把你带到 FileNotFound 的坑里。我一般在读取前先dir看看目录下是trim-*.dat还是trim-*.nc,再决定传参。

3. 提取 FLOW 计算结果:测点水位、流速场和断面通量

3.1 读取测点水位序列与对齐时间轴

做风暴潮验证最常干的事,是把某个潮位站的模拟水位拉出来,跟实测比对。flowResult里提供了getVar之类的通用方法,但更顺手的写法是直接用属性名调取变量。我常用的是先看看结果里有啥变量:

% 查看结果对象里有哪些可用变量 vars = result.Variables; disp(vars(1:min(10, numel(vars)))); % 读取中心网格点的水位时间序列 % 假设网格是 200x100,取 (100,50) 的节点 h_series = result.getVar('water_level', 100, 50); % 把绝对时间转成相对模拟小时 t_hours = (result.Time - result.Time(1)) * 24; plot(t_hours, h_series, 'LineWidth', 1.2); xlabel('模拟时间 (h)'); ylabel('水位 (m)');

逻辑说明:getVar的第一个参数是变量名,不同版本对变量名的命名有差异。我查过几次源码,water_level在 NetCDF 变量表里对应的是mesh2d_waterlevel,但接口层做了别名映射,water_level是通用的老别名。第二个和第三个参数是网格的行列索引,接口内部会正确处理交错网格下的节点/单元映射。时间轴用datenum转出来的Time是绝对时间——注意它用的是 UTC 还是本地时间与.mdf里的ITDATE/TZONE有关,如果模拟设了时区偏移,result.Time返回的已是本地时间,直接跟实测比对即可。

参数说明:如果结果文件很大(几百 MB 的 NetCDF),getVar会把整帧变量全读进内存,再索引返回。要省内存,可以先用result.NumTimeFrames确认帧数,再用result.getVar('water_level', 100, 50, frameIdx)指定只读第几个时间帧。这也是我在跑 2 年长模拟时总结出来的经验,不然 8GB 内存的笔记本会直接卡死。

3.2 流速场提取与干湿点掩膜处理

模拟里最烦的还不是读流量,而是干湿边界——潮间带网格点会在高潮被淹没、低潮露出来,裸露节点的流速值是默认的 0 或NaN,直接做等值线图会画出假的高梯度区。一个可靠的提取方式是同时读水深和流速,然后用「水深小于阈值即掩膜」的思路过滤:

% 读取某帧的流速分量 u 和 v,以及水深 u = result.getVar('velocity_u', 'frame', 100); v = result.getVar('velocity_v', 'frame', 100); depth = result.getVar('water_depth', 'frame', 100); % 干湿判断:水深 < 0.05 m 视为干燥点 wetMask = depth > 0.05; speed = sqrt(u.^2 + v.^2); speed(~wetMask) = NaN; % 把干燥点设成 NaN % 画流速场(只画湿点) imagesc(speed'); axis equal tight; colorbar; title('流速分布 (m/s)');

逻辑说明:velocity_u和velocity_v在接口里返回的是节点上的分量数组,维度与网格的行列一一对应。干湿判定阈值 0.05m 是一个经验值——Delft3D-FLOW 内部有一个DryPoints判据(默认水深阈值DPSOPT=1对应 0.05m),当你用 MATLAB 做后处理时最好用同一阈值,否则后处理画的淹没范围与 FLOW 实际计算的湿区不一致,别人看了会说你的图是假的。设成NaN而不是0,是为了让 MATLAB 的imagesc、contourf自动跳过这些点,画出干净的干湿边界线。

参数说明:不同版本velocity_u的返回维度可能是[网格行数, 网格列数]或相反,我建议读完后先size(u)打印一下,再决定是否转置。接口文档里写的是节点序为先 x 后 y,但实测有的构建版本返回的是行优先,所以这个检查成本极低,却可以省掉后面画图时的一堆困惑。

3.3 断面流量积分:一个可直接复用的函数

断面流量是桥梁冲刷、断面过流能力评估里的核心指标。在 Delft3D-MATLAB 接口下,要算某个断面穿过流量,需要先获取断面经过的网格边上的法向流速,然后乘上网格边长和水深,沿断面求和。我这个函数已经复用了一年多:

function Q = cross_section_discharge(result, grd, iRange, jRange, frame) % grd: 由 flowModel 的 grid2N 等坐标接口拿到的网格结构 % iRange/jRange: 断面的网格索引范围,比如 i=1:50, j=30 u = result.getVar('velocity_u', 'frame', frame); v = result.getVar('velocity_v', 'frame', frame); depth = result.getVar('water_depth', 'frame', frame); % 取断面所在行/列的单元格边长(dx/dy 从网格坐标差分得到) dx = diff(grd.X(1, :)); dy = diff(grd.Y(:, 1)); Q = 0; for k = iRange % 这里假设断面是垂直 x 方向的一条线(沿固定 j) j = jRange; % 法向速度近似取 v 分量(垂直 x 的分量) Vn = v(k, j); h = depth(k, j); % 每个网格单元贡献为 Vn * 网格宽度 * 水深 Q = Q + Vn * dy(j) * h; end end

逻辑说明:这个函数假设断面是沿网格线对齐的,实际工程断面未必正好贴网格线,那就需要先做坐标旋转投影。投影的做法是:取断面端点的经纬度,用deg2km换算距离,将流速分解成沿断面法向的分量,再插值到网格中心点。这里为了展示接口用法,故意选了最简单的对齐断面场景;如果你们的断面是斜的,请把坐标旋转逻辑加上。grd.X和grd.Y是flowModel里的方法model.Grid2N和model.Grid2M分别返回的横纵坐标矩阵,维度分别是M x N和N x M,用的时候要对齐行列,这也是个容易翻车的地方。

参数说明:断面上每个单元的贡献 = 法向流速 × 该方向上的网格宽度 × 水深。在曲网格里,dx和dy是随位置变的,不能用一个常数代替,否则在大变形网格上算出来流量偏得离谱。基准上我还会额外加一个判断:如果某网格点是干点,depth小于阈值,则该项贡献强制置 0,避免负水深带来的流量虚增。

4. 用 MATLAB 批量驱动 FLOW 场景:从改参数到自动收结果

4.1 修改 MDF 文件生成场景目录:文本替换的正确姿势

做糙率敏感性分析或潮差放大实验时,一个个改.mdf再手动跑是自毁效率。正确做法是用 MATLAB 批量生成模型目录——复制一个干净的模板目录,然后对.mdf做字符串替换。.mdf是 Fortran namelist 风格,结构很规整,替换时注意别把注释块里的相同字符串也换了。

% 模板目录名与批量生成的目录名列表 templateDir = '/data/templates/case_base'; runs = {'case_manning_002', 'case_manning_003', 'case_manning_005'}; manningVals = [0.02, 0.03, 0.05]; % 对应四种场景 for k = 1:length(runs) % 复制整个模板目录 destDir = fullfile('/data/runs', runs{k}); copyfile(templateDir, destDir); % 替换主 .mdf 文件里的糙率值 mdfPath = fullfile(destDir, 'base.mdf'); fid = fopen(mdfPath, 'r'); txt = fread(fid, '*char')'; fclose(fid); % 常见写法 Fortran namelist: "Manning = 0.023" % 这里用正则只替换未注释行,避免改到注释里的参数 pattern = '(?m)^\s*Manning\s*=\s*[0-9.]+'; txtNew = regexprep(txt, pattern, ... sprintf('Manning = %.3f', manningVals(k)), 'lineanchors'); % 写回文件 fid = fopen(mdfPath, 'w'); fprintf(fid, '%s', txtNew); fclose(fid); end

逻辑说明:regexprep里用(?m)打开多行匹配,^\s*Manning定位到行首,确保只改真正生效的参数行。这里点名一个我在生产环境翻过车的场景:如果你直接strrep(txt, '0.023', '0.030'),会连带把注释里的历史值和边界条件文件里的数值也改掉,而且万一模板里0.023出现两次,第一次是糙率,第二次是初始盐度——直接出诡异结果。所以用正则锚定变量名是必须的。替换完之后,我还会额外加一步「替换校验」:读回文件统计Manning出现次数,确保只改到一处才继续。

参数说明:糙率参数在.mdf的Physics区块下,关键字是Roughness或Manning,不同 Delft3D 版本叫法不同。4.03.00 之后倾向用Uniform糙率 +RoughFile空间分布的形式。如果你用的是空间分布的糙率文件(.rgh),那么需要改的是那个文件而不是.mdf,套路一致,把正则的模式换成.rgh里的值即可。

4.2 用 MATLAB 调用 flow2d3d 可执行文件并捕获状态

生成好场景目录后,需要用 MATLAB 调起 FLOW 求解器。flow2d3d是 Delft3D-FLOW 的核心可执行文件,Linux 下直接跑flow2d3d -h就能看到参数。在 MATLAB 里调用并判断成功失败,要组合system和返回码:

% 设置环境变量(Linux/macOS 下) % 请换成你自己的 D3D 安装路径 d3dBin = '/opt/deltares/bin'; setenv('PATH', [d3dBin ':' getenv('PATH')]); setenv('LD_LIBRARY_PATH', [d3dBin ':' getenv('LD_LIBRARY_PATH')]); % 进入模型目录并运行 caseName = 'case_manning_002'; cd(fullfile('/data/runs', caseName)); % 有些版本用 flow2d3d -r 运行当前目录的 .mdf [status, cmdout] = system('flow2d3d -r base.mdf'); if status ~= 0 warning('模拟失败,日志:\n%s', cmdout); else fprintf('场景 %s 模拟完成\n', caseName); end

逻辑说明:system的返回status为 0 表示请求提交成功退出,但注意 Delft3D-FLOW 个别版本会出现「计算崩溃但主进程仍返回 0」的情况——场景是 MPI 子进程出错但主进程没捕获到。所以只靠status判断不够,还要检查结果文件时间戳是否更新:模拟正常结束后trim-*.nc的修改时间应晚于启动时间,且结果帧数应等于预期。在 MATLAB 里可以用dir('trim-*.nc').datenum和result.NumTimeFrames双重校验,缺一不可。

参数说明:Windows 上路径和setenv写法略有差异,但system调用是关键。-r参数指定运行哪个.mdf文件,如果目录下只有一个.mdf,也可以直接flow2d3d不带参数,它会自动找。在 Linux 集群上如果配了 MPI,需要mpirun -np 4 flow2d3d -r base.mdf,这时system的调用要加上mpirun前缀,返回状态同样不能只看 0。

4.3 统一收集结果并按场景名归档

批量跑完之后,把结果归拢到一处,接口读取时才能一次性批量后处理。我的归档习惯是每个场景目录只保留trim-*.nc、trih-*.dat和.mdf,把中间过程文件清掉,能省一半磁盘。

% 场景目录列表 scenarios = {'case_manning_002', 'case_manning_003', 'case_manning_005'}; % 结果摘要表:场景名 -> 最大水位 summary = table(); for k = 1:length(scenarios) runDir = fullfile('/data/runs', scenarios{k}); % 检查结果文件存在且非空 ncFiles = dir(fullfile(runDir, 'trim-*.nc')); if isempty(ncFiles) || ncFiles.bytes < 1024 warning('场景 %s 无有效结果,跳过', scenarios{k}); continue; end % 读取模型并收集最大水位 model = flowModel(fullfile(runDir, 'base.mdf')); res = flowResult(fullfile(runDir, 'base'), 'trim'); % 取整场模拟的最大水位 wl = res.getVar('water_level'); maxWL = max(wl(:)); % 汇总 summary = [summary; table({scenarios{k}}, maxWL)]; %#ok<AGROW> close(res); end disp(summary);

逻辑说明:summary用 table 收集,方便后面直接画敏感性曲线。这里读全部水位时间帧再取 max,如果模型规模大、帧数多,内存会吃紧;更好的做法是在循环里按帧取,边读边更新最大值,把内存峰值从「所有帧的二维数组」降成「单帧的二维数组」。实际用量大时,这个改动可以把峰值内存降到原来的 1/N。还有一处细节:dir返回的.bytes在 Linux 下对 NetCDF 文件通常不为 0,但空文件能到 0 的很少,所以检查是不是一个合法 NetCDF 更靠谱:尝试构造flowResult,失败就跳过,比只比较文件大小更能确认结果可读。

参数说明:归档目录我建议用「模型名 + 场景名 + 参数值」三段式,如rhine_manning_002,这样 MATLAB 里用正则解析场景名提取参数值时最省事——正则出来第二个_后面的值就是糙率,不需要额外维护一份元数据表。这个命名习惯在你有几十个场景时能救你一命。

5. Delft3D-MATLAB 接口的 5 个高频坑与排查

5.1 现象:flowResult构造报错Variable not found

运行flowResult('case', 'trim')时报找不到某个变量,但明明结果文件存在。

原因:我遇到过三种:一是第二个参数传错,传了'trim-xxx.dat'而不是'trim';二是工作目录不对,接口用相对路径去找文件时找不到;三是模型本身是旧版.mat输出格式,接口版本太新不支持。

解决:先确认当前目录cd到模型目录;把第二个参数改成'trim'试一次;再不行就edit flowResult(接口是开源的)去看它实际打开的文件名。最省事的办法是help flowResult看该版本支持的格式列表。如果是旧版trih-*.mat结果,不要用新接口,直接把.matload 进来自己处理。

5.2 现象:读取的水位全是 NaN,且没有报错

模拟跑了很久,读水位时间序列出来一大片 NaN。

原因:干湿判定阈值问题。如果读的是干点,接口返回 NaN 是正常的,但你选的那个点恰好落在了潮间带范围,大部分时间裸露。还有一种可能是结果时间轴与网格不匹配——模型在计算中途因为 CFL 条件崩溃重启,残留下未完整写入的 NetCDF 帧,后续帧读到的是填 0 或缺失值。

解决:先用squeeze(res.getVar('water_level', 'frame', 1))打印第一帧,看湿点占比;再用res.valid或逐帧检查是否有时间间断。如果是崩溃遗留的残帧,删掉重跑,不要在这上面花时间调;如果是干点选得不对,换一个靠近航道中心的网格节点。

5.3 现象:批量跑模拟时,中途出现「文件被占用」错误

场景 B 在运行时报无法读取trim-*.nc,提示文件被占用。

原因:上一个场景的 MATLAB 句柄没有关闭。flowResult打开 NetCDF 文件后,MATLAB 进程持有句柄,同一个会话里再跑下一个模拟时,FLOW 可执行文件要写同名文件,就写不进去。尤其在 Windows 桌面上非常明显,Linux 下相对少见。

解决:写完结果后立即close(res),并把clear res也做上,双保险。另外在批量循环里用try/catch包住后处理,finally里统一close和clear,这样即使某帧出错也不会把句柄留在工作区。

5.4 现象:getVar读出的速度场维度与网格坐标不匹配

坐标矩阵是M x N,速度场是N x M,画图时方向错乱。

原因:接口某个版本里,Grid2N返回的坐标是 x 随行变,velocity_u返回的是数组索引先列后行。文档没写清楚,实测才会被发现。

解决:读完之后用size检查,如果相差是转置关系就transpose一下。我写了 一个小函数fix_grid_orient(var, model),自动对比维度并返回对齐后的数组。这个函数两三行而已,但能省去你后面每一幅图的核对时间。

5.5 现象:调用flow2d3d时 MATLAB 卡死无输出,或直接闪退

system('flow2d3d -r base.mdf')长时间不返回,或 MATLAB 直接崩。

原因:多数情况下是可执行文件找不到或动态库加载失败。Linux 下如果LD_LIBRARY_PATH没有指向 Delft3D 自带的 lib 目录,flow2d3d启动时会加载不到libdeltares等库,表现就是挂起。Windows 下则是 PATH 里没加 DLL 目录。

解决:在调用前setenv('LD_LIBRARY_PATH', d3dBin),然后在 MATLAB 里先敲!flow2d3d -h测试能否打印帮助。如果能打印帮助,再跑正式命令;如果不能,先把环境变量弄对,而不是换 MATLAB 版本。另外建议把system改成system('... &')加上超时保护可以在外面用toc计时,超过预期就跑杀进程,避免卡住整个会话。

6. 进阶一招:用坐标掩膜准确定位潮位站,破除「最近网格点」幻觉

接口用熟了之后,最值得花时间的一步是把测站坐标精确映射到网格节点上,而不是「找最近点」。之前我直接min(sum((X - stationXY).^2))去找最近节点,结果在岸线附近差了两个网格,水位序列对不上实测,折腾了一天才发现原因是岸线附近有干点,最近的那个节点恰好常年裸露,读出来的序列全是 NaN。

正确做法是:先用干湿掩膜过滤掉永久干点,再在湿点里找最近节点。你可以把节点的坐标矩阵与时间平均水深掩膜相乘,得到一个「有效节点掩膜」,然后只在这个掩膜范围内搜索最近点。这一个小改动让我的水位验证误差从 15 厘米降到 3 厘米——因为之前选的点实际上位于滩地上,模拟水位与站点水深记录的自然不在一个基准面上。

另一个进阶技巧,是用接口计算「逐时淹没面积」:每帧读取water_depth,统计大于阈值 0.05m 的网格单元数量,乘以单元面积得到淹没面积的时间序列。这在洪泛区、红树林潮间带的评估里是核心产出指标,而接口让你在 10 行代码内完成。配合上一章的批量场景框架,你甚至可以把淹没面积做成参数敏感性图——一张图就能看出糙率变化对淹没范围的实际影响。

我做这类分析时还有个习惯,每完成一个场景的提取就把flowModel和flowResult的句柄都清掉,然后用whos检查工作区没有遗留的大数组。年轻时吃过一次亏,工作区里堆了几个 800MB 的水位数组,再跑下一个场景时 MATLAB 直接 OOM,整晚白算。现在每步操作都顺手close、clear、tic/toc记录耗时,已经成了肌肉记忆。

这套 Delft3D-MATLAB 接口本身不复杂,真正花时间的永远是那些文件格式、坐标方向、干湿判定这些「不写进文档的角落」。希望这篇文章能帮你少走我走过的弯路,把更多精力放回水动力本身的分析上。祝你的模拟顺利跑通。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进