[关闭]
@gunshooter 2020-06-23T08:12:02.000000Z 字数 6229 阅读 3881

编写自己的求解器

OpenFOAM


创建于:20200621


0.0 目的

如果不写自己的求解器,那么学这个软件将毫无意义。而写自己的求解器,常见的,无外乎几种目的:

这些需求里面,其实除了第一条是需要改动求解器框子的,剩下的无非是在一般计算的中间插入一些计算步骤罢了。所以下面写的时候我先从最简单的求解器(简单方程、没有源项、常系数)开始写起,然后单独处理上述若干种不同情况。

ps 还有一种高级情况需要修改求解器,那就是不同的离散格式。这个我不太懂,感觉比较高端,以后再慢慢考虑吧。。
pps 这个笔记局限在不可压粘性流体里面。可压缩和欧拉方程我基本不懂。。。

0.1 顺序

所谓顺序,就是一个求解器从头到尾要干的事情。初学者对这个往往没概念导致看求解器代码的时候云山雾罩的。这里写清楚一点:

  1. #include "createMesh.H"
  1. #include "createFields.H"

但实际上没那么easy,因为具体要读取什么变量和系数每个求解器不太一样,有的时候所以要自己修改一些内容。

  1. pEqn.solve();

或者

  1. fvVectorMatrix UEqn(...);
  2. solve(UEqn == -fvc::grad(p));

,但是of毕竟是个通用计算框架,里面有大量的考虑松弛、非正交修正的语句。目前这个笔记只涉及最简单的矩形结构化网格,多余的因素尽量扣除。

  1. runTime.write();

即可。

1. 创建一个空的求解器

当然可以从一个空文件从第一个字符开始敲,但是of提供了很多生成摸板的工具。19年初去学软件的时候就有所耳闻,但是如何创建一个空的求解器还是今天看了一个教程才知道的,那就是使用命令:

  1. foamNewApp mySolver

这样就可以创建一个路径,里面有一个叫做mySolver.C的文件,文件里有写好的文件头和主函数的框子等等。这个源文件外头还有一个Make路径,Make里面也已经有了现成的files和options 文件了。
值得注意的是,这句命令最好在applications目录下运行,这样新生成的solver会和系统的求解器放在一个路径下,避免新手搞不定编译的问题。

2. 创建网格对象

在新建的空求解器中加入一句话:

  1. #include "createMesh.H"

即可。这句话应该是将求解器路径下的/constant/polyMesh下的网格文件读入程序,编程一个对象。

3. 读取初场和系数:creatFields.H

接下来就是读取初场和系数。在上一句下面加一句:

  1. #include "createFields.H"

当然这个要自己写,原因前面已经写过。所以我们在.C文件的同一个路径下新建一个creatFields.H文件。里面的内容,按照我们的示例方程也就是


来写。那么我们要读入的变量就是 h,要读入的系数就是
变量和系数的读入(和读出,因为有些量,不只是变量是要输出来给人看的)在of里通过对象注册机来完成。新手完全没必要琢磨这是个啥,只要知道变量和系数是这样写入写出的就vans了。

读取系数

先读,仿照scalarTransportFoam的写法:

  1. Info<< "Reading transportProperties\n" << endl;
  2. IOdictionary transportProperties
  3. (
  4. IOobject
  5. (
  6. "transportProperties",
  7. runTime.constant(),
  8. mesh,
  9. IOobject::MUST_READ_IF_MODIFIED,
  10. IOobject::NO_WRITE
  11. )
  12. );

这一段原封不动抄进去就行。到这里是创建了一个IOdictionary的对象,叫做TransportProperties。创建的时候就用IoObject来初始化。如何初始化呢?首先从从/constant路径下的叫做"transportProperties"的文件里读入数据,这分别是IoObject里面前两行的意思。剩下的就是把这个东西注册(也就是写到)mesh上,mesh是第二步里创建出来的东西。
ok,现在我们有一个对象叫做transportProperties,这个对象是IOdictionary类的,也就拥有了这个类下的各种函数。那么我们要用的就是查找具体系数的功能。虽然这个例子里只有一个系数,看上去多次一举。但是真正的求解器会有若干个系数,所以要分别处理。写入的语句是:

  1. Info<< "Reading diffusivity DT\n" << endl;
  2. dimensionedScalar kappa
  3. (
  4. transportProperties.lookup("kappa")
  5. );

可以看到,我们创建了一个对象叫做kappa,是一个dimensionedScalar类的对象。这个对象初始化的时候值从哪来?使用lookup函数从文件里找来。这个函数从哪里来?从我们上一步创建的transportProperties里来。上一步我们说,transportProperties是个IOdictionary类的对象,lookup函数也是从这个类里来的。

读取初场

ok,接下来搞初场。我们的方程只有一个变量,也就只有一个初场。这个初场放在./0路径下面,就叫做h。读取的方式跟上面差不多。上面我们读取系数的时候创建了一个系数属于的对象,那么现在我们也要创建一个变量属于的对象。此处是h,是个标量所以我们要创建一个标量场volScalarField。在of里,写成:

  1. Info<< "Reading field T\n" << endl;
  2. volScalarField h
  3. (
  4. IOobject
  5. (
  6. "h",
  7. runTime.timeName(),
  8. mesh,
  9. IOobject::MUST_READ,
  10. IOobject::AUTO_WRITE
  11. ),
  12. mesh
  13. );

