ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于MATLAB的ROMS后处理工具:从sigma坐标转换到潮汐分析

基于MATLAB的ROMS后处理工具:从sigma坐标转换到潮汐分析 简介本资源是面向海洋科学、环境工程及水文气象方向本科生课程设计与毕业设计的MATLAB集成化ROMS建模工具包旨在降低区域海洋数值模拟门槛解决模型配置复杂、强迫场生成繁琐、输出后处理困难等实际问题。压缩包共817个文件主体为693个MATLAB脚本.m涵盖ROMS前处理如网格生成、潮汐强迫tides、SWAN风场耦合swan_forc、运行控制configs.m参数配置、后处理nctoolbox读取netCDF、m_map地理可视化及耦合接口roms_clm辅以57张结果图.png、12个Java/JAR工具、11个HTML文档说明及README.md操作指南整体14.54MB。已有55人学习下载提供完整可运行的MATLAB环境支持方案包含地形追随坐标适配、多源强迫数据接口、潮汐边界自动生成、NetCDF高效解析及地图投影绘图脚本显著提升ROMS从建模到分析的全流程效率。 如果你跑过ROMSRegional Ocean Modeling System大概率经历过这种状态模型提交到集群后等了两三天终于跑完然后对着几十个netcdf文件发呆——海量数据就躺在那里但每取一个变量都要翻一遍说明文档画一张图要调半天投影参数和配色想算个潮汐调和常数还得现找脚本。我做这套基于MATLAB的ROMS工具说到底就是不想再跟这些细枝末节较劲。它覆盖了我日常后处理中最常碰到的几件事读取网格与场数据、把sigma坐标换算成实际深度、提取任意断面、做潮汐分潮调和分析、输出能直接放进论文的图。如果你也在用MATLAB处理ROMS输出这篇内容基本可以当一份使用笔记来看。1. 为什么ROMS后处理绕不开MATLAB我的使用场景和痛点1.1 ROMS输出数据的一个基本事实ROMS的输出是netcdf格式历史文件和平均文件里通常有几十个变量三维的u、v、temp、salt二维的zeta、ubar、vbar还有网格本身的经纬度和水深。但最让新手不适应的不是体积大而是坐标系——ROMS用的是sigma坐标也就是地形跟随坐标。它的垂直分层不是固定的等深度面而是跟着海底地形走浅水区层薄深水区层厚每一层的实际深度在不同水平格点上都不一样。这意味着你直接从netcdf里读出来的temp(i,j,k)那个k不代表海面下第k米而是第k个sigma层。要把它画在真正的深度-水平坐标图上必须结合水深h、自由表面高度zeta、拉伸参数theta_s和theta_b、以及坐标变换方式Vtransform逐一还原每个格点的实际深度。这一点跟气象上常见的等压面坐标完全不同也是很多刚开始接触ROMS的人被卡住的第一道坎。从实际研究需求来看区域海洋模拟的后处理工作高度集中在几类跨陆架断面图、特定等深面上的水平流场、潮位时间序列的调和分析、以及温盐垂直结构对比。每一类需求的第一步都是坐标转换这就直接决定了工具包的第一个核心模块必须是坐标处理否则后面什么都做不了。1.2 这套工具包要解决的四个核心痛点手动用ncread读取十几个变量变量名容易记混维度顺序还要反复确认。sigma坐标转实际深度的公式不复杂但每次现写容易错尤其是陆地点掩膜和自由表面高度参与计算时。潮汐调和分析需要至少一个月的逐时水位输出手工调t_tide的输入参数很繁琐输出结果还要自己整理成分潮表。出图时m_map投影设置、陆地掩膜处理、色标范围、海岸线精度每次都重新调一遍浪费大量时间。这些痛点不是理论上的可以优化而是我实际跑项目时反复被消耗时间的环节。有一次为了画一张跨陆架断面图从读数据到调坐标、填陆地点、调色标整整花了一个下午最后发现其中一层的z坐标算错了整张图作废重来。写这个工具包的直接动机就是那次之后。1.3 MATLAB vs Python为什么在这个项目里选MATLAB现在Python生态里xarray配合roms-tools处理ROMS输出确实很方便但我在这个工具包里选了MATLAB有几个现实原因课题组的既有脚本和代码资产主要在MATLAB里迁移所有历史处理流程的成本远超写一个新工具。每年进组的新同学接触数值模型的第一周通常需要快速上手处理模拟结果。MATLAB的交互式调试和绘图窗口对新手更友好断点一打、变量双击就能看结构Python的命令行和对象模型反而多一层理解成本。t_tide等潮汐分析工具箱在MATLAB生态里成熟稳定输出格式可以直接用于后续作图。论文投稿时MATLAB的绘图引擎对EPS矢量图的支持稳定字体嵌入和线型控制比多数Python方案省心。这不是说Python不行而是在我这个具体工作流里MATLAB是更顺手的选择。工具包的设计也尽量保持了模块独立性——如果你将来想换到Python核心算法逻辑是可以直接翻译的。2. 工具包目录设计与数据流从netcdf到一张出版级图件2.1 目录结构和模块划分工具包的目录结构如下ROMS_MATLAB_Tools/ ├── io/ % 读取netcdf文件 │ ├── read_grid.m │ ├── read_var.m │ └── read_time.m ├── grid/ % 网格与坐标转换 │ ├── s2z.m │ ├── extract_section.m │ └── mask_land.m ├── analysis/ % 诊断量计算 │ ├── tide_analysis.m │ ├── calc_ke.m │ └── calc_strf.m └── plot/ % 绘图模块 ├── plot_section.m ├── plot_horiz.m └── plot_tide.m模块之间互相独立只通过一个标准的网格结构体grd传递数据。这样做的好处是你可以在任何一步替换自己的处理逻辑而不需要改动其他模块。2.2 数据流一次典型调用链一次典型的调用链是这样的% 1. 读取网格信息生成grd结构体 grd read_grid(roms_his.nc); % 2. 读取某时刻的三维温度场 temp read_var(roms_his.nc, temp, grd, 120); % 3. 把sigma坐标转成实际深度z z s2z(grd, zeta, rho); % 4. 提取跨陆架断面 [dist, depth, sec] extract_section(grd, temp, z, [122 34], [124 36]); % 5. 绘图 plot_section(dist, depth, sec, grd);每一步的输入输出都是清晰的。读到内存里的数据要么是标准的二维/三维数组要么是包含坐标信息的结构体后续处理不需要再关心这个变量在netcdf里叫什么名字。2.3 网格结构体的设计把ROMS的网格信息一次读进来grd结构体是整个工具包的数据中枢设计如下grd.lon_rho % 经度rho点L×M矩阵 grd.lat_rho % 纬度rho点L×M矩阵 grd.h % 水深正值L×M矩阵 grd.mask_rho % 陆海掩膜1海/0陆L×M矩阵 grd.s_rho % sigma层值-1到0N×1向量 grd.Cs_r % 拉伸坐标-1到0N×1向量 grd.Vtransform % 坐标变换方式1或2 grd.theta_s % 表层拉伸参数 grd.theta_b % 底层拉伸参数为什么要单独做一个read_grid.m而不是每次现读因为网格信息基本不随时间变化但它在每个处理步骤里都要用。把它一次性读进来并组织成结构体后续所有函数都只需要传入grd一个参数代码会干净很多。另外read_grid.m里会自动检查Vtransform和theta_s/theta_b这两个容易记混的参数从源头避免坐标算错。3. 五个核心功能模块的实现细节3.1 netcdf读取ncread和维度顺序的坑ROMS的netcdf文件里temp变量的维度通常是(xi_rho, eta_rho, s_rho, time)但MATLAB的ncread读出来之后数组维度会自动反转成(time, s_rho, eta_rho, xi_rho)。这是MATLAB列优先存储的天然特性也是新手最容易踩的坑——如果你直接对读出来的数组做squeeze得到的维度顺序很可能跟你预期的不一样。read_var.m这个函数做了三件事自动确认varargin里是否指定了时间帧如果没有就读取最后一帧。自动交换维度把结果整理成(eta_rho, xi_rho)或(s_rho, eta_rho, xi_rho)的标准顺序。对陆地点做掩膜处理把这些位置的数值设为NaN避免后续计算时出现无意义的结果。代码核心逻辑示意function data read_var(file, vname, grd, tindex) raw ncread(file, vname); % 把维度顺序从MATLAB默认顺序转回ROMS通用顺序 ndims_raw ndims(raw); if ndims_raw 4 data permute(raw, [3 2 1]); if nargin 4 data data(:, :, :, tindex); end elseif ndims_raw 3 data permute(raw, [3 2 1]); if nargin 4 data data(:, :, tindex); end end % 陆地点掩膜 if size(data, 1) size(grd.mask_rho, 1) size(data, 2) size(grd.mask_rho, 2) data(repmat(~grd.mask_rho, [1 1 size(data, 3)])) NaN; end end这里有个小技巧permute之后的维度顺序要在写函数的第一版就确定下来并写进注释否则放几个月之后再回来看大概率要重新试一遍才能想起来。3.2 s坐标转z坐标把地形跟随换算成实际深度ROMS的sigma坐标本质上是把水深归一化到-1到0之间表层和底层通过拉伸函数加密。转成实际深度时需要结合每个格点的水深、自由表面高度和拉伸函数值。工具包里的s2z.m封装了这个计算核心逻辑示意如下以Vtransform2为例function z s2z(grd, zeta, vtype) % S2Z 将ROMS sigma坐标转换为实际深度 % 输入grd - 网格结构体zeta - 自由表面高度vtype - rho/w % 输出z - 实际深度负值表示水下三维数组 [N, eta, xi] h grd.h; s grd.s_rho(:); % -1 ~ 0列向量 Cs grd.Cs_r(:); % 拉伸坐标与s对应 % 核心海面处 zzeta海底处 z-h shp size(h); z zeros([length(s), shp]); for k 1:length(s) z(k, :, :) zeta .* (1 s(k)) h .* Cs(k); end z(repmat(~grd.mask_rho, [1 1 size(z, 1)])) NaN; end注意这里给的是核心思想简化版。ROMS实际的Vtransform2公式更复杂一些因为自由表面高度对水深的影响会随s变化标准公式里还包含1 s和h之间的耦合项。完整实现我放在工具包的s2z.m里公式源头是ROMS官方手册里的坐标变换定义。为什么这个模块要单独封装而不是每次现写因为坐标转换的结果会被断面提取、深度平均、垂直插值等多个模块反复使用。一旦算错所有下游结果都会错而检查这个错误的过程极其痛苦。封装之后只要在写函数时对照官方公式验证过一次后面所有调用都放心了。另外我在s2z.m里额外输出一个z_wW点深度因为ROMS的垂直速度在W点上做物质输运计算时经常要跟它对齐。很多现成脚本只给rho点的深度等到算垂向通量时就尴尬了。3.3 断面提取与插值沿任意路径布线断面的绘制需求在陆架海研究中非常常见沿某条经线画温度断面或者沿着一条斜跨陆架的测线画盐度断面。extract_section.m做的事情是给定两个端点坐标经度、纬度沿线提取目标变量。核心步骤是在经纬度平面上生成一条从起点到终点的折线按一定间隔取采样点。找到每个采样点最近的模型格点rho点。从三维场中取出这些格点上的值配以对应的z坐标。输出沿断面的距离、深度和变量值供绘图使用。这里有个关键细节ROMS的网格不是规则经纬度网格直接用interp2会出错。我在实现时用的是最近邻格点索引加线性插值的组合方式——先找最近的格点再在三点之间做距离加权。在网格分辨率比较均匀的区域这个方法的误差完全可以接受。对于断面图的垂直坐标处理方法是先算出每个格点的实际z坐标然后把断面数据映射到一个统一的深度网格上。这样画出来的断面图横轴是离起点的距离纵轴是深度颜色是变量值看起来跟观测断面图一致。3.4 潮汐调和分析用t_tide算分潮振幅与迟角潮汐分析是这套工具包最常被调用的一块。ROMS跑完一个月后输出逐时的自由表面高度zeta你需要提取M2、S2、K1、O1等主要分潮的振幅和迟角。这个过程中t_tide工具箱是核心引擎。function tide tide_analysis(zeta, dt, start_time, lat) % TIDE_ANALYSIS 对水位时间序列做调和分析 % zeta逐时水位序列一维数组 % dt时间间隔单位小时 % start_time起始时间datenum格式 % lat测点纬度用于节点因子修正 % 把时间序列转成t_tide需要的格式 time_decimal start_time (0:length(zeta)-1) * dt / 24; [tide, ~] t_tide(zeta(:), interval, dt, ... start, time_decimal(1), ... latitude, lat, ... synthesis, 1); endt_tide的输入有几个坑interval单位是小时如果你输出的是6分钟间隔的zeta要传0.1而不是1。start参数是起始时刻的十进制小数年不是datenum。t_tide内部会做转换但如果你自己提前转了反而会出错。我后来统一用datenum配合start参数传省去很多麻烦。分析时段长度至少要覆盖最大分潮的周期比如M2是12.42小时做一个月逐时数据完全够。但如果你只想分析一个星期的数据那K1和O1分不开结果就不可靠。输出结果里tide.tidecon是一个Ns×5的矩阵每行是一个分潮五列分别是振幅、迟角、振幅误差、迟角误差、信噪比。我们最关心的是前两列。提取M2振幅和迟角的代码names tide.name; % 分潮名如 M2 idx_m2 find(strcmp(cellstr(names), M2)); amp_m2 tide.tidecon(idx_m2, 1); pha_m2 tide.tidecon(idx_m2, 2);做空间分布图时把整个区域内每个格点的时间序列都过一遍t_tide会非常耗时。我的经验是先做一次区域平均的时间序列分析确定主要分潮和信噪比合格的格点范围再对感兴趣格点批量跑分析。另外t_tide对内存要求不高但对CPU耗时敏感一个月逐时数据跑500×500个格点在普通台式机上需要不少时间。我这里用了并行遍历MATLAB的parfor可以直接安排。3.5 绘图输出m_map底图、流场叠加和图件排版绘图模块是另一个让我省心的部分。plot_horiz.m负责水平分布图逻辑是m_proj(mercator, long, [lon_min lon_max], lat, [lat_min lat_max]); m_pcolor(grd.lon_rho, grd.lat_rho, var); hold on; m_gshhs(ic, patch, [0.8 0.8 0.8]); % 海岸线填充 m_grid(box, fancy); colorbar;m_map的m_pcolor和MATLAB自带的pcolor在坐标处理上不一样。m_pcolor会自动把经纬度转换到投影坐标系不需要你手动做投影变换。但有一点要注意m_gshhs(ic)的分辨率对一个大区域可能太粗糙对一个小海湾则可能太细导致绘图缓慢。我通常先试一次根据目标区域大小决定用ic还是i低一级分辨率。对于断面图plot_section.m的逻辑更直接pcolor(dist, z, sec); shading interp; set(gca, YDir, reverse); % 让深度方向向下 xlabel(距离 (km)); ylabel(深度 (m));最后一步导图我统一用exportgraphics输出分辨率和字体都调好。论文投稿需要EPS时就直接选EPS格式需要位图时选TIFF或PNG。4. 实战复盘从一次黄海潮汐模拟到M2分潮分布图4.1 模拟设置与数据概况我拿黄海的一次潮汐模拟来复盘整个流程。模拟区域大约是120°E—127°E30°N—40°N水平分辨率1/12°垂直方向30层地形用ETOPO1。开边界水位由TPXO7的八个主要分潮驱动模拟总时长30天逐时输出自由表面高度。跑完之后hisfile大约有720帧文件大小约几GB。这个设置下输出文件里的zeta变量维度是(xi_rho, eta_rho, time)网格是301×401总格点数约12万个。4.2 三步走插值、调和分析、绘图第一步读取网格和数据grd read_grid(his_30d.nc); zeta_all ncread(his_30d.nc, zeta); % 此时维度是 [time, eta, xi] zeta_all permute(zeta_all, [3 2 1]); % 转成 [eta, xi, time]第二步对每个海面格点做调和分析提取M2分潮振幅。注意这一步计算量很大12万个格点跑t_tide我用的策略是先判断该格点的水深和掩膜然后只在有效海区跑amp_m2 nan(size(grd.lon_rho)); pha_m2 nan(size(grd.lon_rho)); mask grd.mask_rho 1; for i 1:size(grd.lon_rho, 1) for j 1:size(grd.lon_rho, 2) if mask(i, j) zeta_ts squeeze(zeta_all(i, j, :)); tide tide_analysis(zeta_ts, 1, datenum(2023-01-01), grd.lat_rho(i, j)); % 提取M2 amp_m2(i, j) tide.tidecon(idx_m2, 1); pha_m2(i, j) tide.tidecon(idx_m2, 2); end end end如果你电脑支持并行计算这里可以直接改parfor——把for i改成parfor i就能把CPU利用率拉满。我实测在八核机器上从串行的两个多小时压缩到四十分钟左右。第三步绘图figure; m_proj(mercator, long, [119 128], lat, [29 41]); m_pcolor(grd.lon_rho, grd.lat_rho, amp_m2); shading interp; caxis([0 3]); colorbar; m_gshhs(ic, patch, [0.8 0.8 0.8]); m_grid(box, fancy); title(M2 振幅 (m));迟角图同理只是色标范围改为0—360度并提醒自己注意角度环绕问题——0度和360度在色标上其实是同一个方向所以我会用caxis([0 360])配合一个循环色标比如hsv避免颜色跳变。4.3 结果验证与验潮站数据的对比模拟值不能只看图必须跟实测数据做对比。这里拿青岛验潮站约120.33°E, 36.07°N为例模拟给出的M2振幅约1.42 m迟角约96°公开资料显示该站多年平均M2振幅大约1.49 m迟角约88°左右。振幅差约5%迟角差约8°在区域潮汐模拟里属于正常精度范围。对比的代码很简单用grd里找到离验潮站最近的格点直接取该点的调和分析结果dist_compute sqrt(((grd.lon_rho - 120.33)./cosd(36.07)).^2 (grd.lat_rho - 36.07).^2); [~, idx] min(dist_compute(:)); [ii, jj] ind2sub(size(dist_compute), idx); fprintf(M2 amp %.2f m, pha %.1f deg\n, amp_m2(ii, jj), pha_m2(ii, jj));这类验证建议同时取两三个不同位置的验潮站做对比如果所有站的误差都落在合理范围内整张分潮图才敢放心用于分析。只靠一个站的对比万一那个站刚好在模式的强项区域会掩盖整体的偏差。5. 踩坑记录这些MATLAB细节文档里不会写5.1 ncread的permute陷阱维度顺序为什么反了前面提到过MATLAB的ncread读出来维度是反的。这里再强调一次ROMS的netcdf里temp维度是(xi, eta, s, time)但ncread返回的数组维度是(time, s, eta, xi)。如果你直接把这个数组交给后续的插值函数结果大概率是错位的。最典型的错误是读了一帧温度用imagesc(squeeze(temp))画图结果发现图是转置的而且上下颠倒。解决办法就是统一用permute把维度顺序转成(eta, xi)在前、s在中间、time在最后。这个规则写进read_var.m之后团队里其他人就不会再犯同样的错误了。5.2 掩膜问题陆地点在插值后变成NaN怎么处理ROMS的陆地点在netcdf里通常已经是填充值FillValue但ncread读出来的可能是一个巨大负数而不是NaN。如果你直接拿这个数据做插值或绘图色标会被拉成一个极端范围图面看起来就是一整片色块。工具包的解决方案是在读取阶段就统一替换data(data -999) NaN; data(~grd.mask_rho) NaN;但这里有个隐患三维场做垂直插值时如果表层有陆地点而深层是海区比如坡折处直接插值可能会把陆地的NaN扩散到周边海区。我的处理方式是在插值前先把整个二维掩膜广播到每一层并明确告诉插值函数这些点不参与计算也就是说任何与陆地点相关的插值结果直接设为NaN不做外推。5.3 t_tide的时间基准和采样间隔设置t_tide的start参数是它最容易出错的地方。如果传的是datenum内部会做一次转换如果你自己先用date2num转成了十进制小数年再传进去就会二次转换导致时间偏移。我最后统一在tide_analysis.m里面处理传入datenum函数内部只做一次转换确保结果可复现。另外ROMS输出的时间通常是自模型起始时刻以来的秒数你需要把这个秒数换算成绝对时间再传给t_tide。换算公式很简单start_abs datenum(model_start_date) time_seconds / 86400;但模型起始日期在ocean_his.nc的time属性里是seconds since 2023-01-01 00:00:00这种格式注意别直接把起始时刻当成0处理。采样间隔的坑也值得一提。ROMS历史文件的输出间隔不一定是1小时有的是6分钟、有的是一天。做潮汐调和分析时如果数据间隔大于6小时M2分潮的特征就混叠了结果不可信。所以在tide_analysis.m入口处我加了一个检查如果dt 2小时直接报错提醒用户该数据不适用于调和分析。这个检查看起来简单但能避免一批为什么我的M2振幅这么大的无头公案。5.4 内存与本文还有配套的精品资源点击获取
返回列表