参考:

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

前置:
OpenFOAM Sharing Programming 开发编程基础00 基本实现和开发 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭

这里需要重申 OpenFOAM 初学者的 C++ 学习的个人建议:目前阶段,需要系统学习 C++ 面向对象基础,暂时不需要掌握 C++ 高级特性,暂时不需要学习编程算法,不需要等到学完 C++ 再开始 OpenFOAM。需要不断学习 C++,需要慢慢深入学习 C++,需要在不断的实践中积累 C++ 经验。

在做计算或者其他交互工作的时候,信息流的写入写出是不可避免的。比如从文件中读取计算参数、读取材料性质,又或者是向文件写入计算后的物理场。

所以,在了解应用的基本实现和开发后,我们紧接着讨论一下程序的输出输出。

本文依然基于 ubuntu 24.04 系统,OpenFOAM 2406 版本(和 2212 版本,2206 版本几乎没有什么差别,不用担心)。此系列以后不再赘述。

C++ 实现

我们已经知道 C++ 可以通过输入输出流来实现从文件中读取或者向文件中写入。

拷贝标准算例到 run 文件夹下。(如果使用 run 指令无效的,参考 OpenFOAM环境准备 | 𝓐𝓮𝓻𝓸𝓼𝓪𝓷𝓭 建立相应文件夹 )

1
2
3
// terminal
run
cp -r $FOAM_TUTORIALS/incompressible/icoFoam/cavity/cavity .

打开该算例,查看 constant/transportProperties 文件和 system/controlDict 文件,可以看到每个关键词都对应着用户给定的值,这些关键词后的值在求解过程中,当然也会被程序读入。

阅读 transportProperties 文件和 controlDict 文件的文件头,可以明白,这两个文件其实都是“字典”文件。虽然两者向主程序输入参数的机制不同,但是我们仍然先行简单理解向主程序输入参数的主要思路。这样也可以在入门阶段更好理解字典文件的意义。

字典的格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// transportProperties

nu 0.01;


// controlDict

...

application icoFoam;

startFrom startTime;

startTime 0;

stopAt endTime;

...

计算该算例

1
2
3
4
5
// terminal
blockMesh
icoFoam

// use foamCleanTutorials to clean case

可以很快得到计算结果,我们打开最终时间步的速度场(0.5/U),查看输出的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// U field
dimensions [0 1 -1 0 0 0 0];

internalField nonuniform List<vector>
400
(
(0.000253405 -0.000250456 0)
(0.000141206 0.000111424 0)
(-0.00117704 0.000564623 0)
(-0.00348128 0.00088502 0)
...
)
;
...

下面,我们尝试讨论和理解这个过程,即

  • 从外部文件向主程序输入参数
  • 从主程序向外部文件输出结果

文件流

在初学的时候我们就知道 C++ 提供 iostream 标准库,包含 cincout 方法,用于从标准输入中读取信息流,或者从标准输出中写入信息流。除此之外,C++ 还提供 fstream 标准库,用于外部文件和信息流之间的交互。

  • ofstream 表示输出文件流,用于创建文件并向文件写入信息
  • ifstream 表示输入文件流,用于从文件读取信息
  • fstream 表示通用文件流,同时具有写入写出的方法

具体看一下怎么使用文件流。

项目准备

建立本文的项目文件夹

1
2
3
// terminal
ofsp
mkdir ofsp_01_IO_cpp

通过 vscode 的 C/C++ Project Generater 打开新项目 ofsp_01_IO_cpp

项目实现

我们通过该项目模拟求解器读取算例的字典文件的参数。

主源码 src/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
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
#include <iostream> // IO标准库
#include <string> // 字符串库
#include <fstream> // 文件流库
#include <cassert> // 异常判断库

