ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MATLAB读取CGNS文件实战:CFD后处理与机器学习特征提取

MATLAB读取CGNS文件实战:CFD后处理与机器学习特征提取

简介:这份资源是面向CFD研究者与工程师的CGNS MATLAB接口库,用于在MATLAB环境中读写和分析CGNS格式的流体动力学数据。CGNS由NASA推荐,采用层次结构组织网格、流场与边界条件等信息,而该库通过MEX机制调用底层C/C++实现,让用户无需了解底层语言细节即可完成数据交换与后处理。压缩包共514个文件,以476个m脚本为主体,辅以13个c与12个h源码、mexw64/mexmaci/mexglx等跨平台MEX二进制文件,以及license、readme和lib等配置说明,整体约1.33MB。资源涵盖读取、写入、数据运算、可视化与自定义脚本等典型功能,可支撑批量处理、算法迭代和结果报告生成。目前已有777人学习下载,适合需要将MATLAB数值计算能力与CGNS标准数据格式打通的CFD从业者参考使用。

1. 从一份 CFD 后处理脚本说起:CGNS 在 MATLAB 里到底怎么用

如果你做过 CFD 后处理,大概率遇到过这种场景:求解器吐出来一堆.cgns文件,里面存着网格坐标、流场变量、边界条件,但手头只有 MATLAB,没有 Tecplot 也没有 ParaView 的批量脚本环境。这时候要么手动一个个导出 CSV,要么写一堆h5read硬啃 HDF5 结构——前者费时,后者容易在节点层级上翻车。CGNS 这套标准本身不复杂,它是一套面向 CFD 的数据组织规范,底层可以是 HDF5 也可以是 ADF,核心是把网格、解、边界条件按节点树挂起来。MATLAB 版本的工具链要解决的就是:让 MATLAB 能直接读写这棵树,而不是让你去跟底层文件格式搏斗。

这份资源适合三类人:做气动/热流仿真、需要批量提取流场截面的工程师;带学生做 CFD 课程设计、想用 MATLAB 统一处理数据的导师;以及手头有历史 CGNS 数据、想迁移到 MATLAB 做后处理或机器学习特征提取的开发者。它不解决求解器本身的问题,只解决“数据进得来、出得去、改得动”这一段。

2. CGNS 的节点树与 MATLAB 的映射逻辑:先搞懂结构再动手

2.1 CGNS 的 SIDS 结构与常见节点路径

CGNS 的核心是 SIDS(Standard Interface Data Structures),它把 CFD 数据组织成一棵有固定命名规则的树。根节点下通常挂CGNSLibraryVersion、Base,Base下挂Zone,Zone下再分GridCoordinates、FlowSolution、ZoneBC、ZoneType等。每个节点有名字、标签(label)、数据类型和维度。比如一个典型的Zone下,GridCoordinates里会有CoordinateX、CoordinateY、CoordinateZ,每个都是数组;FlowSolution里会有Pressure、VelocityX等。

MATLAB 这边没有原生的 CGNS 读取函数,常见做法是走两条路:一是通过 HDF5 底层接口直接读,因为大部分现代 CGNS 文件是 HDF5 容器;二是用 MEX 封装 C 版 CGNS 库。这份资源走的是第一条路为主、辅以结构解析函数的方式,好处是不依赖编译环境,纯 MATLAB 就能跑,代价是需要自己处理节点路径拼接和数据类型转换。

节点路径的写法是第一个容易翻车的地方。CGNS 里路径用/分隔,但节点名可能带空格或特殊字符,比如Base下如果有多个 base,名字可能是Base_1、Base_2。我一般会先写一个递归遍历函数,把整棵树的路径和标签打印出来,确认结构后再定位具体数据。

