仿真配置文件解析器

仿真配置文件解析器

仿真软件图像

一个典型仿真软件的前后端是分离的,通过一个配置文件实现前后端的信息传递:

前端界面

  • 用户通过图形用户界面(GUI)或命令行界面(CLI)进行交互。
  • 用户选择或设置仿真的各项参数、模型、边界条件等。
  • 前端界面将这些选择和设置转换为一个配置文件(JSON、XML、YAML等格式),并保存或直接传递给后端求解器。

配置文件

  • 配置文件包含仿真所需的所有信息,包含几何结构、材料属性、网格划分、边界条件、求解算法和输出选项等。
  • 配置文件是独立于主程序的,可以编辑或重用它来进行不同的仿真任务。

后端求解器

  • 后端求解器读取并解析配置文件,提取其中的参数和设置。
  • 基于配置文件中的信息,实例化相应的对象(如几何体、材料、网格、边界条件等)。
  • 求解器按照仿真流程进行计算,包括具体的数值求解、迭代、结果存储等操作。
  • 仿真完成后,求解器生成输出文件或数据,包括结果文件、日志文件、图形输出等。

结果输出与可视化

  • 仿真结果可以保存为文件,也可以通过前端界面直接展示。
  • 前端界面可以解析并可视化这些结果,提供直观的分析工具。

在这种结构下,前端和后端通过配置文件解耦,前端界面可以是任意的(GUI、CLI),而后端求解器只关心解析和执行配置文件中的任务。用户可以通过配置文件自定义仿真任务,不需要源代码重新编译,这也是用编译型语言实现仿真任意任务的方式。对于解释型语言,配置文件其实也是可以不需要的,用户直接写一个.py脚本用解释器动态执行就可以了。在商业软件中,前后端都是要有的。对于Lammps这种开源科学计算程序,一般没有前端界面。用户编辑一个配置文件来描述自己的仿真体系信息,求解器读取解析后直接完成对应的仿真,用户自行进行后处理。

配置文件和工厂模式

假设我们正在使用JSON格式的配置文件,想要编写一个解析器根据配置文件生成相应的对象。比如如下所示用户定义了两个几何对象,一个长方体一个圆柱体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data = {
"geometry":[
{
"no": 1,
"type":"cuboid",
"corner":{"x":0.0, "y":0.0, "z":0.0},
"length":{"width":1.0, "height":2.0, "depth":1.0}
},
{
"no": 2,
"type":"cylinder",
"radius": 2,
"height": 3,
"center": {"x":1, "y":0, "z":0},
"normal": {"x":0, "y":1, "z":0}
}
]
}

几何类满足如下的定义:

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
from abc import ABC, abstractmethod

class Geometry(ABC):
@abstractmethod
def some_method(self):
pass

class Cuboid(Geometry):
def __init__(self, corner, length):
self.corner = corner # corner should be a dict with x, y, z
self.length = length # length should be a dict with width, height, depth
def __repr__(self):
return "I'm a cuboid with length {}".format(self.corner, self.length)
def some_method(self):
pass

class Cylinder(Geometry):
def __init__(self, radius, height, center, normal):
self.radius = radius
self.height = height
self.center = center # center should be a dict with x, y, z
self.normal = normal # normal should be a dict with x, y, z

def __repr__(self):
return "I'm a cylinder with radius {} and height {}".format(self.radius, self.height)

def some_method(self):
pass

此时就可以用工厂模式来实现这个解析器的设计。首先为所有的几何解析器创建一个接口,让parse方法创建一个具体的几何对象,具体的几何解析器实现各自的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GeometryParser(ABC):
@abstractmethod
def parse(self, data):
pass

class CuboidParser(GeometryParser):
def parse(self, data):
corner = data['corner']
length = data['length']
return Cuboid(corner, length)

class CylinderParser(GeometryParser):
def parse(self, data):
radius = data['radius']
height = data['height']
center = data['center']
normal = data['normal']
return Cylinder(radius, height, center, normal)

进一步创建一个工厂选择器,根据几何类型返回相应的解析器实例对象。

1
2
3
4
5
6
7
8
9
10
11
12
class GeometryParserSelector:
parsers = {
"cuboid": CuboidParser(),
"cylinder": CylinderParser()
}

@staticmethod
def get_parser(geometry_type) -> GeometryParser:
parser = GeometryParserSelector.parsers.get(geometry_type)
if not parser:
raise ValueError(f"Unknown geometry type: {geometry_type}")
return parser

执行可以获得生成的几何对象:

1
2
3
4
generated_geos = [
GeometryParserSelector.get_parser(geo_info['type']).parse(geo_info)
for geo_info in geo_data['geometry']
]
1
>>> generated_geos
1
2
[I'm a cuboid with length {'x': 0.0, 'y': 0.0, 'z': 0.0},
I'm a cylinder with radius 2 and height 3]