参考:

感谢原作者们的无私引路和宝贵工作。

前置:
https://aerosand.cn/categories/CFD/

强烈建议在学习过计算流体力学基础以及有限体积法之后再开始本系列学习。至少也要同时开始,不然最后可能很难理解 OpenFOAM 求解器标准算法。

OpenFOAM 是什么呢?引用 wiki 解释如下

OpenFOAM (for “Open-source Field Operation And Manipulation”) is a C++ toolbox for the development of customized numerical solvers, and pre-/post-processing utilities for the solution of continuum mechanics problems, most prominently including computational fluid dynamics (CFD).

我们从简单的 C++ 程序实现开始,简单了解编译原理,通过 makefile 逐渐掌控我们的项目,过渡到了解 OpenFOAM 的 Make 实现方式,然后认识 OpenFOAM 的基本程序,最后逐渐深入 OpenFOAM 的程序开发。

鉴于 OpenFOAM 的使用环境,我们选择在 ubuntu 22.04 系统环境中,基于 OpenFOAM 2306 版本进行开发,方便起见使用 vscode 工具。

OpenFOAM 的安装,vscode 的安装,快捷命令的自定义等讨论参见 OpenFOAM 环境准备 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn)

原生实现

我们使用 C++ 写一段简单的 “hello world” 程序。

hello world

对于 OpenFOAM 来说,不管是求解器还是算例,放在任何一个文件夹都可以。放在 $FOAM_RUN 路径下也是为了方便管理。

比如,此系列讨论中,我们把项目放在自建文件夹 /userPath/ofsp 之下(/userPath/ 即用户自定义的文件夹路径,用户替换成自己的路径),并定义了快捷命令 ofsp 快速跳转到该路径下。快捷命令的自定义使用请参考 OpenFOAM 环境准备 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn)

我们使用如下约定

  • userPath/:用户指定路径,该路径下再新建例如 userPath/ofsp/ 文件夹,ofsp/ 下再新建各个项目
  • userProject/:用户指定项目路径,在 userPath/ofsp/userProject/ 文件下存放具体的项目文件