function listNodes(filePath, nodePath) % 递归列出 CGNS/HDF5 文件中的节点路径与标签 % filePath: .cgns 文件路径 % nodePath: 当前递归路径,初始调用传 '/' info = h5info(filePath, nodePath); fprintf('%s\n', nodePath); for i = 1:length(info.Datasets) fprintf(' [DATASET] %s/%s\n', nodePath, info.Datasets(i).Name); end for i = 1:length(info.Groups) childPath = fullfile(nodePath, info.Groups(i).Name); listNodes(filePath, childPath); end end

这个函数的作用是把 HDF5 层级完整打印出来,h5info返回的Groups和Datasets分别对应 CGNS 里的中间节点和叶子数据节点。注意fullfile在 Windows 下会用反斜杠,但 HDF5 内部路径要求正斜杠,所以实际拼接时建议用[nodePath '/' name]而不是fullfile。参数上,nodePath初始传'/',如果文件有多个 base,你会看到/Base_1、/Base_2这样的分支。

2.2 读取网格与流场变量的最小可用代码

确认结构后,读取就相对直接了。下面这段代码演示从指定 Zone 读取坐标和压力场,并组装成 MATLAB 的网格数据结构。

function [X, Y, Z, P] = readZoneData(filePath, baseName, zoneName) % 读取指定 Base/Zone 下的坐标与压力 % 返回 X,Y,Z 为三维数组,P 为同尺寸压力场 basePath = ['/' baseName]; zonePath = [basePath '/' zoneName]; % 读取坐标,CGNS 中通常为 CoordinateX/Y/Z X = h5read(filePath, [zonePath '/GridCoordinates/CoordinateX']); Y = h5read(filePath, [zonePath '/GridCoordinates/CoordinateY']); Z = h5read(filePath, [zonePath '/GridCoordinates/CoordinateZ']); % 读取流场解,节点名可能是 Pressure 或 Pressure_1 solInfo = h5info(filePath, [zonePath '/FlowSolution']); pName = ''; for i = 1:length(solInfo.Datasets) if contains(solInfo.Datasets(i).Name, 'Pressure') pName = solInfo.Datasets(i).Name; break; end end if isempty(pName) error('未找到压力场节点'); end P = h5read(filePath, [zonePath '/FlowSolution/' pName]); % CGNS 中坐标可能是一维展开,需按 Zone 尺寸重塑 % 这里假设已知道 I,J,K,实际可从 Zone 的 DataSize 属性读取 % 常见做法是读取 /Zone/ZoneType 和 /Zone/DataSize 后 reshape end

逻辑上,h5read直接按路径取数据集,返回的数组维度取决于 CGNS 写入时的排列。坑在于:CGNS 的坐标在 HDF5 里可能是按(I,J,K)展平的一维数组,也可能是三维数组,取决于写入工具。我一般会先size一下读出来的东西,如果是一维,就去读Zone下的DataSize节点拿到I,J,K,再reshape。参数上,baseName和zoneName必须与listNodes打印出来的完全一致,大小写敏感。

2.3 写入 CGNS:从 MATLAB 数组到标准节点

写入比读取更容易出问题,因为要自己构造符合 SIDS 的节点层级。常见做法是先用h5create建数据集,再写属性。下面是一个简化流程,把一个结构化网格的坐标和压力写进新文件。

function writeSimpleCGNS(outFile, X, Y, Z, P) % 将坐标和压力写入新的 CGNS/HDF5 文件 % 注意:完整 CGNS 还需写入 ZoneType、DataSize 等元数据 if exist(outFile, 'file'); delete(outFile); end % 创建 Base 和 Zone 层级 h5create(outFile, '/Base/Zone/GridCoordinates/CoordinateX', size(X)); h5create(outFile, '/Base/Zone/GridCoordinates/CoordinateY', size(Y)); h5create(outFile, '/Base/Zone/GridCoordinates/CoordinateZ', size(Z)); h5create(outFile, '/Base/Zone/FlowSolution/Pressure', size(P)); % 写入数据 h5write(outFile, '/Base/Zone/GridCoordinates/CoordinateX', X); h5write(outFile, '/Base/Zone/GridCoordinates/CoordinateY', Y); h5write(outFile, '/Base/Zone/GridCoordinates/CoordinateZ', Z); h5write(outFile, '/Base/Zone/FlowSolution/Pressure', P); % 写入必要的属性(简化示意) h5writeatt(outFile, '/Base', 'label', 'CGNSBase_t'); h5writeatt(outFile, '/Base/Zone', 'label', 'Zone_t'); end