int main(int argc, char *argv[])
{
// 给外部参数准备变量
double viscosity;
std::string application;
double deltaT;
int writeInterval;
bool purgeWrite;
std::string var[5];

std::fstream infile; // 定义一个文件流
infile.open("ofspProperties", std::fstream::in); // 打开文件并准备读取
if (!infile) { // 异常判断,如果打开失败,则执行下面
std::cout << "# WARNING: NO input!" << std::endl;
assert(infile); // 终止程序并抛出错误信息(频繁调用影响性能)
}
infile >> var[0] >> viscosity;
infile >> var[1] >> application;
infile >> var[2] >> deltaT;
infile >> var[3] >> writeInterval;
infile >> var[4] >> purgeWrite;
infile.close(); // 必须关闭文件

std::cout << std::endl
<< var[0] << "\t\t" << viscosity << std::endl
<< var[1] << "\t" << application << std::endl
<< var[2] << "\t\t" << deltaT << std::endl
<< var[3] << "\t" << writeInterval << std::endl
<< var[4] << "\t" << purgeWrite << std::endl
<< std::endl;


// may we got the field U
int nCells = 4;
int dimension = 3;
double initVelocity = 0.1;
double fieldU[nCells][dimension] = {0};
for (int i = 0; i < nCells; ++i)
{
for (int j = 0; j < dimension; ++j)
{
fieldU[i][j] = initVelocity * i * deltaT;
}
}

// output our field U
std::fstream outfile;
outfile.open("U", std::fstream::out); // 打开文件并准备写入
outfile << "internalField\tnonuniform List<vector>" << std::endl;
outfile << nCells << "\n(" << std::endl;
for (int i = 0; i < nCells; ++i)
{
outfile << "( ";
for (int j = 0; j < dimension; ++j)
{
outfile << fieldU[i][j] << " ";
}
outfile << ")\n";
}
outfile << ")\n;";

outfile.close(); // 必须关闭文件

// 现代 C++ 可以不写 return 0;
}

