Before running this case, you need to install Paddle Graph Learning graph learning tool and PyAMG algebraic multigrid tool via pip install -r requirements.txt command.
In recent years, the successful application of deep learning in computer vision and natural language processing has prompted people to explore the application of artificial intelligence in the field of scientific computing, especially in the field of Computational Fluid Dynamics (CFD).
Fluid is a very complex physical system, and the behavior of fluid is governed by the Navier-Stokes equations. Grid-based finite volume or finite element simulation methods are widely used numerical methods in CFD. The physical problems studied by computational fluid dynamics are often very complex and usually require a lot of computing resources to find the solution to the problem, so a trade-off between solution accuracy and computational cost is needed. In order to perform numerical simulation, the computational domain is usually discretized by grids. Since the grid has good geometric and physical problem representation capabilities and is compatible with the graph structure, the authors of this article use graph neural networks to construct a data-driven model for flow field prediction by training CFD simulation data.
The authors propose a graph neural network-based CFD calculation model called AMGNET (A Multi-scale Graph neural Network), which can predict flow fields under different physical parameters. This method has the following characteristics:
AMGNET converts the grid in CFD into a graph structure and processes and aggregates information through graph neural networks. Compared with traditional GCN methods, the prediction error of this method is significantly lower.
AMGNET can calculate the fluid velocity in the x and y directions at the same time, and can also calculate the fluid pressure.
AMGNET coarsens the graph through the RS algorithm (Olson and Schroder, 2018), and can predict using only a small number of nodes, further improving the prediction speed.
The figure below shows the network structure of this method. The basic principle of this model is to convert the grid structure into a graph structure, and then encode the nodes and edges in the graph through the physical information, location information and node type of the nodes in the grid. Then, the obtained graph neural network is coarsened using a coarsening layer based on the algebraic multigrid algorithm (RS) to classify all nodes into coarse node sets and fine node sets, where the coarse node set is a subset of the fine node set. The node set of the coarse graph is the coarse node set, thus completing the coarsening of the graph and reducing the scale of the graph. After coarsening is completed, the features of the graph are summarized and extracted through the designed graph neural network message passing block (GN). Afterwards, the graph restoration layer uses reverse operations and uses spatial interpolation (Qi et al., 2017) to upsample the graph. For example, to interpolate node \(i\), find the \(k\) nodes closest to node \(i\) in the coarse graph, and then calculate the features of node \(i\) through the formula. Finally, the velocity and pressure information of each node is obtained through the decoder.
Next, we will explain how to convert the problem into PaddleScience code step by step and solve the problem using deep learning methods.
In order to quickly understand PaddleScience, only key steps such as model construction, equation construction, and computational domain construction are described below, while other details please refer to API Documentation.
The airfoil dataset used in this case comes from de Avila Belbute-Peres et al., where the airfoil dataset uses NACA0012 airfoil, including train, test and corresponding grid data mesh_fine; the cylinder dataset is a CFD calculation example calculated by the original author using software.
Execute the following command to download and unzip the dataset.
# set cylinder modelmodel=ppsci.arch.AMGNet(**cfg.MODEL)
In order to access the value of specific variables accurately and quickly during calculation, we specify the input variable name of the network model as ("input", ) and the output variable name as ("pred", ), these names are consistent with the subsequent code.
In this case, we use supervised datasets to train the model, so we need to build supervised constraints.
Before defining constraints, we need to specify the path of the dataset and other related configurations, and store this information in the corresponding YAML file, as shown below.
# set constraintsup_constraint=ppsci.constraint.SupervisedConstraint(train_dataloader_cfg,output_expr={"pred":lambdaout:out["pred"]},loss=ppsci.loss.FunctionalLoss(train_mse_func),name="Sup",)cfg.TRAIN.iters_per_epoch=len(sup_constraint.data_loader)# wrap constraints together
The training process will call the optimizer to update model parameters. Here, the Adam optimizer is selected, and a fixed 5e-4 is used as the learning rate.
Usually during the training process, the training status of the current model is evaluated using the validation set (test set) at a certain epoch interval, so ppsci.validate.SupervisedValidator is used to construct the validator. The construction process is similar to Constraint Construction, just change the data directory to the directory of the test set, and set EVAL.batch_size=1 in the configuration file.
# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshAirfoilDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)
# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshCylinderDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)validator={rmse_validator.name:rmse_validator}
The evaluation metric is the RMSE value of the predicted result and the real result, so a custom metric calculation function needs to be defined, as shown below.
utils.log_images(input_["input"].pos,prefield["pred"],truefield,rmse_validator.data_loader.dataset.elems_list,index,"airfoil",)defevaluate(cfg:DictConfig):# set random seed for reproducibilityppsci.utils.misc.set_random_seed(cfg.seed)# initialize logger
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.# Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at# http://www.apache.org/licenses/LICENSE-2.0# Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.from__future__importannotationsfromosimportpathasospfromtypingimportTYPE_CHECKINGfromtypingimportDictfromtypingimportListimporthydraimportutilsfromomegaconfimportDictConfigfrompaddle.nnimportfunctionalasFimportppscifromppsci.utilsimportloggerifTYPE_CHECKING:importpaddleimportpgldeftrain_mse_func(output_dict:Dict[str,"paddle.Tensor"],label_dict:Dict[str,"pgl.Graph"],*args,)->paddle.Tensor:return{"pred":F.mse_loss(output_dict["pred"],label_dict["label"].y)}defeval_rmse_func(output_dict:Dict[str,List["paddle.Tensor"]],label_dict:Dict[str,List["pgl.Graph"]],*args,)->Dict[str,paddle.Tensor]:mse_losses=[F.mse_loss(pred,label.y)for(pred,label)inzip(output_dict["pred"],label_dict["label"])]return{"RMSE":(sum(mse_losses)/len(mse_losses))**0.5}deftrain(cfg:DictConfig):# set random seed for reproducibilityppsci.utils.misc.set_random_seed(cfg.seed)# initialize loggerlogger.init_logger("ppsci",osp.join(cfg.output_dir,"train.log"),"info")# set airfoil modelmodel=ppsci.arch.AMGNet(**cfg.MODEL)# set dataloader configtrain_dataloader_cfg={"dataset":{"name":"MeshAirfoilDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.TRAIN_DATA_DIR,"mesh_graph_path":cfg.TRAIN_MESH_GRAPH_PATH,},"batch_size":cfg.TRAIN.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":True,},"num_workers":1,}# set constraintsup_constraint=ppsci.constraint.SupervisedConstraint(train_dataloader_cfg,output_expr={"pred":lambdaout:out["pred"]},loss=ppsci.loss.FunctionalLoss(train_mse_func),name="Sup",)cfg.TRAIN.iters_per_epoch=len(sup_constraint.data_loader)# wrap constraints togetherconstraint={sup_constraint.name:sup_constraint}# set optimizeroptimizer=ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshAirfoilDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)validator={rmse_validator.name:rmse_validator}# initialize solversolver=ppsci.solver.Solver(model,constraint,optimizer=optimizer,validator=validator,cfg=cfg,)# train modelsolver.train()# visualize predictionlogger.message("Now visualizing prediction, please wait...")withsolver.no_grad_context_manager(True):forindex,(input_,label,_)inenumerate(rmse_validator.data_loader):truefield=label["label"].yprefield=model(input_)utils.log_images(input_["input"].pos,prefield["pred"],truefield,rmse_validator.data_loader.dataset.elems_list,index,"airfoil",)defevaluate(cfg:DictConfig):# set random seed for reproducibilityppsci.utils.misc.set_random_seed(cfg.seed)# initialize loggerlogger.init_logger("ppsci",osp.join(cfg.output_dir,"eval.log"),"info")# set airfoil modelmodel=ppsci.arch.AMGNet(**cfg.MODEL)# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshAirfoilDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)validator={rmse_validator.name:rmse_validator}solver=ppsci.solver.Solver(model,output_dir=cfg.output_dir,log_freq=cfg.log_freq,seed=cfg.seed,validator=validator,pretrained_model_path=cfg.EVAL.pretrained_model_path,eval_with_no_grad=cfg.EVAL.eval_with_no_grad,)# evaluate modelsolver.eval()# visualize predictionwithsolver.no_grad_context_manager(True):forindex,(input_,label,_)inenumerate(rmse_validator.data_loader):truefield=label["label"].yprefield=model(input_)utils.log_images(input_["input"].pos,prefield["pred"],truefield,rmse_validator.data_loader.dataset.elems_list,index,"airfoil",)@hydra.main(version_base=None,config_path="./conf",config_name="amgnet_airfoil.yaml")defmain(cfg:DictConfig):ifcfg.mode=="train":train(cfg)elifcfg.mode=="eval":evaluate(cfg)else:raiseValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")if__name__=="__main__":main()
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.# Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at# http://www.apache.org/licenses/LICENSE-2.0# Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.from__future__importannotationsfromosimportpathasospfromtypingimportTYPE_CHECKINGfromtypingimportDictfromtypingimportListimporthydraimportutilsfromomegaconfimportDictConfigfrompaddle.nnimportfunctionalasFimportppscifromppsci.utilsimportloggerifTYPE_CHECKING:importpaddleimportpgldeftrain_mse_func(output_dict:Dict[str,"paddle.Tensor"],label_dict:Dict[str,"pgl.Graph"],*args,)->paddle.Tensor:return{"pred":F.mse_loss(output_dict["pred"],label_dict["label"].y)}defeval_rmse_func(output_dict:Dict[str,List["paddle.Tensor"]],label_dict:Dict[str,List["pgl.Graph"]],*args,)->Dict[str,paddle.Tensor]:mse_losses=[F.mse_loss(pred,label.y)for(pred,label)inzip(output_dict["pred"],label_dict["label"])]return{"RMSE":(sum(mse_losses)/len(mse_losses))**0.5}deftrain(cfg:DictConfig):# set random seed for reproducibilityppsci.utils.misc.set_random_seed(cfg.seed)# initialize loggerlogger.init_logger("ppsci",osp.join(cfg.output_dir,"train.log"),"info")# set cylinder modelmodel=ppsci.arch.AMGNet(**cfg.MODEL)# set dataloader configtrain_dataloader_cfg={"dataset":{"name":"MeshCylinderDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.TRAIN_DATA_DIR,"mesh_graph_path":cfg.TRAIN_MESH_GRAPH_PATH,},"batch_size":cfg.TRAIN.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":True,},"num_workers":1,}# set constraintsup_constraint=ppsci.constraint.SupervisedConstraint(train_dataloader_cfg,output_expr={"pred":lambdaout:out["pred"]},loss=ppsci.loss.FunctionalLoss(train_mse_func),name="Sup",)# wrap constraints togetherconstraint={sup_constraint.name:sup_constraint}# set optimizeroptimizer=ppsci.optimizer.Adam(cfg.TRAIN.learning_rate)(model)# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshCylinderDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)validator={rmse_validator.name:rmse_validator}# initialize solversolver=ppsci.solver.Solver(model,constraint,cfg.output_dir,optimizer,None,cfg.TRAIN.epochs,cfg.TRAIN.iters_per_epoch,save_freq=cfg.TRAIN.save_freq,eval_during_train=cfg.TRAIN.eval_during_train,eval_freq=cfg.TRAIN.eval_freq,validator=validator,eval_with_no_grad=cfg.EVAL.eval_with_no_grad,)# train modelsolver.train()# visualize predictionlogger.message("Now visualizing prediction, please wait...")withsolver.no_grad_context_manager(True):forindex,(input_,label,_)inenumerate(rmse_validator.data_loader):truefield=label["label"].yprefield=model(input_)utils.log_images(input_["input"].pos,prefield["pred"],truefield,rmse_validator.data_loader.dataset.elems_list,index,"cylinder",)defevaluate(cfg:DictConfig):# set random seed for reproducibilityppsci.utils.misc.set_random_seed(cfg.seed)# initialize loggerlogger.init_logger("ppsci",osp.join(cfg.output_dir,"eval.log"),"info")# set airfoil modelmodel=ppsci.arch.AMGNet(**cfg.MODEL)# set validatoreval_dataloader_cfg={"dataset":{"name":"MeshCylinderDataset","input_keys":("input",),"label_keys":("label",),"data_dir":cfg.EVAL_DATA_DIR,"mesh_graph_path":cfg.EVAL_MESH_GRAPH_PATH,},"batch_size":cfg.EVAL.batch_size,"sampler":{"name":"BatchSampler","drop_last":False,"shuffle":False,},}rmse_validator=ppsci.validate.SupervisedValidator(eval_dataloader_cfg,loss=ppsci.loss.FunctionalLoss(train_mse_func),output_expr={"pred":lambdaout:out["pred"].unsqueeze(0)},metric={"RMSE":ppsci.metric.FunctionalMetric(eval_rmse_func)},name="RMSE_validator",)validator={rmse_validator.name:rmse_validator}solver=ppsci.solver.Solver(model,output_dir=cfg.output_dir,log_freq=cfg.log_freq,seed=cfg.seed,validator=validator,pretrained_model_path=cfg.EVAL.pretrained_model_path,eval_with_no_grad=cfg.EVAL.eval_with_no_grad,)# evaluate modelsolver.eval()# visualize predictionwithsolver.no_grad_context_manager(True):forindex,(input_,label,_)inenumerate(rmse_validator.data_loader):truefield=label["label"].yprefield=model(input_)utils.log_images(input_["input"].pos,prefield["pred"],truefield,rmse_validator.data_loader.dataset.elems_list,index,"cylinder",)@hydra.main(version_base=None,config_path="./conf",config_name="amgnet_cylinder.yaml")defmain(cfg:DictConfig):ifcfg.mode=="train":train(cfg)elifcfg.mode=="eval":evaluate(cfg)else:raiseValueError(f"cfg.mode should in ['train', 'eval'], but got '{cfg.mode}'")if__name__=="__main__":main()
The following shows the prediction results and reference results of the model for pressure \(p(x,y)\), x (horizontal) direction velocity \(u(x,y)\), and y (vertical) direction velocity \(v(x,y)\) at each point in the computational domain.
Left: Predicted x-direction velocity p, Right: Actual x-direction velocityLeft: Predicted pressure p, Right: Actual pressure pLeft: Predicted y-direction velocity p, Right: Actual y-direction velocity
Left: Predicted x-direction velocity p, Right: Actual x-direction velocityLeft: Predicted pressure p, Right: Actual pressure pLeft: Predicted y-direction velocity p, Right: Actual y-direction velocity
It can be seen that the model prediction results are basically consistent with the real results.