ARTICLE DETAIL

资讯详情

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

OpenFOAM二次开发教程(05):场与网格对象模型——volScalarField、fvMesh 与 IOobject 注册表

OpenFOAM二次开发教程(05):场与网格对象模型——volScalarField、fvMesh 与 IOobject 注册表 OpenFOAM二次开发教程05场与网格对象模型——volScalarField、fvMesh 与 IOobject 注册表版本与事实声明fvMesh类及其构造函数含IOobject构造、可选的后处理参数见官方 Doxygen 的fvMesh.Hcpp.openfoam.org。volScalarField/volVectorField/surfaceScalarField属finiteVolume库的场类型族路径与继承关系以本机$FOAM_SRC/finiteVolume/fields/与官方 Doxygen 为准。单位dimensionSet的指数顺序与语义以官方源码dimensionSet.H注释为准本文给出的顺序与官方实现一致但请落笔前复核本机版本。tmp是 OpenFOAM 的模板化引用计数/临时对象包装类其语义以官方源码与 Doxygen 为准。一句话结论OpenFOAM 的场不是一堆数组而是由网格、维度、边界条件三件东西共同定义的对象——volScalarField/volVectorField依附fvMesh携带dimensionSet单位代数并在赋值/运算时被校验通过IOobject与objectRegistry与磁盘及全局注册表关联掌握这三层关系才能解释为什么单位写错会编译或运行报错以及为什么场能按名字从注册表里取回来。〇、本篇要解决的认知问题Q1OpenFOAM 的场到底是什么为什么它不是一个简单的数组Q2体心场volField与面心场surfaceField有什么区别为什么面通量要用面心场Q3dimensionSet的单位代数是怎样工作的它如何在编译期/运行期保护你Q4IOobject与objectRegistry分别负责什么为什么注册名很重要Q5tmp是什么为什么 OpenFOAM 大量使用tmp...而不是直接返回对象一、机制解析1.1 场的三重身份网格 维度 边界在 OpenFOAM 里一个volScalarField内部由三样东西共同定义┌─────────────────────────────────────────┐ volScalarField │ GeometricFieldscalar, fvPatchField, │ (体心标量场) │ volMesh │ └─────────────────────────────────────────┘ │ │ │ ① 网格拓扑 ─┘ ② 单位维度 ─┘ └─ ③ 边界条件 (fvMesh) (dimensionSet) (fvPatchField 数组) ├ 单元数 N ├ 7 个指数 ├ each patch 一个 ├ 单元中心 C ├ 由 dimensionSet ├ 决定边界上的值/梯度 └ 体积 V └ 的 pow/[] 校验 └ 与内部场并存为什么这对你重要这张图解释了三类最让人抓狂的问题——“为什么两个场不能相加”→ 因为维度不同②。速度 压力在物理上无意义OpenFOAM 用单位代数在运行时直接拦下。“为什么我的场在边界上值不对”→ 因为边界不是内部场的外推而是独立的 ③ 部分由0/场文件里的type决定第 11 篇专讲。“为什么并行/单机结果一样换个网格就崩”→ 因为 ① 网格拓扑变了场必须按新网格重建不能跨网格复用。取值方式体心场的内部值用field.internalField()常简写作field.primitiveFieldRef()访问边界值用field.boundaryField()[patchi]。内部场与边界场是两套数据写代码时别混。1.2 体心场 vs 面心场为什么要有两种类别类型示例值存放位置典型用途体心场volume fieldvolScalarField、volVectorField、volTensorField网格单元中心速度U、压力p、温度T、湍流量k/omega面心场surface fieldsurfaceScalarField、surfaceVectorField网格面中心面通量phi、面法向梯度为什么要专门有面心场有限体积法的核心是通量穿过面。对流通量phi是面法向速度 × 面面积天然定义在面上把它存成面心场可以避免每次计算都插值也保证守恒性。这就是为什么createPhi.H要单独构造phi——它不是U的副本而是经过插值与面积加权的面量。经验法则凡是穿过面的量用surfaceScalarField凡是单元内的量用volField。把通量错写成体心场是初学者最典型的性能与正确性问题源。1.3 dimensionSet单位代数如何保护你OpenFOAM 用dimensionSet表示物理量的量纲它是7 个整数指数对应国际单位制的 7 个基本量dimensionSet(mass, length, time, temperature, moles, current, luminousIntensity) [kg] [m] [s] [K] [mol] [A] [cd]常用单位可以由这些指数表达物理量表达式示意说明无量纲dimensionSet(0,0,0,0,0,0,0)如湍流强度、体积分数速度dimensionSet(0,1,-1,0,0,0,0)m/s压力运动学dimensionSet(0,2,-2,0,0,0,0)m²/s²不可压缩 p/ρ动力黏度dimensionSet(1,-1,-1,0,0,0,0)kg/(m·s)温度dimensionSet(0,0,0,1,0,0,0)K它在两个层面保护你编译期部分运算要求类型一致类型不匹配直接编译失败例如把volVectorField赋给volScalarField。运行期涉及不同维度的加法/比较会触发dimensionSet校验并报 “dimension mismatch”官方术语为 dimension checking。这是 OpenFOAM 少有的运行期也管单位的机制是你防呆的第一道防线。铁律延伸工程通识任何从外部数据CSV、实验值读入并写入场的量必须显式换算到与场维度一致不能看着数值差不多就直接写。单位不一致是 CFD 结果的隐形杀手且往往在物理上看起来还挺合理。1.4 IOobject 与 objectRegistry场如何与磁盘和全局表关联IOobject是一个身份描述符它告诉 OpenFOAM 四件事名字、时间、所属注册表、读写策略。构造场时把它传给场构造函数场就知道去磁盘哪里读、是否写回。objectRegistry是运行期的对象登记表每个fvMesh以及runTime本身就是一个注册表。当你创建一个IOobject时若把registerObject设为真默认该对象就会被登记进注册表之后可以按名字取回来// 创建时登记默认行为volScalarFieldT(IOobject(T,runTime.timeName(),mesh,IOobject::MUST_READ,IOobject::AUTO_WRITE),mesh);// 别处按名字从注册表取回同一对象不重新读盘constvolScalarFieldTrefmesh.lookupObjectvolScalarField(T);为什么这对你重要注册表是 OpenFOAM 松耦合设计的核心。函数对象、fvModels、边界条件都不需要拿到你的变量引用——它们只需按名字去注册表查。这就是为什么场名拼错Tvstemperature会导致对象不存在报错自定义库能在不修改求解器的情况下访问求解器的场第 12、13 篇的关键机制。1.5 tmp为什么 OpenFOAM 到处是 tmptmpT是 OpenFOAM 的临时对象包装承担引用计数 移动/复用的职责。它的存在是为了解决一个 C 经典难题表达式fvc::grad(p)返回一个临时场它该由谁负责释放tmp的语义大致是若临时对象被移动进了另一个对象则不重复拷贝若被借用constRef()则不转移所有权引用计数归零时自动释放。对你的实际影响不写成tmp的规则而是三条实用经验看到tmpvolVectorField返回值先()求值再取引用例如tmpvolVectorField tgrad fvc::grad(p);想长期持有就volVectorField gradP tgrad();拷贝/移动到自己名下否则tgrad过期后引用悬空。不要保存对临时场的引用const volVectorField g fvc::grad(p);这种写法在某些情况下会悬空。安全写法是先落地成自己的对象。ref()/ref()()用于就地修改需要原地改临时量时用相应接口但要确保语义清晰。二、完整代码与逐行剖析代码 2-1一个完整的场读写与单位校验工具C/*---------------------------------------------------------------------------*\ myFieldTool.C —— 读入 T 场、做一次有物理含义的修改、写回并校验单位 编译放好 Make/files 与 Make/options 后执行 wmake 运行myFieldTool -case 算例 \*---------------------------------------------------------------------------*/#includefvCFD.H// 有限体积公共头fvMesh、volField、fvc/fvm、dimensionSetintmain(intargc,char*argv[]){// 复刻求解器骨架的“公共前戏”但只取我们需要的两步#includesetRootCase.H// 解析 -case#includecreateTime.H// 建立 runTime#includecreateMesh.H// 建立 fvMesh本工具不需要场自动创建// ---- 1. 构造一个 IOobject 并从磁盘读 T 场 ----// 六个参数名字、时间名、注册表(mesh)、读策略、写策略、是否登记(默认 true)InfoReading field T\nendl;volScalarFieldT(IOobject(T,runTime.timeName(),mesh,IOobject::MUST_READ,// 缺文件立即报错不做静默默认值IOobject::AUTO_WRITE// 后续写出时可被写回),mesh);// ---- 2. 打印维度验证单位代数确实生效 ----// dimensionSet 支持输出为类似 [0 0 0 1 0 0 0] 的指数向量InfoT dimensions T.dimensions()endl;// ---- 3. 读取一个“参考温度”做量纲一致的线性变换 ----// Tnew T dT其中 dT 必须有与 T 相同的维度否则运行期报维度不匹配constdimensionedScalardT(dT,T.dimensions(),// 直接复用 T 的维度永不出错的做法5.0// 数值部分单位与 T 一致示例值非任何标准规定);// ---- 4. 遍历内部场与边界场体心场的两套数据----scalar maxT-GREAT,minTGREAT;forAll(T,celli)// 宏遍历所有单元{T[celli]dT.value();// 仅改内部场maxTmax(maxT,T[celli]);minTmin(minT,T[celli]);}// 边界场每个 patch 一个 fvPatchField单独处理forAll(T.boundaryField(),patchi){// firstTimefalse 表示不是首次更新用 evaluate() 让边界按类型重新求值T.boundaryFieldRef()[patchi].evaluate();}InfoT after shift: min minT max maxTendl;// ---- 5. 演示“按名字从注册表取回对象”----// 若 T 未被登记或名字写错此行会触发 objectRegistry 查找失败constvolScalarFieldTsamemesh.lookupObjectvolScalarField(T);InfolookupObject(\T\) matches local T: (TsameT?true:false)endl;// 应为同一对象// ---- 6. 写入结果到当前时间目录 ----T.write();// 显式写出不受 AUTO_WRITE 时机限制InfoEnd\nendl;return0;}逐行剖析只 include 了setRootCase.H/createTime.H/createMesh.H没有createFields.H本工具不需要求解器那些场说明骨架是可以按需裁剪的。IOobject的MUST_READAUTO_WRITE这是既要能读、又要能写回的标准组合。反例后处理工具常把写策略设为NO_WRITE因为它只读不写。T.dimensions()打印维度这是调试单位问题的第一手段。当出现 “dimension mismatch” 时把相关场的维度都打出来立刻能看出谁的单位不对。dimensionedScalar dT(dT, T.dimensions(), 5.0)用T.dimensions()而不是手写dimensionSet(...)是永不写错单位的最佳实践——需要什么单位就让源场告诉你。forAll(T, celli)OpenFOAM 的标准遍历宏等价于对T.internalField()逐单元循环。注意T[celli]是内部场的下标访问不是整个场。T.boundaryFieldRef()[patchi].evaluate()边界是独立数据这一机制的落地。改完内部场后边界值需要按各自类型重新求值否则内部/边界不自洽。boundaryField()是只读访问boundaryFieldRef()才可写。mesh.lookupObjectvolScalarField(T)演示注册表按名取回。用指针比较Tsame T验证确实是同一个对象不是重新读盘的副本——这段代码本身就是对objectRegistry机制的可执行证明。T.write()显式写出用于工具类应用不受writeControl时机约束求解器里则一般用runTime.write()。代码 2-2配套的 Make 文件# Make/files myFieldTool.C EXE $(FOAM_USER_APPBIN)/myFieldTool# Make/options EXE_INC \ -I$(LIB_SRC)/finiteVolume/lnInclude EXE_LIBS \ -lfiniteVolume逐行剖析Make/files里的EXE $(FOAM_USER_APPBIN)/myFieldTool是铁律 4 的直接体现产物进用户目录升级不丢。Make/options用EXE_INC/EXE_LIBS可执行文件少写-I.../lnInclude就会在编译fvCFD.H时找不到头文件少写-lfiniteVolume就会在链接fvMesh符号时报 undefined reference——第 02、03 篇的报错纪律在这里闭环。代码 2-3验证脚本POSIX Shell#!/bin/sh# verify_field_tool.sh —— 在官方算例副本上验证 myFieldTool 的行为# 用法CASE_SRC某个含 T 场的官方算例 sh verify_field_tool.shset-euCASE_SRC${CASE_SRC:?请先用 CASE_SRC... 指定一个含 T 场的官方算例路径}WORK$PWD/_field_tool_caserm-rf$WORK;cp-r$CASE_SRC$WORKcd$WORK# 记录修改前的 T 场首行数值便于对比echo[before] 0/T 头部:;sed-n1,6p0/T2/dev/null||echo (该算例没有 0/T请换一个含 T 的算例)myFieldTool-case.log.myFieldTool21||{echo[FAIL] 工具运行失败见 log.myFieldTool;exit1;}grep-qEndlog.myFieldToolecho[OK] 工具正常结束grep-qdimensionslog.myFieldToolecho[OK] 已打印维度信息echo[after] 0/T 头部应可见数值已变:;sed-n1,6p0/Techo[OK] 验证完成产物$FOAM_USER_APPBIN/myFieldTool 可直接用于其它算例。逐行剖析CASE_SRC必须显式给定因为含 T 场的官方算例随版本/发行线而异脚本不硬编码路径这正是不臆造路径的具体做法。先记录改前再记录改后这是验证脚本的黄金准则——只有对比才能证明行为确实发生了。myFieldTool -case .因为工具复用了setRootCase.H所以与官方应用用法一致第 03 篇讲的骨架红利。用grep -q检查日志中的关键行而非看日志没红字可被自动化消费的判据才叫判据。三、常见报错与排查报错 3-1-- FOAM FATAL ERROR: Trying to construct a dimensionSet with incompatible dimensions或 dimension mismatch。现象程序运行到某处报维度不匹配。根因把两个不同维度的量相加/比较或给dimensionedScalar手写了错误的dimensionSet。解法用field.dimensions()打印所有相关量的维度逐一对账优先用复用源场维度的写法如dimensionedScalar(name, T.dimensions(), 1.0)从根本上避免手写指数。报错 3-2-- FOAM FATAL IO ERROR: cannot find file ...找不到某场。现象构造场时报找不到文件。根因MUST_READ的场在0/或当前时间目录下不存在或名字拼写与文件不一致大小写敏感。解法ls 0/核对文件名确认IOobject的第一个参数与文件名完全一致。边界条件文件与场文件是同一个文件不要分开找。报错 3-3-- FOAM FATAL ERROR: object of type volScalarField named T not found in registry。现象lookupObject...(T)找不到对象。根因该对象没有登记构造时把注册开关设为false或名字写错或它在另一个注册表里例如属于另一个网格region。解法核对名字确认构造时未关闭登记多区域算例要用对应网格对象去查。报错 3-4程序能跑但结果不对——“边界值没跟着变”。现象改了内部场但边界仍显示旧值。根因只改了T[celli]内部场没更新边界场内部场与边界场是两套数据。解法改完内部场后对T.boundaryFieldRef()逐 patch 调用evaluate()或按边界类型显式设置保证内部/边界自洽。关键区分boundaryField()只读boundaryFieldRef()可写。报错 3-5error: conversion from tmp... to non-scalar type ... requested。现象编译报tmp相关的转换错误。根因fvc::grad(...)等返回tmpT直接当T用会编译失败或把tmp赋给const T造成生命周期问题。解法显式求值——volVectorField gradP fvc::grad(p);这样 gradP 是自己的对象再使用gradP。四、动手练习练习 1编译与运行用代码 2-1、2-2 编译myFieldTool并选择一个含T场的官方算例副本运行代码 2-3。判定wmake无 error产物$(FOAM_USER_APPBIN)/myFieldTool存在日志出现End0/T中数值确实发生变化。练习 2单位实验把代码 2-1 中dT的构造改成手写错误维度例如给一个温度场配速度维度。判定程序报维度不匹配错误或数值行为明显异常并能指出这是dimensionSet校验在起作用改回T.dimensions()后恢复正常。练习 3注册表在代码 2-1 中把IOobject的注册开关显式设为false若该版本构造函数支持该参数以官方 Doxygen 签名为准再运行lookupObject。判定能观察到查找失败或取到不同对象并能用objectRegistry的机制解释原因。练习 4体心 vs 面心阅读本机createPhi.H源码写出phi的构造过程由U与网格插值/面积得到。判定能准确说出phi的类型surfaceScalarField与它为什么必须是面心场能在源码中指出插值与面积相乘的具体表达式。练习 5思考题无标准答案假设你要把一个外部 CSV 里的温度剖面读入并写入0/T。说明需要注意的三件事。验证要点(a) 是否需要显式单位换算到与T.dimensions()一致(b) 是否需要正确处理边界 patch 的值内部场与边界场两套数据© 是否需要考虑数据的网格对应关系外部数据按坐标还是按单元号对应。五、小结与下一篇预告本篇建立了 OpenFOAM 二次开发最重要的心智模型场是网格 维度 边界的三位一体对象内部场与边界场是两套数据改内部场必须同步边界dimensionSet用 7 个指数做单位代数在编译与运行两个层面防呆IOobject管身份与读写策略objectRegistry让对象可按名取回这就是函数对象与fvModels能松耦合访问场的底层机制tmp是生命周期管理的包装别长期持有对临时场的引用。第 06 篇《有限体积离散与方程装配》将把场用起来fvm::隐式与fvc::显式两族算子、fvMatrix矩阵装配、solve()与SolverPerformance——学完你会明白为什么 OpenFOAM 说求解方程其实就是装配一个矩阵然后解它。本篇认知问题回显FAQQ1OpenFOAM 的场是什么为什么不是一个简单数组A场是网格 单位维度 边界条件共同定义的对象。以 volScalarField 为例它记录网格拓扑单元数、单元中心、体积、dimensionSet 七维单位指数以及每个边界补丁patch对应一个 fvPatchField。内部值通过 internalField 访问边界值通过 boundaryField()[patchi] 访问二者是两套独立数据。因此两个场不能相加可能源于维度不同边界值不对源于边界场未更新换网格后场失效源于网格拓扑是场的一部分。Q2体心场与面心场有什么区别为什么面通量用面心场A体心场volScalarField、volVectorField、volTensorField的值存在网格单元中心用于速度 U、压力 p、温度 T、湍流量 k/omega 等。面心场surfaceScalarField、surfaceVectorField的值存在面中心用于面通量 phi 与面法向梯度。有限体积法的核心是穿过面的通量phi 等于面法向速度乘面面积、天然定义在面上存成面心场可避免每次计算都插值并保证守恒这也是 createPhi.H 要单独构造 phi 而非复制 U 的原因。Q3dimensionSet 的单位代数如何工作AdimensionSet 由 7 个整数指数构成对应质量、长度、时间、温度、摩尔量、电流、发光强度单位依次为 kg、m、s、K、mol、A、cd。常用量以此为基速度是 (0,1,-1,0,0,0,0)运动学压力是 (0,2,-2,0,0,0,0)温度是 (0,0,0,1,0,0,0)无量纲是全零。部分类型不匹配在编译期报错涉及不同维度的加法和比较在运行期触发维度校验并报 dimension mismatch。最佳实践是复用源场维度如 dimensionedScalar(name, T.dimensions(), 1.0)而非手写指数。Q4IOobject 与 objectRegistry 各负责什么AIOobject 是对象的身份描述符携带名字、时间名、所属注册表、读策略如 MUST_READ与写策略如 AUTO_WRITE决定场从哪里读、是否写回。objectRegistry 是运行期对象登记表fvMesh 与 runTime 本身都是注册表构造时若登记之后可用 mesh.lookupObject(“T”) 按名字取回同一对象非重新读盘副本。注册表机制是函数对象、fvModels 与自定义边界能在不修改求解器的情况下访问模型场的底层基础因此场名拼写一致非常重要。Q5tmp 是什么为什么别长时间持有对临时场的引用Atmp 是 OpenFOAM 的模板化临时对象包装负责引用计数与移动语义用来解决表达式返回的临时场由谁释放的问题被移动进新对象则不重复拷贝被借用constRef则不转移所有权引用计数归零自动释放。实际影响有三条看到 tmp 返回值应先求值再取引用如 volVectorField gradP fvc::grad§;不要保存对临时场的引用const volVectorField g fvc::grad§; 可能悬空需要就地修改时用相应 ref 接口并保证语义清晰。
返回列表