这段代码只写了最核心的数据,实际 CGNS 文件还需要CGNSLibraryVersion、ZoneType、DataSize等节点,否则其他软件可能不认。参数上,X,Y,Z,P的维度要一致,如果是一维展开的,size要对应I*J*K。写入后建议用listNodes再遍历一遍,确认层级和标签正确。

3. 批量处理与变量提取:把 CGNS 接进 MATLAB 工作流

3.1 批量读取多个 Zone 并拼接截面数据

实际项目里一个 CGNS 文件往往有多个 Zone(比如多块网格),或者一个目录下有一系列时间步文件。批量处理的核心是循环加容错。下面这段代码遍历目录下所有.cgns,提取每个文件里所有 Zone 的压力,并按时间步拼成三维数组。

function allP = batchExtractPressure(dataDir) % 批量读取目录下所有 cgns 文件的压力场 % 返回 cell 数组,每个元素对应一个文件的所有 Zone 压力 files = dir(fullfile(dataDir, '*.cgns')); allP = cell(length(files), 1); for f = 1:length(files) filePath = fullfile(dataDir, files(f).name); % 获取所有 Base info = h5info(filePath, '/'); bases = {info.Groups.Name}; zonePressures = {}; for b = 1:length(bases) baseInfo = h5info(filePath, bases{b}); zones = {baseInfo.Groups.Name}; for z = 1:length(zones) try zonePath = [zones{z} '/FlowSolution']; solInfo = h5info(filePath, zonePath); for s = 1:length(solInfo.Datasets) if contains(solInfo.Datasets(s).Name, 'Pressure') pData = h5read(filePath, ... [zonePath '/' solInfo.Datasets(s).Name]); zonePressures{end+1} = pData; end end catch ME warning('跳过 %s:%s', zones{z}, ME.message); end end end allP{f} = zonePressures; end end

这里用try-catch包住每个 Zone 的读取,是因为不同 Zone 的FlowSolution结构可能不一致,有的没有压力场,有的节点名不同。contains做模糊匹配比精确匹配更稳。参数上,dataDir是数据目录,返回的allP是嵌套 cell,外层对应文件,内层对应 Zone。如果要做时间序列分析,还需要从文件名或Base属性里解析时间步,常见做法是用正则从文件名提取数字。

3.2 把 CGNS 数据转成 MATLAB 的 structured grid 并插值

读出来的坐标和变量往往是分散的,要做截面云图或沿程曲线,得先整理成griddedInterpolant能用的形式。下面演示把一维展开的坐标和压力重塑成三维,再在某个平面上插值。

function plotSlice(X, Y, Z, P, I, J, K, sliceAxis, sliceValue) % 将 CGNS 数据重塑并绘制指定截面的压力云图 % I,J,K 为网格尺寸,sliceAxis 为 'x'/'y'/'z' X = reshape(X, [I, J, K]); Y = reshape(Y, [I, J, K]); Z = reshape(Z, [I, J, K]); P = reshape(P, [I, J, K]); % 构建插值对象,注意 griddedInterpolant 要求单调坐标 % 如果坐标不是单调的,需要先转为 scatteredInterpolant F = griddedInterpolant(X, Y, Z, P, 'linear', 'none'); switch sliceAxis case 'x' [Yq, Zq] = meshgrid(linspace(min(Y(:)), max(Y(:)), 200), ... linspace(min(Z(:)), max(Z(:)), 200)); Xq = sliceValue * ones(size(Yq)); case 'y' [Xq, Zq] = meshgrid(linspace(min(X(:)), max(X(:)), 200), ... linspace(min(Z(:)), max(Z(:)), 200)); Yq = sliceValue * ones(size(Xq)); case 'z' [Xq, Yq] = meshgrid(linspace(min(X(:)), max(X(:)), 200), ... linspace(min(Y(:)), max(Y(:)), 200)); Zq = sliceValue * ones(size(Xq)); end Pq = F(Xq, Yq, Zq); surf(Xq, Yq, Zq, Pq, 'EdgeColor', 'none'); colorbar; axis equal; end