关于为什么这里有两个mesh,以及对象注册机的写详细说明,参阅资料123。从我个人的感觉来说,这部分属于软件架构和数据结构的内容,涉及的是如何把软件写的高效,并不涉及太多cfd的内容,如果仅仅是在of的框架下编写自己的求解器或许不需要太过在意。。。。

4.方程离散

接下来的任务是创建一个离散的方程组。方程组在of中一般以fvVectorMatrix或者fvScalarMatrix的类的对象存在。前者是用来储存速度这类矢量,压力、温度、vof相函数等标量储存在后者。此处我们创建一个fvScalarMatrix的对象hEqn:

  1. fvScalarMatrix hEqn
  2. (
  3. fvm::ddt(h)
  4. == fvm::laplacian(kappa, h)
  5. );

可以看见这里的写法很类似与真实的数学写法。
常见的微分算符,其实就是瞬态微分,散度和laplace,分别写作ddt,div,laplacian。这三者又分别属于fvm和fvc两个命名空间,fvm是隐式,fvc是显式,取决与需要的离散格式。
当然还有一个要注意的地方是时间循环。在of里没有真正的稳态计算(?)所有的问题都是瞬态的,只是在稳态问题中可以放大时间步让方程迅速收敛到稳态而已(此处存疑)。所以在写稳态求解器的时候一样要写上瞬态项。
既然有瞬态项就必须有步进策略,而在of中步进一般由simple算法的框架来完成,其实这种简单求解器完全不需要simple算法,此处只是为了通用性而调用simple而已。
具体的语句是,在此前,就使用mesh生成一个叫做simple的对象,属于simpleControl类,语句是:

  1. simpleControl simple(mesh);

接下来,在上述离散步骤的外面需要套一个循环:

  1. while (simple.loop(runTime))
  2. {
  3. }

5.方程求解

构建好方程组之后,只需要一个简单的solve就能搞定求解:

  1. hEqn.solve();

6. 结果输出

每个时间步都需要把结果输出来,所以加上一句:

  1. runTime.write();

整个求解器就算初具雏形了。看一下整体的样子:

  1. #include "fvCFD.H"// 必备头文件
  2. #include "fvOptions.H"// 与源项有关,目前不涉及可以删掉
  3. #include "simpleControl.H"// 时间推进有关
  4. int main()// 主函数
  5. {
  6. #include "setRootCaseLists.H"//必备
  7. #include "createTime.H"//创建时间对象
  8. #include "createMesh.H"//创建mesh 后续频繁使用
  9. simpleControl simple(mesh);//创建simple对象,用于控制时间推进
  10. #include "createFields.H"//创建场,读取初场和系数
  11. Info<< "\nCalculating h transport\n" << endl;//提示计算开始
  12. #include "CourantNo.H"// 计算库朗数
  13. while (simple.loop(runTime))// 时间循环开始
  14. {
  15. Info<< "Time = " << runTime.timeName() << nl << endl;//输出当前时间
  16. while (simple.correctNonOrthogonal())//非正交修正,本求解器不涉及可以删掉
  17. {
  18. fvScalarMatrix hEqn//创建线性方程组
  19. (
  20. fvm::ddt(h)
  21. == fvm::laplacian(kappa, h)
  22. );
  23. hEqn.relax();//松弛,可以删掉
  24. // fvOptions.constrain(hEqn);//与源项有关,目前不涉及,可以删掉
  25. hEqn.solve();//求解
  26. //fvOptions.correct(h);//同上
  27. }
  28. runTime.write();//输出
  29. }
  30. Info<< "End\n" << endl;//提示计算结束
  31. return 0;
  32. }

接下来要做的是把源代码编程一个可执行程序。所有的solver都是可执行程序。所以需要编译。编译通过wmake即可,之前的Make文件夹就是为这个准备的。

6.5 一个插曲

实际上示例的方程还是有点麻烦,因为实际上是:


因为上述被动标量方程只能处理单个变量,所以应该写成:

这样子有2种处理方法:

  1. fvScalarMatrix thetaEqn
  2. (
  3. fvm::ddt(theta)
  4. );
  5. solve(thetaEqn == fvm::laplacian(kappa,h));//求解第一个方程得到theta
  6. h = -1*pow(-1600000+341532/(theta-0.075),0.252525);//代入求解h

没想到竟然支持这么狂野的表达式,竟然编译通过了。。。
等后面网格画好跑一下试试。

7. 编译

使用wmake,这个of自带的编译系统即可。因为之前我们使用foamNewApp命令生成了Make,内容都不用修改。直接在在当前求解器目录下:

  1. wmake

就vans了。
这部分有个很强的教程可以参考。

8. 运行求解器

求解的前提是准备网格+初场。我不太喜欢blockMesh,又学不太会snappyHexMesh,所以网格都用商软来画,画完转换一下。这部分的内容见另一个笔记:使用ICEM生成的网格进行计算

9. 变系数问题

10. 源项的处理

fvOptions

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注