我们同样为该项目提供模仿字典的文件。ofspProperties 文件内容如下(路径为 /ofsp/ofsp_01_IO_cpp/ofspProperties

1
2
3
4
5
nu                  0.01
application laplacianFoam
deltaT 0.005
writeInterval 20
purgeWrite 0

编译运行

终端编译并运行此项目

1
2
3
// terminal
make clean
make run

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
g++ -std=c++17 -Wall -Wextra -g -Iinclude -c -MMD src/main.cpp  -o src/main.o
src/main.cpp: In function ‘int main(int, char**)’:
src/main.cpp:6:14: warning: unused parameter ‘argc’ [-Wunused-parameter]
6 | int main(int argc, char *argv[])
| ~~~~^~~~
src/main.cpp:6:26: warning: unused parameter ‘argv’ [-Wunused-parameter]
6 | int main(int argc, char *argv[])
| ~~~~~~^~~~~~
g++ -std=c++17 -Wall -Wextra -g -Iinclude -o output/main src/main.o -Llib
Executing all complete!
./output/main

nu 0.01
application laplacianFoam
deltaT 0.005
writeInterval 20
purgeWrite 0

Executing run: all complete!

我们可以看到每一项外部文件的关键词后的参数都被读入了程序,可以在程序中操作或输出。

同时发现项目根目录下生成了 U 文件,打开看到其中内容为

1
2
3
4
5
6
7
8
9
internalField	nonuniform List<vector>
4
(
( 0 0 0 )
( 0.0005 0.0005 0.0005 )
( 0.001 0.001 0.001 )
( 0.0015 0.0015 0.0015 )
)
;

格式和 OpenFOAM 的输出速度场的格式类似。

通过这个项目,我们可以简单理解, OpenFOAM 求解器是如何通过字典文件拿到参数指定的值。求解器输出的计算结果呢,本质上是 OpenFOAM 可以识别,后处理软件可以识别的,有特定排版格式的文件。

重构项目

让我们停下脚步,仔细想一下上面的项目。我们很难称它是按关键词读取,因为代码只是实现了读取。如果我们更改输入文件的关键词顺序,就会导致传入参数的不一致(读者可以尝试更换 ofspProperties 文件中关键词顺序,重新运行代码,查看结果)。

我们来考虑一下如何真正的按关键词读取。

OF 原生读参

我们看一下 OpenFOAM 中一些简单类型的参数的读取语句是什么样子的。

OpenFOAM 提供 lookup() 函数来按关键词读取,类似于如下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
IOdictionary transportProperties
(
IOobject
(
"transportProperties",
runTime.constant(),
mesh,
IOobject::MUST_READ_IF_MODIFIED,
IOobject::NO_WRITE
)
);

double viscosity = transportProperties.lookup("nu");

可以看到 OpenFOAM 的代码,基于 IOobject 实体,构造了 IOdictionary 类的 transportProperties 实体,而 IOobject 则是基于文件 "transportProperties" 和其他参数构造的。而 transportProperties 使用属于的 IOdictionary 类的方法 lookup() 指定关键词 "nu" 来读取此关键词后的参数值。虽然这个语法在 OpenFOAM 中有点过时,但是相对来说非常直观。

现在更多的使用 dimensionedScalar viscosity("nu", transportProperties) 这种写法,更加的面向对象,而非面向过程。

简单概括如下,

  • IOdictionary 是和文件读取相关的类
  • IOdictionary 基于外部文件 transportProperties 构造实体 transportProperties
  • 实体 transportProperties 具有 IOdictionary 类的方法
  • 也就是说,实体 transportProperties 可以使用 lookup() 方法

我们尝试自己动手模仿,实现 OpenFOAM 的这个读取功能。

项目准备

建立本项目文件夹

1
2
3
// terminal
ofsp
mkdir ofsp_01_IO_keyword

整体的架构如下

1
2
3
4
5
6
7
8
9
10
11
12
ofsp_01_IO_keyword/
├── IOdictionary/
│   ├── IOdictionary.C
│   ├── IOdictionary.H
│   └── Make/
│   ├── files
│   └── options
├── Make/
│   ├── files
│   └── options
├── ofsp_01_IO_keyword.C
└── ofspProperties

自定义字典文件 ofspProperties 中提供和上节项目相同的内容。

自定义 IOdictionary

我们设想,自定义的 IOdictionary 类也可以有类似 OpenFOAM 的语法表达。简化的形式如下

1
2
3
4
5
6
7
// 想要的语法效果
IOdictionary ofspProperties // 构造类的实体
(
"ofspProperties" // 外部文件的名称,根目录路径
);

double viscosity = ofspProperties.lookup("nu"); // 类的实体可以使用方法

其中,ofspPropertiesIOdictionary 类的实例化。

可以看到,这个自定义的 IOdictionary 类需要读取外部文件,所以我们考虑仍然要使用 C++ 原生 fstream 类。

在类的声明的时候,我们也可以定义一个自己的命名空间,避免错误调用。

类的声明 IOdictionary.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
#pragma once

#include <iostream>
#include <fstream>
#include <string>

namespace Aerosand // 自定义命名空间,避免命名重复
{

class IOdictionary
{

private:
std::ifstream dictionary_;
std::string filePath_;

public:

// Constructor

IOdictionary(const std::string& filePath);

// Destructor

~IOdictionary();

// Member Function

std::string lookup(const std::string& keyword);

};

}

类的定义 IOdictionary.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
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
#include "IOdictionary.H"

// Constructor

Aerosand::IOdictionary::IOdictionary(const std::string& filePath)
{
filePath_ = filePath;
}

// Destructor

Aerosand::IOdictionary::~IOdictionary()
{
if (dictionary_.is_open())
{
dictionary_.close();
}
}

// Member Function

std::string Aerosand::IOdictionary::lookup(const std::string& keyword)
{
dictionary_.open(filePath_);

std::string returnValue;

std::string line;

while (std::getline(dictionary_, line))
{
// 截取关键词后的字符串
size_t keywordPos = line.find(keyword);
if (keywordPos != std::string::npos)
{
returnValue = line.substr(keywordPos + keyword.length());
}

// 剔除所有的空格
size_t spacePos = returnValue.find(" ");
while (spacePos != std::string::npos)
{
returnValue.erase(spacePos, 1);
spacePos = returnValue.find(" ");
}
}

dictionary_.close();

return returnValue;
}

库 Make

文件 IOdictionary/Make/files 内容如下

1
2
3
IOdictionary.C

LIB = $(FOAM_USER_LIBBIN)/libIOdictionary

文件 IOdictionary/Make/options 留空。

编译生成动态库

1
2
3
// terminal
wclean IOdictionary
wmake IOdictionary

主源码

重写主源码,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <fstream>

#include "IOdictionary.H"

int main(int argc, char const *argv[])
{
Aerosand::IOdictionary ofspProperties("ofspProperties");

std::string viscosity = ofspProperties.lookup("nu");
std::string application = ofspProperties.lookup("application");
std::string deltaT = ofspProperties.lookup("deltaT");
std::string writeInterval = ofspProperties.lookup("writeInterval");
std::string purgeWrite = ofspProperties.lookup("purgeWrite");

std::cout << "nu\t\t" << viscosity << std::endl;
std::cout << "application\t" << application << std::endl;
std::cout << "deltaT\t\t" << deltaT << std::endl;
std::cout << "writeInterval\t" << writeInterval << std::endl;
std::cout << "purgeWrite\t" << purgeWrite << std::endl;

return 0;
}

项目 Make

文件 ofsp_01_IO_keyword/Make/files 内容如下

1
2
3
4
ofsp_01_IO_keyword.C

EXE = $(FOAM_USER_APPBIN)/ofsp_01_IO_keyword

文件 ofsp_01_IO_keyword/Make/options 内容如下

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

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

编译运行

终端执行

1
2
3
4
5
// terminal
wclean
wmake

ofsp_01_IO_keyword

终端输出结果如下

1
2
3
4
5
nu              0.01
application laplacianFoam
deltaT 0.005
writeInterval 20
purgeWrite 0

即使我们更换字典文件中内容的顺序,或者增加新的关键词,都不会影响该项目按关键词读入取值。

重构项目的思路基本正确。但是读取得到的参数类型统一是 string 类型,不符合我们的要求,我们需要进一步开发功能特性。

开发特性

这里有一些作者关于版本存档的建议(暂不使用 git 控制版本)。

当对项目有新的想法增加或者改进的时候,建议拷贝至项目,在新项目上进行修改和开发,保留原始文件以便回滚版本。

项目准备

建立本项目文件夹

1
2
3
4
5
6
7
8
9
// terminal
ofsp
cp -r ofsp_01_IO_keyword ofsp_01_IO_feature
cd ofsp_01_IO_feature
wclean IOdictionary
wclean

mv ofsp_01_IO_keyword.C ofsp_01_IO_feature.C
sed -i s/"IO_keyword"/"IO_feature"/g Make/files

整体的架构如下

1
2
3
4
5
6
7
8
9
10
11
12
ofsp_01_IO_feature/
├── IOdictionary/
│   ├── IOdictionary.C
│   ├── IOdictionary.H
│   └── Make/
│   ├── files
│   └── options
├── Make/
│   ├── files
│   └── options
├── ofsp_01_IO_feature.C
└── ofspProperties

开发库和主程序的 Make 参考上节项目进行修改。

自定义字典文件 ofspProperties 中提供和上节项目相同的内容。

区分参数类型

当我们拿到 string 类型的返回值后,我们需要把该返回值转换成我们需要的类型。

开发设计思路

考虑函数 lookup() 需要读入各种数据类型,以及要返回给主程序各种数据类型,我们尝试使用 C++ 的模板 template 进行开发。

代码改进

改进自定义开发类 IOdictionary,声明和定义如下

类的声明 IOdictionary.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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>

namespace Aerosand // 自定义命名空间,避免命名重复
{

class IOdictionary
{

private:
std::ifstream dictionary_;
std::string filePath_;

public:

// Constructor

IOdictionary(const std::string& filePath);

// Destructor

~IOdictionary();

private:

// Private Member Function

// 私有成员函数,仅在类内使用
template<typename T>
T convertFromString(const std::string& str);

public:

// Public Member Function

template<typename T>
T lookup(const std::string& keyword);

};


// Private Member Function

template<typename T>
T Aerosand::IOdictionary::convertFromString(const std::string& str)
{
T value;
std::istringstream iss(str);
iss >> value;

return value;
}

// Public Member Function

template<typename T>
T Aerosand::IOdictionary::lookup(const std::string& keyword)
{
dictionary_.open(filePath_); // 打开文件

T returnValue;

std::string line; // 读入行
std::string valueLine; // 处理行

while (std::getline(dictionary_, line)) // 按行读入
{
size_t keywordPos = line.find(keyword); // 关键词的第一个字母的位置
if (keywordPos != std::string::npos) // 如果能找到关键词
{
// 截取关键词后的字符串(包含空格)
valueLine = line.substr(keywordPos + keyword.length());

// 剔除所有的空格
size_t spacePos = valueLine.find(" ");
while (spacePos != std::string::npos)
{
valueLine.erase(spacePos, 1);
spacePos = valueLine.find(" ");
}
}
}

dictionary_.close(); // 关闭文件

returnValue = convertFromString<T>(valueLine); // 转换为用户指定类型

return returnValue;

}

}

这里需要注意,因为模板的编译机制不同,我们把类内模板函数的定义和声明放在了同一个文件,分开会导致主程序找不到函数的实例化对象。因为涉及到了更多的 C++ 知识,这里暂不深究,知道这么处理就可以了。

类的定义 IOdictionary.C 内容相应的修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "IOdictionary.H"

// Constructor

Aerosand::IOdictionary::IOdictionary(const std::string& filePath)
{
filePath_ = filePath;
}

// Destructor

Aerosand::IOdictionary::~IOdictionary() // 其实 C++ 会自动关闭
{
if (dictionary_.is_open())
{
dictionary_.close();
}
}

由此以来,主源码 ofsp_01_IO_feature.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
27
28
29
#include <iostream>
#include <fstream>
#include <string>

#include "IOdictionary.H"

int main(int argc, char const *argv[])
{
Aerosand::IOdictionary ofspProperties("ofspProperties"); // 调用构造函数

// 调用模板函数
double viscosity = ofspProperties.lookup<double>("nu");
std::string application = ofspProperties.lookup<std::string>("application");
double deltaT = ofspProperties.lookup<double>("deltaT");
int writeInterval = ofspProperties.lookup<int>("writeInterval");
bool purgeWrite = ofspProperties.lookup<bool>("purgeWrite");

std::cout << "nu\t\t" << viscosity << std::endl;
std::cout << "application\t" << application << std::endl;
std::cout << "deltaT\t\t" << deltaT << std::endl;
std::cout << "writeInterval\t" << writeInterval << std::endl;
std::cout << "purgeWrite\t" << purgeWrite << std::endl;

// 验证参数读取的类型正确,可以参与计算
std::cout << "\nWrite time interval = " << deltaT * writeInterval << std::endl;

return 0;
}

测试

编译运行

1
2
3
4
5
// terminal
wmake IOdictionary
wmake

ofsp_01_IO_feature

结果如下

1
2
3
4
5
6
7
nu              0.01
application laplacianFoam
deltaT 0.005
writeInterval 20
purgeWrite 0

Write time interval = 0.1

可以看到读取功能正常,并且通过计算验证了读取的参数值的类型也是正确的。

风格和注释和报错

有了前文对字符串的处理的思路,我们可以进行自定义类的风格开发设计。

开发设计思路

风格的设计参考 C++,每行末尾加 ; 表示语句结束,如下所示

1
nu         0.01;

注释的设计参考 C++ ,有以下两种形式

1
2
3
4
// nu is viscosity
nu 0.01;

deltaT 0.005; // we can change deltaT

报错的设计简单考虑有以下几种情况

  1. 字典文件不存在或者文件名称错误,应该终止程序并输出对应的报错信息
  2. 字典中的关键词找不到,应该终止程序并输出对应的报错信息
  3. 字典中的关键词存在,但是没有给定值,应该终止程序并输出对应的报错信息

代码改进

自定义类 IOdictionary 需要进一步改进。

类的声明 IOdictionary.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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>

namespace Aerosand // 自定义命名空间,避免命名重复
{

class IOdictionary
{

private:
std::ifstream dictionary_;
std::string filePath_;

public:

// Constructor

IOdictionary(const std::string& filePath);

// Destructor

~IOdictionary();

private:

// Private Member Function

// 私有成员函数,仅在类内使用
template<typename T>
T convertFromString(const std::string& str);

public:

// Public Member Function

template<typename T>
T lookup(const std::string& keyword);

};


// Private Member Function

template<typename T>
T Aerosand::IOdictionary::convertFromString(const std::string& str)
{
T value;
std::istringstream iss(str);
iss >> value;

return value;
}

// Public Member Function

template<typename T>
T Aerosand::IOdictionary::lookup(const std::string& keyword)
{
dictionary_.open(filePath_); // 打开文件

T returnValue;

bool foundKeyword = false; // 是否找到关键词的指标

std::string line; // 读入行
std::string valueLine; // 处理行

while (std::getline(dictionary_, line)) // 按行读入
{
if (line.find("//") == 0) // 跳过行注释
{
continue;
}

size_t keywordPos = line.find(keyword); // 关键词的第一个字母的位置
if (keywordPos != std::string::npos) // 如果能找到关键词
{
foundKeyword = true; // 找到关键词

// 截取关键词后的字符串
valueLine = line.substr(keywordPos + keyword.length());

// 截取分号之前的字符串
if (valueLine.find(";"))
{
valueLine = valueLine.substr(0,valueLine.find(";"));
}

// 截取 // 之前的字符串,其实和上一行冗余了
if (valueLine.find("//") != std::string::npos)
{
valueLine = valueLine.substr(0, valueLine.find("//"));
}

// 剔除所有的空格
size_t spacePos = valueLine.find(" ");
while (spacePos != std::string::npos)
{
valueLine.erase(spacePos, 1);
spacePos = valueLine.find(" ");
}

// 如果处理完发现没有值,报错
if (valueLine.empty()){
std::cerr << "\n--> AEROSAND FATAL ERROR: " << std::endl
<< "cannot find value of " << keyword << std::endl
<< std::endl;
exit(1);
}
}
}

// 如果没有找到关键词,报错
if (!foundKeyword)
{
std::cerr << "\n--> AEROSAND FATAL ERROR: " << std::endl
<< "cannot find keyword " << keyword << std::endl
<< std::endl;
exit(1);
}

dictionary_.close(); // 关闭文件

returnValue = convertFromString<T>(valueLine); // 转换为用户指定类型

return returnValue;

}

}

类的定义 IOdictionary.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
27
28
29
#include "IOdictionary.H"

// Constructor

Aerosand::IOdictionary::IOdictionary(const std::string& filePath)
{
filePath_ = filePath;

dictionary_.open(filePath_);
if (!dictionary_.is_open()) // 如果打开失败,报错
{
std::cerr << "\n--> AEROSAND FATAL ERROR: " << std::endl
<< "cannot find file \"" << filePath_ << "\"\n" << std::endl
<< std::endl;
exit(1);
}
dictionary_.close();
}

// Destructor

Aerosand::IOdictionary::~IOdictionary() // 其实 C++ 会自动关闭
{
if (dictionary_.is_open())
{
dictionary_.close();
}
}

测试

修改本地字典 ofspProperties 内容如下

1
2
3
4
5
6
7
8
9
10
11
// This is ofspProperties.

nu 0.02; // see the difference

// test
deltaT 0.005;
writeInterval 20;
purgeWrite 0; // test


application laplacianFoam;

重新编译,读者可以测试字典文件可能出现的多种情况,可以发现结果满足设计要求。

开发小结

在开发特性的过程,我们可以意识到读取操作需要面对的数据类型还有很多,特别是 OpenFOAM 自己的类型。而且,需要处理的 OpenFOAM 输入文件格式和异常处理也有很多。这些功能特性毫无疑问需要更好的程序架构和更多的程序开发。

值的高兴的是,通过我们自己动手开发输入输出功能,相信现在对 OpenFOAM 的字典文件已经有了一定的概念。

下面,我们看一看 OpenFOAM 为我们提供的输入输出方法。

OpenFOAM 实现

OpenFOAM 的应用一般需要从 case 中读取字典,向 case 中输出计算结果等等。

OpenFOAM 是怎么实现从文件夹读取和写入的呢?OpenFOAM 的读取和写入更加高级,按关键词进行索引查找的方法直接封装在了相关的类中,直接使用方法即可,暂时不用深究到实现的代码层面。

项目准备

1
2
3
4
5
// terminal
ofsp
foamNewApp ofsp_01_IO
cd ofsp_01_IO
cp -r $FOAM_TUTORIALS/incompressible/icoFoam/cavity/cavity debug_case

文件结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ofsp_01_IO/
├── caseclean
├── caserun
├── debug_case/
│   ├── 0/
│   │   ├── p
│   │   └── U
│   ├── constant/
│   │   └── transportProperties
│   └── system/
│   ├── blockMeshDict
│   ├── controlDict
│   ├── decomposeParDict
│   ├── fvSchemes
│   ├── fvSolution
│   └── PDRblockMeshDict
├── Make/
│   ├── files
│   └── options
├── ofsp_01_IO.C
└── README.md

脚本和说明

脚本和上一篇讨论的类似,修改脚本内的求解器名称即可。

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

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

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

ofsp_01_IO -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."

README.md 写入需要说明的内容。

以后除非有特别情况,不再赘述脚本和说明

主源码

主源码如下

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include "fvCFD.H"

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

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

#include "createMesh.H"


/*
* Reading from the dictionary
*/
const word dictName("customProperties"); // 创建word类型变量保存字典名称
IOobject dictIO // 构造 IOobject 类型变量
(
dictName, // 字典名称
runTime.constant(), // 字典位置
mesh, // 和mesh相关
IOobject::MUST_READ, // 必须从字典读取
IOobject::NO_WRITE // 不向字典写入
);

if (!dictIO.typeHeaderOk<dictionary>(true))
{
FatalErrorIn(args.executable()) << "Cannot open specified dictionary"
<< dictName << exit(FatalError);
}
// 如果字典文件在文件头中指定的不是 dictionary 类型,则报错

dictionary myDictionary;
myDictionary = IOdictionary(dictIO);
// 从IOobject变量中创建字典对象

// 一般使用下面这种紧凑写法
Info<< "Reading myProperties\n" << endl;
IOdictionary myProperties // 字典变量名和字典文件名取相同
(
IOobject
(
"myProperties",
runTime.constant(),
mesh,
IOobject::MUST_READ,
IOobject::NO_WRITE
)
);

word solver; // 创建word类型变量
myProperties.lookup("application") >> solver;
// 从myProperties文件中查找到关键词,并取值赋给solver

word format(myProperties.lookup("writeFormat"));
// 或者写成更紧凑的形式

scalar timeStep(myProperties.lookupOrDefault("deltaT", scalar(0.01)));
// 也可以写成
// scalar timeStep(myProperties.lookupOrDefault<scalar>("deltaT", 0.01));
// 如果字典中没有提供这一关键词,则使用此句提供的默认值

dimensionedScalar alpha("alpha",dimless,myProperties);
// 常用这种语法读取字典中的参数

dimensionedScalar beta(myProperties.lookup("beta"));
// 这种写法在老代码中常见,但是在新一些的版本中,编译虽然能过,但是提醒语法过时


bool ifPurgeWrite(myProperties.lookupOrDefault<Switch>("purgeWrite",0));
// bool类型也可以按关键词查找并读取

List<scalar> pointList(myProperties.lookup("point"));
// 列表也可以读取

HashTable<vector,word> sourceField(myProperties.lookup("source"));
// 哈希表也可以读取

vector myVec = vector(myProperties.subDict("subDict").lookup("myVec"));
// 也可以在字典文件中再使用子字典
// 注意后文字典文件中的子字典写法

// 输出读取的内容
Info<< nl
<< "application: " << solver << nl << nl
<< "writeFormat: " << format << nl << nl
<< "deltaT: " << timeStep << nl << nl
<< "alpha: " << alpha << nl << nl
<< "beta: " << beta << nl << nl
<< "purgeWrite: " << ifPurgeWrite << nl << nl
<< "point: " << pointList << nl << nl
<< "source: " << sourceField << nl << nl
<< "myVec: " << myVec << nl << nl
<< endl;

/*
* Writing to files
*/
fileName outputDir = runTime.path()/"processing"; // 创建outputDir变量并赋值路径
mkDir(outputDir); // 创建上句路径的文件夹

autoPtr<OFstream> outputFilePtr; // 输出文件流的指针
outputFilePtr.reset(new OFstream(outputDir/"myOutPut.dat")); // 给指针定向

// 通过指针给输出文件写入信息
outputFilePtr() << "processing/myOutPut.dat" << endl;
outputFilePtr() << "0 1 2 3 ..." << endl;
sourceField.insert("U3", vector(1, 0.0, 0.0));
outputFilePtr() << sourceField << endl;

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

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

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

return 0;
}