关键点是griddedInterpolant要求坐标网格是单调的,如果 CGNS 里的网格有负体积或重叠,插值会返回NaN。这时候得换scatteredInterpolant,但速度会慢很多。参数上,I,J,K必须和DataSize一致,sliceValue是截面位置。我一般会先检查X,Y,Z的min/max,确认范围合理再插值。

3.3 与 MATLAB 自带 CFD 后处理函数的衔接

MATLAB 没有专门的 CFD 后处理工具箱,但PDE Toolbox和Partial Differential Equation Toolbox里的网格数据结构可以复用。常见做法是把 CGNS 读出来的节点和单元转成femodel或pdeplot3D能接受的格式。如果只是做流场可视化,直接用slice、isosurface、streamslice这些基础函数就够了。下面是一个把压力场转成isosurface的示例。

function showIsoSurface(X, Y, Z, P, isoLevel) % 绘制指定压力等值面 X = reshape(X, size(P)); Y = reshape(Y, size(P)); Z = reshape(Z, size(P)); fv = isosurface(X, Y, Z, P, isoLevel); patch(fv, 'FaceColor', 'red', 'EdgeColor', 'none', 'FaceAlpha', 0.7); isocaps(X, Y, Z, P, isoLevel); view(3); axis equal; camlight; lighting gouraud; end

这段代码假设X,Y,Z,P已经是同维度的三维数组。如果读出来是一维的,先reshape。isoLevel的选择通常看压力的percentile,比如取 50% 分位数。参数上,FaceAlpha控制透明度,isocaps用来封住等值面的开口。

4. 避坑与排查:CGNS 在 MATLAB 里最容易翻车的五个地方

4.1 现象:h5read报 “Unable to open file” 或 “Dataset not found”

原因通常有两个:一是文件根本不是 HDF5 容器,而是老式 ADF 格式,MATLAB 的h5read不认;二是路径拼错了,CGNS 的节点名可能带空格或特殊字符,比如Base_1写成Base1。解决方法是先用h5info(filePath, '/')看根节点下有什么,确认是 HDF5 后再逐层h5info定位。如果是 ADF 格式,常见做法是先用 C 版 CGNS 工具转成 HDF5,或者找专门的 ADF 读取 MEX。

4.2 现象:读出来的数组维度不对,reshape后图像错乱

原因是 CGNS 里坐标和变量的存储顺序可能是(I,J,K)展平,也可能是(K,J,I),取决于写入工具。解决方法是先size读出来的数组,如果是一维,去读Zone下的DataSize节点,拿到I,J,K后尝试两种reshape顺序,看哪个的min/max合理。我一般会写一个autoReshape函数,根据DataSize和数组长度自动判断。

4.3 现象:写入的文件其他软件打不开

原因是只写了数据节点,没写 CGNS 必需的元数据节点,比如CGNSLibraryVersion、ZoneType、DataSize、GridCoordinates的label属性。解决方法是参考 CGNS SIDS 文档,把必需节点补全。常见做法是先用一个已知能打开的文件做模板,用h5info把它的节点树打印出来,照着建。

4.4 现象:批量读取时内存爆掉

原因是把所有时间步的数据都读进内存,CFD 网格动辄百万节点,几十个时间步就撑爆了。解决方法是边读边处理,比如只提取截面数据或统计量,不要保留完整三维场。我一般会写一个processOneFile函数,读一个文件、算完指标、清掉变量,再读下一个。