例如,新建总项目文件夹(// terminal 表示下面命令需要在终端输入执行)

1
2
3
// terminal
cd /userPath/ofsp
ofsp

ofspOpenFoam sharing programming 的缩写

在用户路径下,建立本章节的项目文件夹。

详细终端命令演示如下

1
2
3
4
// terminal
ofsp
mkdir ofsp_00_helloWorld_cpp
code ofsp_00_helloWorld_cpp

通过 vscode 打开项目后,可以使用 ctrl + ~ 唤出 vscode 的终端控制台,快捷进行命令操作。

终端中使用命令新建文件

1
2
// terminal
code main.cpp Aerosand.cpp Aerosand.h

终端中可以使用 tree 命令查看文件树状结构(命令参考 OpenFOAM 常用指令 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 (aerosand.cn)

最后,我们能看到文件结构为

1
2
3
4
|- ofsp_00_helloWorld_cpp/
|- Aerosand.h
|- Aerosand.cpp
|- main.cpp

我们约定在文件表示中,名称后加了 / 的表示文件夹,没有的表示文件。

我们分别写入示例代码,内容如下

类的声明 Aerosand.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Aerosand.h

#pragma once

class Aerosand
{
public:
void SetLocalTime(double t);
double GetLocalTime() const;


private:
double localTime_;
};

代码风格:

本系列讨论的代码尽量贴近 OpenFOAM 的代码风格,比如私有成员变量名称尾缀下划线。

类的定义 Aerosand.cpp

1
2
3
4
5
6
7
8
9
10
11
12
// Aerosand.cpp

#include "Aerosand.h"

void Aerosand::SetLocalTime(double t) {
localTime_ = t;
}

double Aerosand::GetLocalTime() const {
return localTime_;
}

主源码 main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.cpp

#include <iostream>

#include "Aerosand.h"

using namespace std;

int main()
{
int a = 1;
double pi = 3.1415926;

cout << "Hi, OpenFOAM!" << " Here we are." << endl;
cout << a << " + " << pi << " = " << a + pi << endl;
cout << a << " * " << pi << " = " << a * pi << endl;


Aerosand mySolver;
mySolver.SetLocalTime(0.2);
cout << "\nCurrent time step is : " << mySolver.GetLocalTime() << endl;

return 0;
}

虽然我们笼统的把代码到程序的整个过程称为“编译”,实际上,在 Linux 系统下,C++ 程序的编译过程分为四个过程。

1
预处理 -> 编译 -> 汇编 -> 链接

我们将逐步拆解并实现各个环节。

预处理

将程序代码中包含的 #include 文件插入替换到原文件中,在 Linux 系统下生成中间预处理输出文件(后缀 .i 文件)。

我们在终端里通过命令执行这一过程

1
2
3
// terminal 
g++ -E Aerosand.cpp -o Aerosand.i
g++ -E main.cpp -o main.i
  • g++ 的 -E 标识指定编译器进行预处理
  • g++ 的 -o (小写)标识指定编译器生成文件的名称

结果生成 Aerosand.imain.i 文件。

后缀 .i 表示 intermediate preprocessor output 中间预处理输出

编译

编译器对预处理后的中间预处理输出文件进一步语法分析,在 Linux 系统下生成汇编语言形式的源文件(后缀 .s 文件)。

终端中执行编译过程

1
2
3
// terminal
g++ -S Aerosand.i -o Aerosand.s
g++ -S main.i -o main.s

g++ 的 -S 标识指定编译器进行编译(大写 S

结果生成 Aerosand.smain.s 文件。

后缀 .s (小写)表示 source code written in assembly 汇编语言形式的源文件

汇编

编译器把编译后的汇编语言形式的源文件转换成可执行机器指令,在 Linux 系统下生成目标文件

终端中执行汇编过程

1
2
3
// termianl
g++ -c Aerosand.s -o Aerosand.o
g++ -c main.s -o main.o

g++ 的 -c (小写)标识指定编译器进行汇编

结果生成 Aerosand.oof00_0.o 文件。

后缀 .o (小写)表示 object file 目标文件

动态库

当项目中有大量“类”的时候,我们希望某些“类”能固定下来提供某种“方法”,这种“方法”就形成一个可以重复使用的“库”(library)。当其他项目使用这个库的时候,库本身无需再次“预处理”,“编译”以及“汇编”,仅仅和这个项目链接即可。

因为静态库开销大,浪费空间,更新维护困难,所以 OpenFOAM 大量使用动态库,我们这里也只以动态库为例。

动态库在程序编译时并不链接到目标代码,而仅仅在程序运行时才被链接载入。不同的程序如果调用相同的库,那么内存里只需要一份该动态库的可分享实例,这样就大大减少了空间浪费。此外,因为动态库仅在程序运行时才被链接载入,所以库的单独维护更新也十分方便。

编译器可以对汇编后的 .o 目标文件进行整理形成动态库,在 Linux 系统下生成 .so 文件。

终端中执行生成动态库命令

1
2
// terminal
g++ -shared -fPIC Aerosand.o -o libAerosand.so
  • g++ 的 -shared 标识指定生成动态链接库
  • g++ 的 -fPIC 标识指定创建与地址无关的编译程序,f 即 file,PIC 即 position independent code
  • 动态库文件以 lib 开头

生成 Aerosand.so 文件,该文件就是可链接的动态库。

后缀 .so 代表着 shared object 共享目标

链接

编译器把所有的目标文件以及库文件都组织成一个可以执行的二进制文件。

使用 echo 命令查看原本动态库链接路径,可以发现并不是项目本地路径。临时指定动态库路径为当前文件夹(不要担心,临时指定不影响 OpenFOAM 动态库路径的环境配置)。

1
2
3
4
// terminal
echo $LD_LIBRARY_PATH

export LD_LIBRARY_PATH=.

终端中将主源码目标文件和动态库链接,生成可执行文件

1
2
// terminal
g++ main.o -L. -lAerosand -o main
  • g++ 的 -L 标识紧跟指定的库的路径, 使用 -L. 表示动态库在当前路径
  • g++ 的 -l 标识紧跟指定的库的名称,使用时省略 lib 字段

最终生成 main 可执行程序。

注意,上面指定的动态库路径是临时的,如果重启计算机,想要再次运行 main 程序,必须要再次指定动态库路径。

需要知道的是,无论是把新开发的库放在本项目下,或是其他特定路径下,每个项目都可以链接使用这个动态库,只要指定正确的链接路径。这也是动态库“相对独立”“自由链接”的意义所在。

运行

总结整个过程如下

编译过程

运行该先项目

1
2
// terminal
./main

运行结果为

1
2
3
4
5
Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159

Current time step is : 0.2

make 实现

上一节的编译过程虽然清晰,但是一步一步的执行十分繁琐。

为了简化项目编译,以及兼顾理解编译细节,我们采用 make 工具来管理我们的开发项目。很多人也会使用 cmake 来构建项目,cmake 更加简洁高效,不过本质上也是基于 makefile 的。

我们可以为项目提供 makefile 文件通用的描述所有执行步骤。这样只需要简单执行 makefile 文件就可以编译整个项目,大大方便调试运行。此外,上面项目的所有代码文件都放在一起,十分不方便,所以我们对代码进行分类管理。

项目准备

我们建立新项目 ofsp_00_helloWorld_make

1
2
3
4
// termianl
ofsp
mkdir ofsp_00_helloWorld_make
code ofsp_00_helloWorld_make

文件结构如下:

1
2
3
4
5
6
7
|- ofsp_00_helloWorld_make/
|- Aerosand/
|- Aerosand.h
|- Aerosand.cpp
|- makefile
|- ofsp_00_helloWorld_make.cpp
|- makefile

在我们早期开发的很多情况下,开发的库只是某个项目的特定库。所以,我们仍然把包含众多 形成的 开发库 作为一个文件夹放进开发项目内。项目根目录下有项目主源码, 项目的 项目 makefile ,以及开发库文件夹。开发库文件夹内有库自己的 库 makefile。文件结构清晰,层次明确。

因为头文件路径有变化,主源码头文件包含修改为

1
#include "Aerosand/Aerosand.h"

其他代码的内容保持不变。

库 makefile

库的 makefile 只负责库的编译,目的是得到动态库,方便主程序链接使用。

makefile 文件将编译过程简化成“编译”和“链接”两部分,不再展开“预处理”,“汇编”的过程。编译过程把源代码编译成 .o 目标文件或者库文件等中间文件,“链接”过程把中间文件链接成最后可执行程序。

makefile 文件的基本格式为

1
2
<target>:  <support>
<command>

基于前文对 C++ 项目编译原理的过程的讨论,库 makefile 可以非常直白的写成

1
2
3
4
5
6
libAerosand.so: Aerosand.o
g++ -shared -fPIC Aerosand.o -o libAerosand.so

Aerosand.o: Aerosand.cpp
g++ -c -fPIC Aerosand.cpp -o Aerosand.o

  • 顺序重要。先指定动态库的编译,再指定汇编 .o 文件的编译。
  • <support> 部分不包括 .h 文件

进入库文件夹,执行 makefile

1
2
3
// terminal
cd Aerosand
make

清理的时候自行删除 .o 文件和 .so 文件即可。

可以看到库文件夹下生成了我们需要的 libAerosand.so 文件。

当代码文件繁多的时候,这么直白的写 makefile 效率低,不利于维护,我们优化 makefile 写法到通用形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# MACRO

CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -g -c
LDFLAGS := -shared -fPIC

NAME := $(shell basename $(CURDIR))
SOURCES := $(wildcard *.cpp)
TARGET := lib$(NAME).so
OBJECTS := $(NAME).o

# compile
$(TARGET): $(OBJECTS)
$(CXX) $(LDFLAGS) $(OBJECTS) -o $(TARGET)

$(OBJECTS): $(SOURCES)
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(OBJECTS)

# make clean
clean:
rm -rf *.so
rm -rf *.o

希望读者不要对 makefile 的写法感到担心,需要知道的都写在本文里了,陌生的语法知道可以这么使用即可,不需要花费时间了解原因和原理。

给出必要的解释如下:

第一段定义了一些命令的宏变量,方便第二段命令的书写。注意获取当前文件夹名称 NAME 的写法,获取当前路径下所有源文件 SOURCE 的写法。

第二段使用宏变量写了编译命令,本质上和原生写法没有什么区别。

第三段自定义了 make clean 命令。注意 Linux 系统下常见的类似写法,例如 *.so 指代所有的 .so 文件。

动态库编译(确认在库路径下)

1
2
3
4
// terminal
pwd
make clean
make

结果生成了动态库 userPath/ofsp_00_helloWorld_make/Aerosand/libAerosand.so 文件(注意路径)。

项目 makefile

有了上节的经验,我们直接写项目 makefile 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# MACRO
CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -g
LDFLAGS := -shared -fPIC

NAME := $(shell basename $(CURDIR))

# specify the path of library here
DLPATH := -L./Aerosand -lAerosand

SOURCES := $(wildcard *.cpp)
OBJECTS := $(NAME).o

OUTPUT := $(NAME)


# config library path
export LD_LIBRARY_PATH=./Aerosand

# compile
$(OUTPUT) : $(OBJECTS)
$(CXX) $(CXXFLAGS) $(OBJECTS) $(DLPATH) -o $(OUTPUT)

$(OBJECTS) : $(SOURCES)
$(CXX) $(CXXFLAGS) -c $(SOURCES) -o $(OBJECTS)


# make run
run:
./$(OUTPUT)

# make clean
clean:
rm -rf *.so
rm -rf *.o

如果主源码没有先行单独的生成目标文件,而是通过 -c 标识把编译和链接放在一句指令内写完,编译后的文件将无法执行,这是因为 -c 标识指定的生成文件不能直接运行。

程序编译(注意要离开库文件夹, cd 到本项目根目录下)

1
2
3
4
5
6
7
// terminal
cd ..
make
make run

// if clean
make clean

注意,makefile 中的 compile 部分临时指定了动态库链接路径。

运行结果如下

1
2
3
4
5
Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159

Current time step is : 0.2

vscode 插件

这里插入一点题外话。

对于一般的 C++ 项目,可以使用 vscode 的插件 C/C++ Project Generator,操作和备注如下

  • 基于 makefile 的多文件项目模版
  • 使用 F1 打开快捷命令输入,使用 Create C++ project
  • 弹出窗口中选择到准备好的空白项目文件夹,并打开
  • src/main.cpp 中开发主函数代码
  • include/ 路径下开发头文件的定义,在 src/ 路径下开发头文件的实现
  • 终端使用命令 make 编译此项目,make run 编译并运行,make clean 清理项目
  • 也可以使用 ./output/main 直接运行该主程序

也可以使用 vscode 的插件 c cpp cmake project,操作和备注如下

  • 基于 CMakeList 的多文件项目模板
  • 使用 F1 打开快捷命令输入,使用 CMake Project: Create Project
  • 弹出窗口中选择到准备好的空白项目文件夹,并打开
  • src/main.cpp 中开发主函数代码
  • include/ 路径下开发头文件的定义,在 src/ 路径下开发头文件的实现
  • 终端使用命令 cmake build/ 进行项目编译
  • 终端使用命令 make -C build/ 进行项目最后编译
  • 终端使用命令 ./build/xxx 运行该主程序

wmake 实现

在理解了 C++ 项目的 make 实现方式之后,我们现在可以更加容易的明白 OpenFOAM 提供的 wmake 实现方式。

可以找到 OpenFOAM 的 wmake 文件夹。

1
2
// terminal
cd $WM_PROJECT_DIR/wmake

大概扫一眼文件构成,简单来说, OpenFOAM 使用 wmake 管理应用,wmake 是基于 make 的一个脚本,但是提供了针对 OpenFOAM 的特性。

OpenFOAM 使用 Make/filesMake/options 实现 wmake 的编译管理,下面我们具体使用一下就会明白。

OpenFOAM 约定源文件后缀为 .C,头文件后缀为 .H 。为了让读者不要对文件架构感到困扰,这里作了不严谨的区分。OpenFOAM “应用层面” 的 .H 文件更多只是为了对主源码按功能进行拆分,方便代码阅读和维护,很多并不是类的头文件。这和 C++ 开发层面的头文件以及 OpenFOAM “源码层面”的头文件(如 $FOAM_SRC/OpenFOAM/dimensionSet.H)有些不同。

本文下面讨论的都是“源码层面”的头文件,暂无“应用层面”的头文件。

项目准备

OpenFOAM 中,应用(application)包括

  • 求解器(solvers)
  • 工具(tools)
  • 实用程序(utilities)

在本系列讨论中,我们所说的 “项目” 是完整解决某个问题的所有内容,包括

  • 求解器
  • 调试算例
  • 调用库
  • 工具
  • 实用程序
  • 等等

建立应用(application)/userPath/ofsp/ofsp_00_helloWorld_wmake 。进入该文件夹,写入源代码。

1
2
3
// terminal
ofsp
mkdir ofsp_00_helloWorld_wmake

先行给出代码内容如下:

Aerosand 库头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Aerosand.H

#pragma once

class Aerosand
{
public:
void SetLocalTime(double t);
double GetLocalTime() const;


private:
double localTime_;
};

Aerosand 库源文件

1
2
3
4
5
6
7
8
9
10
11
12
// Aerosand.C

#include "Aerosand.H"

void Aerosand::SetLocalTime(double t) {
localTime_ = t;
}

double Aerosand::GetLocalTime() const {
return localTime_;
}

应用主源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ofsp_00_helloWorld_wmake.C

#include <iostream>

#include "Aerosand.H"

using namespace std;

int main()
{
int a = 1;
double pi = 3.1415926;

cout << "Hi, OpenFOAM!" << " Here we are." << endl;
cout << a << " + " << pi << " = " << a + pi << endl;
cout << a << " * " << pi << " = " << a * pi << endl;


Aerosand mySolver;
mySolver.SetLocalTime(0.2);
cout << "\nCurrent time step is : " << mySolver.GetLocalTime() << endl;

return 0;
}

  • 注意每个代码文件的最后都需要留个一个空白行,否则 OpenFOAM 会警告 parse error
  • 类的头文件不再需要相对路径

OpenFOAM 提供了 Make 文件来帮助开发,其中

  • /Make/files 指定编译需要的所有源文件和目标路径以及目标文件名称
  • /Make/options 指定 application 或者库所需要包含的头文件、链接的其他库文件(包含路径)

类似把 makefile 文件换成了 OpenFOAM 的 Make 文件

硬链接

硬链接并不需要单独编译库文件,而是将库文件直接包含在主应用上,所以这种情况不需要准备库自己的 Make 文件。

代码文件结构如下

1
2
3
4
5
6
7
8
|- ofsp_00_helloWorld_wmake/
|- Aerosand/
|- Aerosand.h
|- Aerosand.cpp
|- ofsp_00_helloWorld_wmake.cpp
|- Make/
|- files
|- options

文件 ofsp_00_helloWorld_wmake/Make/files 内容如下

1
2
3
4
5
Aerosand/Aerosand.C
ofsp_00_helloWorld_wmake.C

EXE = $(FOAM_USER_APPBIN)/ofsp_00_helloWorld_wmake

此应用需要“包括”自定义库 Aerosand。

这里用词使用“包括”而不是“链接”,为了强调 Aerosand 并没有生成自己的独立库文件(如静态库 .a 文件或者动态库 .so 文件)

文件 ofsp_00_helloWorld_wmake/Make/options 内容如下

1
2
3
EXE_INC = \
-IAerosand

因为前文临时定义了库的链接路径,这里需要可能重启终端,以恢复 OpenFOAM 的环境配置。

执行编译

1
2
3
// terminal 
wclean
wmake

终端输出信息有三段,对应着应用编译的三个过程。

第一段是自定义的 Aerosand 库编译得到目标文件 Aerosand.o (见输出信息的末尾处)

1
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100  -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -c Aerosand/Aerosand.C -o Make/linux64GccDPInt32Opt/Aerosand/Aerosand.o

第二段是主源码编译得到目标文件 of00_2.o (见输出信息的末尾处)

1
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100  -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -c ofsp_00_helloWorld_wmake.C -o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o

第三段是链接的过程,链接自定义库或者其他的动态链接,最终生成可执行文件(见输出信息的末尾处)。

1
2
3
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100  -IAerosand -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -Xlinker --add-needed -Xlinker --no-as-needed  Make/linux64GccDPInt32Opt/Aerosand/Aerosand.o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \
-lOpenFOAM -ldl \
-lm -o /home/aerosand/OpenFOAM/aerosand-v2312/platforms/linux64GccDPInt32Opt/bin/ofsp_00_helloWorld_wmake

编译的过程文件在 ofsp_00_helloWorld_wmake/Make/linux64GccDPInt32Opt/ 文件夹下(根据平台可能会有所不同)。编译形成的可执行程序在 $FOAM_USER_APPBIN 文件夹下。

这个可执行程序不需要从任何外部文件读取参数,可以在任何路径下通过终端命令直接运行。

我们可以直接运行这个应用

1
2
// terminal
ofsp_00_helloWorld_wmake

运行结果如下

1
2
3
4
5
Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159

Current time step is : 0.2

动态库

在实际的开发中,我们还是要使用动态库来保证内存和效率。

我们调整文件结构如下

1
2
3
4
5
6
7
8
9
10
11
|- ofsp_00_helloWorld_wmake/
|- Aerosand/
|- Aerosand.H
|- Aerosand.C
|- Make/
|- files
|- options
|- ofsp_00_helloWorld_wmake.C
|- Make/
|- files
|- options

注意此时的文件结构,库自己单独拥有一套 库 Make

库 Make

为库创建 Make 文件

文件 ofsp_00_helloWorld_wmake/Aerosand/Make/files 内容如下

1
2
3
4
Aerosand.C

LIB = $(FOAM_USER_LIBBIN)/libAerosand

  • 注意库使用 LIB 而不是 EXE
  • 库的目标路径结尾是 LIBBIN
  • 目标文件需要加 lib

因为这个 Aerosand 库的实现不再进一步需要链接其他库,所以 userApp/Aerosand/Make/options 直接空置即可。

根目录下编译这个库

1
2
3
// terminal

wmake Aerosand

终端输出信息有两段,我们关注每段输出的最后部分

第一段是自定义的 Aerosand 库编译得到目标文件 Aerosand.o (见输出信息的末尾处)

1
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100   -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -c Aerosand.C -o Make/linux64GccDPInt32Opt/Aerosand.o

第二段将目标文件 Aerosand.o 编译成动态库 Aerosand.so (见输出信息的末尾处)

1
2
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100   -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -shared -Xlinker --add-needed -Xlinker --no-as-needed  Make/linux64GccDPInt32Opt/Aerosand.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \
-o /home/aerosand/OpenFOAM/aerosand-v2312/platforms/linux64GccDPInt32Opt/lib/libAerosand.so

可以通过命令 cd $FOAM_USER_LIBBIN 确认 Aerosand 库的编译结果文件 libAerosand.so 的所在位置。

因为库内的类可能会有多个,甚至还有其他子库,所以库编译后会同时在库的目录下生成 lnInclude 文件夹,lnInclude 包含了该库所有类(/子库)的声明( .H 文件)或者实现 .C文件 的快捷方式,方便后续链接的时候可以提供统一路径。可以参考 OpenFOAM 的 $FOAM_SRC/OpenFOAM 库,可以看到根目录下有 lnInclude 文件夹,其中包含了此库内的所有类(/子库)的快捷方式。

我们在终端使用 find 命令搜索 OpenFOAM 源代码文件的时候,往往能看到相近的路径 lnInclude/ 下也有一个同名文件,这个就是快捷方式。我们一般选择打开阅读本体。

应用 Make

因为 Aerosand 库已经被编译成了动态库,所以我们需要在应用的 Make/options 文件中为应用提供动态库链接。Make/files 指定的编译不再需要该动态库的源代码。

修改 userApp/Make/files 文件

1
2
3
4
ofsp_00_helloWorld_wmake.C

EXE = $(FOAM_USER_APPBIN)/ofsp_00_heloWorld_wmake

修改 userApp/Make/options 文件

1
2
3
4
5
6
7
EXE_INC = \
-IAerosand/lnInclude

EXE_LIBS = \
-L$(FOAM_USER_LIBBIN) \
-lAerosand

标识符号

  • EXE_INC 指定需要包含的库
    • -I 同样用来指定包含库.H”,从 lnInclude 文件夹取得路径
  • EXE_LIB 指定需要链接的库
    • -L 同样用来指定链接库文件.so”的路径
    • -l 同样用来指定链接库文件.so”的名称(字母是 link 的 l,不是 include 的 i)

我们常见的 OpenFOAM 求解器的 Make/options 中没有 -L 指定只有 -l 指定,这是因为那些求解器中使用的都是原生库,链接路径已经得到配置,无需进行 -L 路径指定,仅进行 -l 名称指定即可。如果是用户自定义库,编译结果在其他文件位置,当然也需要进行 -L 路径指定。

一定要注意路径,路径对于成功编译非常重要。一般指定的路径都是从本地出发的(可以从本地路径出发,定义其他库的绝对路径,见下文演示)。

编译运行

编译应用

1
2
3
// terminal
wclean
wmake

此时终端信息分为两段,第一段将主源码编译成目标文件,第二段链接动态库到应用,生成可执行文件。

第一段是主源码编译得到目标文件 ofsp_00_helloWorld_wmake.o (见输出信息的末尾处)

1
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100  -IAerosand/lnInclude -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -c ofsp_00_helloWorld_wmake.C -o Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o

第二段链接动态库到应用,并生成可执行文件 ofsp_00_helloWorld_wmake (见输出信息的末尾处两句)

1
2
3
g++ -std=c++14 -m64 -pthread -DOPENFOAM=2312 -DWM_DP -DWM_LABEL_SIZE=32 -Wall -Wextra -Wold-style-cast -Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof -Wno-attributes -Wno-unknown-pragmas -O3  -DNoRepository -ftemplate-depth-100  -IAerosand/lnInclude -iquote. -IlnInclude -I/usr/lib/openfoam/openfoam2312/src/OpenFOAM/lnInclude -I/usr/lib/openfoam/openfoam2312/src/OSspecific/POSIX/lnInclude   -fPIC -Xlinker --add-needed -Xlinker --no-as-needed  Make/linux64GccDPInt32Opt/ofsp_00_helloWorld_wmake.o -L/usr/lib/openfoam/openfoam2312/platforms/linux64GccDPInt32Opt/lib \
-L/home/aerosand/OpenFOAM/aerosand-v2312/platforms/linux64GccDPInt32Opt/lib -lAerosand -lOpenFOAM -ldl \
-lm -o /home/aerosand/OpenFOAM/aerosand-v2312/platforms/linux64GccDPInt32Opt/bin/ofsp_00_helloWorld_wmake

可以通过 cd $FOAM_USER_APPBIN 确认应用的可执行程序被编译到了哪里。

运行编译生成的应用程序

1
2
// terminal
ofsp_00_helloWorld_wmake

运行结果如下

1
2
3
4
5
Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159

Current time step is : 0.2

结果和前面各种方式实现的结果相同。

OpenFOAM 类

前面我们已经了解了 C++ 原生实现、make 实现以及 OpenFOAM 的 wmake 实现。在正式进入到 OpenFOAM 实现之前,我们有必要稍微了解一点 OpenFOAM 中的类,以方便使用。

常见的类

OpenFOAM 中有大量的类。我们大概了解一些常见的类型/类:

下面可以在 OpenFOAM: API Guide: OpenFOAM®: Open source CFD : API 中分别查询。

  • Switch
    • OpenFOAM 为了用户使用简单方便,大量使用 Switch 供用户选择。Switch 本质上一种布尔类型。用户可以使用 falseoffnonf 表示否定选项,使用 trueonyesyt 表示肯定选项
  • label
    • 本质上是 OpenFOAM 定义的具有 INT_SIZE 大小的 integer data type
  • scalar
    • 本质上是 OpenFOAM 定义的具有 FLOAT_SIZE 或者 DOUBLE_SIZE 大小的 floating-point data type
  • vector
    • 矢量数据结构
    • 包含重载后的数学运算方法
  • tensor
    • 张量数据结构
    • 包含重载后的数学运算方法
  • dimensionedScalar/*Vector/*Tensor
    • 具有 OpenFOAM 单位系统的量
    • 包含名称、单位和取值等成员数据,以及相应的成员方法
  • scalar*/vector*/tensorField
    • 基础类型的列表值
    • 描述各种场
    • 包含数据和方法
  • dimensionSet
    • OpenFOAM 的单位系统
    • dimensionSet (const scalar mass, const scalar length, const scalar time, const scalar temperature, const scalar moles, const scalar current=0, const scalar luminousIntensity=0)
    • [质量, 长度, 时间, 温度, 摩尔, 电流, 光强]
    • OpenFOAM 也提供很多单位的组合 $FOAM_SRC/OpenFOAM/dimensionSet/dimensionSets.H
    • 以后会深入讨论
  • tmp<>
    • OpenFOAM 经常见到的处理临时数据的特殊类
    • 因为 C++ 很多时候的内存都需要手动管理,所以 OpenFOAM 为开发者提供此类
    • 无需深究
  • IOobject
    • 提供接入 OpenFOAM 各种数据结构的方法,本身没有成员数据
    • 以后会深入讨论

Vector 类

我们拿出 Vector 类看一下 OpenFOAM 对 Vector 类的实现。

更多代码的详细讨论参见 ofsc 系列。

找到 Vector 类,进入文件夹,

1
2
3
4
// terminal
cd $FOAM_SRC/OpenFOAM/primitives/Vector
l
// bools/ complex/ floats/ ints/ lists/ Vector.H VectorI.H

阅读 Vector.H 的介绍可知,该类是模板化的 3D Vector,继承自 VectorSpace 并添加了 3 分量的构造、分量元素的接口函数、点积和叉积的方法等。具体到 Vector.H 的代码,可以看到简单的方法在代码中直接实现,复杂方法则大量使用内联函数(inline funtion)以提高代码性能。OpenFOAM 特别提供 VectorI.H 文件来写内联函数的实现。该类没有 .C 文件,因为所有方法都是通过内联函数实现的。其他的文件夹 /bools/complex/floats/ints/lists 都是 Vector 类对不同基本类型的 typedef

我们大概看一下 Vector.H 代码

陌生的代码细节可以忽略,或者网上查询相关语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
namespace Foam
{

// Forward Declarations
template<class T> class List; // 提前声明

/*---------------------------------------------------------------------------*\
Class Vector Declaration
\*---------------------------------------------------------------------------*/

template<class Cmpt>
class Vector
:
public VectorSpace<Vector<Cmpt>, Cmpt, 3> // 继承自父类
{
public:

// Typedefs

//- Equivalent type of labels used for valid component indexing
typedef Vector<label> labelType; // 为了方便 typedef


// Member Constants

//- Rank of Vector is 1
static constexpr direction rank = 1; // 定义常量表达式


//- Component labeling enumeration
enum components { X, Y, Z }; // 枚举类型


// Generated Methods

//- Default construct
Vector() = default; // 默认构造

//- Copy construct
Vector(const Vector&) = default; // 默认拷贝构造

//- Copy assignment
Vector& operator=(const Vector&) = default; // 默认拷贝运算符重载


// Constructors

//- Construct initialized to zero
inline Vector(const Foam::zero); // 置零构造

//- Copy construct from VectorSpace of the same rank
template<class Cmpt2>
inline Vector(const VectorSpace<Vector<Cmpt2>, Cmpt2, 3>& vs);
// 基于 VectorSpace 对象的拷贝构造

//- Construct from three components
inline Vector(const Cmpt& vx, const Cmpt& vy, const Cmpt& vz);
// 基于3分量的构造

//- Construct from Istream
inline explicit Vector(Istream& is);
// 基于 Istream 的构造
...

比如,基于 3 分量的构造,其内联函数在 VectorI.H 中实现如下(Vector 后面加后缀 I 用来表示是对内联函数的实现)

1
2
3
4
5
6
7
8
9
10
11
12
template<class Cmpt>
inline Foam::Vector<Cmpt>::Vector
(
const Cmpt& vx,
const Cmpt& vy,
const Cmpt& vz
)
{
this->v_[X] = vx;
this->v_[Y] = vy;
this->v_[Z] = vz;
}
  • 成员数据 v_ 和重载的运算符 [] 来自父类 VectorSpace
    • 查看 $FOAM_SRC/primitives/VectorSpace/VectorSpace.H
  • 传入函数的参数为 vx, vy, vz,并使用常引用提高性能,保证数据安全
  • 函数将 3 分量参数对应传入类的成员数据
  • 模板化的函数,类型为 Cmpt
  • 如果我们自己写也可以参考这种类的架构

比如,3 分量的接口函数因为简单,就没有内联而直接跟在 Vector.H 对应的声明中直接实现,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Member Functions

// Component Access

//- Access to the vector x component
const Cmpt& x() const noexcept { return this->v_[X]; }

//- Access to the vector y component
const Cmpt& y() const noexcept { return this->v_[Y]; }

//- Access to the vector z component
const Cmpt& z() const noexcept { return this->v_[Z]; }

//- Access to the vector x component
Cmpt& x() noexcept { return this->v_[X]; }

//- Access to the vector y component
Cmpt& y() noexcept { return this->v_[Y]; }

//- Access to the vector z component
Cmpt& z() noexcept { return this->v_[Z]; }
  • 对常量数据和非常量数据定义了两套函数,提高性能
  • 标识符 noexcept 直接告诉编译器该函数不会发生异常,利于编译优化

比如,标量积和矢量积的声明和实现分别是

声明如下:

1
2
3
4
5
6
7
// Vector.H

//- Scalar-product of \c this with another Vector.
inline Cmpt inner(const Vector<Cmpt>& v2) const;

//- Cross-product of \c this with another Vector.
inline Vector<Cmpt> cross(const Vector<Cmpt>& v2) const;

对应的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// VectorI.H
template<class Cmpt>
inline Cmpt Foam::Vector<Cmpt>::inner(const Vector<Cmpt>& v2) const
{
const Vector<Cmpt>& v1 = *this;

return (v1.x()*v2.x() + v1.y()*v2.y() + v1.z()*v2.z());
}


template<class Cmpt>
inline Foam::Vector<Cmpt>
Foam::Vector<Cmpt>::cross(const Vector<Cmpt>& v2) const
{
const Vector<Cmpt>& v1 = *this;

return Vector<Cmpt>
(
(v1.y()*v2.z() - v1.z()*v2.y()),
(v1.z()*v2.x() - v1.x()*v2.z()),
(v1.x()*v2.y() - v1.y()*v2.x())
);
}
  • const 约束输入参数,需要使用常量 vector
  • const 约束函数体,函数体内部不能修改类的成员数据
  • 另外声明 v1 作为本类的成员数据 v_ 的引用
  • 返回 v1v2 的计算结果

根据类型 Cmpt 的不同,就实现了针对不同类型的方法。比如 Vector<scalar> 就是 scalar 类型的类实现。比如,在 /Vector/ints/labelVector.H 文件中说明了这一点,如下

1
typedef Vector<label> labelVector;

类似的,我们也可以找到 tensor.H 阅读和了解 tensor 类。

上面这些代码介绍主要是为了帮助读者慢慢熟悉 C++ 语言在 OpenFOAM 中的使用,克服对 C++ 语言的陌生和恐惧,便于读者理解下文要写的主源码。暂时不需要花费更多时间去阅读更多 OpenFOAM 的源代码,后续会在 ofsc 系列讨论代码。

项目准备

我们通过一个应用来使用 OpenFOAM 的自有类型/类。

1
2
3
4
// terminal
ofsp
mkdir ofsp_00_tensor
code ofsp_00_tensor

建立文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|- userProject/
|- Aerosand/
|- class1/
|- class1.H
|- class1.C
|- class2/
|- class2.H
|- class2.C
|- Make/
|- files
|- options
|- ofsp_00_tensor.C
|- Make/
|- files
|- options

注意,开发库的文件结构与前文稍有不同。

我们在前文已经可以注意到 OpenFOAM 库下一般有多个子库/类。

用户的开发库里可能也会由好几个类构成,开发库拥有自己的 Make 文件,集中管理多个类,比如这里有 class1class2

开发库

头文件 class1.H 如下

1
2
3
4
5
6
7
8
9
10
11
12
#pragma once

class class1
{
private:
double localTime_;

public:
void SetLocalTime(double t);
double GetLocalTime() const;
};

源文件 class1.C 如下

1
2
3
4
5
6
7
8
9
10
#include "class1.H"

void class1::SetLocalTime(double t) {
localTime_ = t;
}

double class1::GetLocalTime() const {
return localTime_;
}

头文件 class2.H 如下

1
2
3
4
5
6
7
8
#pragma once

class class2
{
public:
void class2Info() const;
};

源文件 class2.C 如下

1
2
3
4
5
6
7
8
9
#include "class2.H"

#include <iostream>

void class2::class2Info() const
{
std::cout << "This is class2\n" << std::endl;;
}

库 Make

为开发库写 Make 文件

文件 ofsp_00_tensor/Aerosand/Make/files 如下

1
2
3
4
5
class1/class1.C
class2/class2.C

LIB = $(FOAM_USER_LIBBIN)/libAerosand

本次开发库的实现没有再用到其他库,所以库文件 userProject/Aerosand/Make/options 空置。

将开发库编译成动态库

1
2
// terminal 
wmake Aerosand

顺利编译后,可以在 userProject/Aerosand/lnInclude 内看到类的快捷方式。

主源码

如果我们要使用 tensor 类,不仅要包含它的头文件,还需要包含相关的库。

查找 tensor 库

1
2
3
4
5
// terminal 
find $FOAM_SRC -name tensor.H
// $FOAM_SRC/OpenFOAM/lnInclude/tensor.H
// $FOAM_SRC/OpenFOAM/primitives/Tensor/floats/tensor.H
ls $FOAM_SRC/OpenFOAM

通过 ls 命令我们可以看到 OpenFOAM 拥有自己的 Make 文件以及 lnInclude ,所以 tensor 类属于 OpenFOAM 库。后续我们需要在应用中包含、链接 OpenFOAM 库。

同样的,我们想要使用 dimensionedTensortensorField

1
2
3
4
5
6
7
8
// terminal
find $FOAM_SRC -name dimensionedTensor.H
// $FOAM_SRC/OpenFOAM/lnInclude/dimensionedTensor.H
// $FOAM_SRC/OpenFOAM/dimensionedTypes/dimensionedTensor/dimensionedTensor.H

find $FOAM_SRC -name tensorField.H
// $FOAM_SRC/OpenFOAM/lnInclude/tensorField.H
// $FOAM_SRC/OpenFOAM/fields/Fields/tensorField/tensorField.H

可以看到这两个类也属于 OpenFOAM 库。

我们包含这些库,也包含自己的开发库

主源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "tensor.H"
#include "dimensionedTensor.H"
#include "tensorField.H"

#include "class1.H"
#include "class2.H"

using namespace Foam;

int main()
{
scalar s(3.14);
vector v1(1, 2, 3);
vector v2(0.5, 1, 1.5);
Info<< s << " * " << v1 << " = " << s * v1 << nl
<< "pos(s): " << pos(s) << nl
<< "asinh(s): " << Foam::asinh(s) << nl
<< endl;

tensor T(11, 12, 13, 21, 22, 23, 31, 32, 33);
Info<< "T: " << T << nl
<< "Txy: " << T.xy() << nl
<< endl;

tensor T1(1, 2, 3, 4, 5, 6, 7, 8, 9);
tensor T2(1, 2, 3, 1, 2, 3, 1, 2, 3);
tensor T3 = T1 + T2;
Info<< "T3: " << T3 << nl << endl;

tensor T4(3, -2, 1, -2, 2, 0, 1, 0, 4);
Info<< "T4': " << inv(T4) << nl
<< "T4' * T4: " << (inv(T4) & T4) << nl
<< "T4.x(): " << T4.x() << nl
<< "T4.y(): " << T4.y() << nl
<< "T4.z(): " << T4.z() << nl
<< "T4^T: " << T4.T() << nl
<< "det(T4): " << det(T4) << nl
<< endl;

dimensionedTensor sigma
(
"sigma",
dimensionSet(1, -2, -2, 0, 0, 0, 0),
tensor(1e6, 0, 0, 0, 1e6, 0, 0, 0, 1e6)
);
Info<< "sigma: " << sigma << nl
<< "sigma name: " << sigma.name() << nl
<< "sigma dimension: " << sigma.dimensions() << nl
<< "sigma value: " << sigma.value() << nl
<< "sigma yy value: " << sigma.value().yy() << nl
<< endl;

tensorField tf(2, tensor::one);
Info<< "tf: " << tf << endl;
tf[0] = tensor(1, 2, 3, 4, 5, 6, 7, 8, 9);
tf[1] = T2;
Info << "tf: " << tf << nl
<< "2.0 * tf" << 2.0 * tf << nl
<< endl;

label a = 1;
scalar pi = 3.1415926;
Info<< "Hi, OpenFOAM!" << " Here we are." << nl
<< a << " + " << pi << " = " << a + pi << nl
<< a << " * " << pi << " = " << a * pi << nl
<< endl;


class1 mySolver;
mySolver.SetLocalTime(0.2);
Info<< "\nCurrent time step is : " << mySolver.GetLocalTime() << endl;

class2 myMessage;
myMessage.class2Info();

return 0;
}

应用 Make

文件 ofsp_00_tensor/Make/files 如下

1
2
3
4
ofsp_00_tensor.C

EXE = $(FOAM_USER_APPBIN)/ofsp_00_tensor

原则上来说,我们应该在 userApp/Make/options 里面链接 OpenFOAM 库。实际上,因为 OpenFOAM 库是最基本的库,所以,所有的应用都默认隐含的自动链接 OpenFOAM 库,无需再显式的写明。

所以,我们只需要在 userApp/Make/options 里面链接用户的开发库。

1
2
3
4
5
6
7
EXE_INC = \
-IAerosand/lnInclude

EXE_LIBS = \
-L$(FOAM_USER_LIBBIN) \
-lAerosand

编译运行

1
2
3
4
// terminal
wclean
wmake
ofsp_00_tensor

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
3.14 * (1 2 3) = (3.14 6.28 9.42)
pos(s): 1
asinh(s): 1.86181

T: (11 12 13 21 22 23 31 32 33)
Txy: 12

T3: (2 4 6 5 7 9 8 10 12)

T4': (1.33333 1.33333 -0.333333 1.33333 1.83333 -0.333333 -0.333333 -0.333333 0.333333)
T4' * T4: (1 0 0 1.66533e-16 1 0 -5.55112e-17 0 1)
T4.x(): (3 -2 1)
T4.y(): (-2 2 0)
T4.z(): (1 0 4)
T4^T: (3 -2 1 -2 2 0 1 0 4)
det(T4): 6

sigma: sigma [1 -2 -2 0 0 0 0] (1e+06 0 0 0 1e+06 0 0 0 1e+06)
sigma name: sigma
sigma dimension: [1 -2 -2 0 0 0 0]
sigma value: (1e+06 0 0 0 1e+06 0 0 0 1e+06)
sigma yy value: 1e+06

tf: 2{(1 1 1 1 1 1 1 1 1)}
tf: 2((1 2 3 4 5 6 7 8 9) (1 2 3 1 2 3 1 2 3))
2.0 * tf2((2 4 6 8 10 12 14 16 18) (2 4 6 2 4 6 2 4 6))

Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159

Current time step is : 0.2
This is class2

fvCFD.H

在实际开发中,我们还需要用到更多的和 FVM 相关的类来离散求解偏微分方程。OpenFOAM 提供 fvCFD.H ,其中包含了大部分和 FVM 相关的头文件,包括 tensor 类等。使用 fvCFD.H 可以大大减少主源码要写的头文件数量。

我们找一下 fvCFD.H 文件

1
2
3
4
// terminal
find $FOAM_SRC -name fvCFD.H
// $FOAM_SRC/finiteVolume/lnInclude/fvCFD.H
// $FOAM_SRC/finiteVolume/cfdTools/general/include/fvCFD.H

显然 fvCFD.H 不在 OpenFOAM 库 里,而是在 finiteVolume 库 里,所以需要另外在 Make/options 中包含、链接。

我们将上述主源码中的非开发库头文件全部替换成 fvCFD.H

此时的 Make/options 文件应修改为(当然开发库链接仍然需要有)

1
2
3
4
5
6
7
8
9
10
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude \
-IAerosand/lnInclude

EXE_LIBS = \
-lfiniteVolume \
-lmeshTools \
-L$(FOAM_USER_LIBBIN) \
-lAerosand
  • finiteVolume 被显式的包含、链接
  • meshTools 补充了 finiteVolume 中涉及的其他库

重新编译运行,结果相同。

OpenFOAM 应用

在完全理解了上面关于编译过程讨论的种种,我们使用 OpenFOAM 提供的工具,串一个较为完整的应用开发流程。

项目准备

在用户文件夹下创建应用项目,进入该应用项目

1
2
3
4
// terminal
ofsp
foamNewApp ofsp_00_helloWorld_openfoam
code ofsp_00_helloWorld_openfoam

接着可以拷贝一个简单算例用于开发过程的测试(对于该项目来说,仅仅是为了通过算例检查)

1
2
// terminal 
cp -r $FOAM_TUTORIALS/incompressible/icoFoam/cavity/cavity debug_case

准备脚本

为了方便开发,可以创建脚本文件,下面举个例子。

1
2
// terminal
code caserun caseclean

脚本 caserun 主要是负责应用编译成功后,调试算例的运行,暂时写入如下内容

1
2
3
4
5
6
#!/bin/bash

blockMesh -case debug_case | tee debug_case/log.mesh
echo "Meshing done."

ofsp_00_helloWorld_openfoam -case debug_case | tee debug_case/log.run

caseclean 脚本主要是负责清理应用到到编译前状态,如果应用要修改,那么测试算例也要还原到运行前的状态,所以暂时写入如下内容

1
2
3
4
5
6
#!/bin/bash

wclean
rm -rf debug_case/log.*
foamCleanTutorials debug_case
echo "Cleaning done."

库的编译清理也可以类似的创建脚本,这里不多赘述。

我们可以简单的运行这些脚本,如下所示

1
2
// terminal
./caserun

如果运行提醒 Permission denied,那就需要给文件权限

1
2
// terminal
chmod +x caserun caseclean

说明文件

为了方便后续开发和使用,还应该准备说明文件。

1
2
// terminal
code README.md

写入需要的开发备忘、运行步骤、注意事项等内容。请不要吝啬时间,务必花费一点点时间把问题写清楚,不然日后转交他人,或者自己重新阅读项目,都会十分痛苦。

项目结构

所以,项目的文件结构如下

1
2
3
4
5
6
7
8
9
10
11
|- debug_case/
|- 0/
|- constant/
|- system/
|- of00.C
|- README.md
|- caserun
|- caseclean
|- Make/
|- files
|- options

我们准备在本项目中使用前文讨论的开发库,所以该项目路径下没有开发库文件。

主源码

打开主源码 ofsp_00_helloWorld_openfoam.C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 
* OpenFOAM things ...
*/

#include "fvCFD.H"

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

int main(int argc, char *argv[])
{
#include "setRootCase.H"
#include "createTime.H"

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

Info<< nl;
runTime.printExecutionTime(Info);

Info<< "End\n" << endl;

return 0;
}


// ************************************************************************* //

这里约定

  • /* comments */ 写在代码功能块之前,用于正式说明或者介绍此代码功能块
  • // comments 写在具体代码行附近,用于解释、标记或者想法等

代码解释

  • OpenFOAM 文件头内容
    • 文中均省略处理
  • #include "fvCFD.H"
    • 包含了所有的 FVM 方法,通常必须
  • #include "setRootCase.H"
    • 建立应用的参数列表的类,检查算例文件结构
    • 使用 find $FOAM_SRC -name setRootCase.H 去查阅
    • 后续会详细讨论
  • #include "createTime.H"
    • 创建 time 类的 runTime 对象,需要算例
    • 后续会详细讨论
  • #include "creatMesh.H"
    • 求解器应用需要该头文件
    • 创建 fvMesh 类的 mehs 对象
    • 后续会详细讨论
  • Info
    • OpenFOAM 提供的输出语法,适配 OpenFOAM 多种类型
  • nl
    • OpenFOAM 提供的换行符,和 endl 类似
    • 本系列约定,当存在大段输出的时候,中间换行用 nl ,结尾处用 endl
  • runTime.printExecutionTime(Info)
    • 打印时间相关信息

主源码开发

我们在主代码中添加简单的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "fvCFD.H"

#include "class1.H" // 调用我们想要的其他开发库头文件名称

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

int main(int argc, char *argv[])
{
#include "setRootCase.H"
#include "createTime.H"
#include "createMesh.H"

label a = 1;
scalar pi = 3.1415926;
Info<< "Hi, OpenFOAM!" << " Here we are." << nl
<< a << " + " << pi << " = " << a + pi << nl
<< a << " * " << pi << " = " << a * pi << nl
<< endl;

class1 mySolver;
mySolver.SetLocalTime(0.2);
Info<< "\nCurrent time step is : " << mySolver.GetLocalTime() << endl;

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

Info<< nl;
runTime.printExecutionTime(Info);

Info<< "End\n" << endl;

return 0;
}

注意,这里演示了如何链接使用其他开发库。

本项目调用的依然是上一节的开发库 Aerosand (见 #include class1.H )。所以,我们需要妥善处理链接问题。

应用 Make

默认生成的 userApp/Make/options 基础上添加自开发库

1
2
3
4
5
6
7
8
9
10
11
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude \
-I../ofsp_00_tensor/Aerosand/lnInclude

EXE_LIBS = \
-lfiniteVolume \
-lmeshTools \
-L$(FOAM_USER_LIBBIN) \
-lAerosand

  • 注意书写格式,比如标识符、行尾的 \ 换行
    • 原则上可以一行写完所有语句,虽然有警告,但不是不行
    • 为了方便维护,使用换行符 \ 来换行,换行符 \ 前后多加空格无所谓,后面如果有空格会有警告
  • EXE_INC
    • -I 标识开头列出所有需要包含的头文件的路径
    • $(LIB_SRC) 是环境变量
    • 可以等同替换为 -I$(FOAM_SRC)/finiteVolume/lnInclude
    • 或者直接写成绝对地址 -I/usr/lib/openfoam/openfoam2306/src/finiteVolume/lnInclude
    • 开发库的调用是相对地址 ../ofsp_00_tensor/Aerosand/lnInclude
  • EXE_LIBS
    • 小写 -l 标识列出库的名字
    • 大写 -L 标识列出库的路径

查看默认生成的 userApp/Make/file,其内容不需要修改。

1
2
3
ofsp_00_helloWorld_openfoam.C

EXE = $(FOAM_USER_APPBIN)/ofsp_00_helloWorld_openfoam

编译运行

编译并运行该应用

1
2
3
4
wclean
wmake
./caseclean
./caserun
  • 编译时会警告 mesh 未使用,我们确实没有使用,这不影响

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*---------------------------------------------------------------------------*\
| ========= | |
| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |
| \\ / O peration | Version: 2306 |
| \\ / A nd | Website: www.openfoam.com |
| \\/ M anipulation | |
\*---------------------------------------------------------------------------*/
Build : _f8e05934-20230403 OPENFOAM=2306 patch=230110 version=2306
Arch : "LSB;label=32;scalar=64"
Exec : ofsp_00_helloWorld_openfoam -case debug_case
Date : May 06 2023
Time : 20:26:13
Host : aerosand
PID : 18593
I/O : uncollated
Case : /home/aerosand/aerosand/ofsp/ofsp_00_helloWorld_openfoam/debug_case
nProcs : 1
trapFpe: Floating point exception trapping enabled (FOAM_SIGFPE).
fileModificationChecking : Monitoring run-time modified files using timeStampMaster (fileModificationSkew 5, maxFileModificationPolls 20)
allowSystemOperations : Allowing user-supplied system call operations

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
Create time

Create mesh for time = 0

Hi, OpenFOAM! Here we are.
1 + 3.14159 = 4.14159
1 * 3.14159 = 3.14159


Current time step is : 0.2

ExecutionTime = 0.01 s ClockTime = 0 s

End

以后的运行结果不再赘述 OpenFOAM 标准输出信息。

小结

到此为止,我们应该清楚明白了 OpenFOAM 应用开发的逻辑和架构。

希望这样的介绍能让读者感受到“清晰”和“连续”。

在实际开发中,开发库可以是用户自定义边界条件,也可以是一些用户工具等等。