字典文件

提供字典文件 debug_case/constant/customProperties,该字典没有读取写入操作,只需要写上正确的文件头,内容留空处理。

1
2
3
4
5
6
7
8
9
FoamFile
{
version 2.0;
format ascii;
class dictionary;
location "constant";
object customProperties;
}

字典文件 debug_case/constant/myProperties ,内容如下

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
FoamFile
{
version 2.0;
format ascii;
class dictionary;
location "constant";
object myProperties;
}

application icoFoam;

writeFormat ascii;

purgeWrite 1;

alpha 0.2;
beta beta [0 0 0 0 0 0 0] 0.5;

point
(
0
1
2
);

source
(
U1 (0 0 0)
U2 (1 0 0)
);

subDict
{
myVec (0.0 0.0 1.0);
}

编译运行

终端运行

1
2
3
4
5
// terminal
wclean
wmake
./caseclean
./caserun

以后不再赘述简单的执行命令

终端显示结果如下

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
// terminal

Create time

Create mesh for time = 0

Reading myProperties


application: icoFoam

writeFormat: ascii

deltaT: 0.01

alpha: alpha [0 0 0 0 0 0 0] 0.2

beta: beta [0 0 0 0 0 0 0] 0.5

purgeWrite: 1

point: 3(0 1 2)

source:
2
(
U1 (0 0 0)
U2 (1 0 0)
)

myVec: (0 0 1)



ExecutionTime = 0 s ClockTime = 0 s

End

另外算例文件夹下有了一个新建文件夹 debug_case/processing/,路径下的 myOutPut.dat 内容如下

1
2
3
4
5
6
7
8
9
10
processing/myOutPut.dat
0 1 2 3 ...

3
(
U1 (0 0 0)
U3 (1 0 0)
U2 (1 0 0)
)

小结

我们同样从最一般的 C++ 基础情况切入,了解了基于文件流的输入输出,也讨论了 OpenFOAM 设计的文件流输入输出方法,以后会不断地使用文件流输入输出。