4.5 现象:griddedInterpolant返回全NaN

原因是网格坐标不单调,或者有重复点。CGNS 里的多块网格在拼接处可能有重叠节点。解决方法是先检查X,Y,Z的diff是否有零或负值,如果有,改用scatteredInterpolant,或者先做网格去重。参数上,scatteredInterpolant的Method选'linear'比'nearest'平滑,但更慢。

5. 进阶:用 MATLAB 做 CGNS 数据的机器学习特征提取

5.1 从流场到特征向量:截面压力矩与涡量统计

做气动优化或流场分类时,直接把整个压力场喂给模型不现实,得先降维。常见做法是提取若干截面的压力统计量:均值、方差、偏度、峰度,再加上涡量幅值的分位数。下面这段代码从 CGNS 读出的压力场计算这些特征。

function feats = extractFlowFeatures(X, Y, Z, P, U, V, W) % 从流场提取统计特征 % U,V,W 为速度分量,用于计算涡量 feats = []; % 压力统计 feats = [feats, mean(P(:)), std(P(:)), skewness(P(:)), kurtosis(P(:))]; % 涡量计算(假设 X,Y,Z 为三维数组) [~, dVdx] = gradient(V, X, Y, Z); [dUdy, ~] = gradient(U, X, Y, Z); omega_z = dVdx - dUdy; feats = [feats, mean(abs(omega_z(:))), ... quantile(abs(omega_z(:)), [0.5, 0.9, 0.99])]; % 沿流向的压力梯度 [dPdx, ~, ~] = gradient(P, X, Y, Z); feats = [feats, mean(dPdx(:)), std(dPdx(:))]; end

逻辑上,gradient需要坐标数组和变量数组同维度,如果读出来是一维的,先reshape。skewness和kurtosis需要 Statistics Toolbox,如果没有,可以用moment手动算。参数上,quantile的分位数根据具体问题调,我一般取 0.5、0.9、0.99 覆盖主体和尾部。

5.2 把特征矩阵接进fitcsvm或fitcensemble

提取完特征后,每个样本是一个行向量,标签是工况类别(比如攻角区间)。下面演示用fitcensemble做分类,并做交叉验证。

function trainFlowClassifier(featureMatrix, labels) % featureMatrix: 每行一个样本,每列一个特征 % labels: 类别标签 cv = cvpartition(labels, 'KFold', 5); acc = zeros(cv.NumTestSets, 1); for k = 1:cv.NumTestSets trainIdx = training(cv, k); testIdx = test(cv, k); model = fitcensemble(featureMatrix(trainIdx, :), ... labels(trainIdx), 'Method', 'Bag'); pred = predict(model, featureMatrix(testIdx, :)); acc(k) = sum(pred == labels(testIdx)) / sum(testIdx); end fprintf('交叉验证准确率:%.2f%% ± %.2f%%\n', ... mean(acc)*100, std(acc)*100); end

参数上,Method可选'Bag'、'AdaBoostM2'、'RUSBoost',样本不平衡时用'RUSBoost'。cvpartition的KFold一般取 5 或 10。注意特征矩阵要做标准化,否则量纲大的特征会主导距离计算,常见做法是zscore一下。

5.3 一个我踩过的坑:特征量纲不一致导致模型失效

早期做流场分类时,我把压力均值(量级 1e5)和涡量均值(量级 1e2)直接拼在一起,结果 SVM 完全学不动,准确率一直在 50% 附近晃。排查了半天才发现是量纲问题。从那以后我每次提完特征都强制走一遍zscore,并且把标准化参数存下来,预测新样本时用同一套均值和标准差。这个习惯帮我省了很多后悔药。另外,CGNS 读出来的压力如果是表压还是绝压,也要确认清楚,不同求解器写的不一样,混用会导致特征漂移。

希望帮到你。

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

RELATED READING

延伸阅读

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