OneScience commited on
Commit
d46a58d
·
verified ·
1 Parent(s): 216b972

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - ""
4
+ language:
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - fluid dynamics
10
+ - external flow prediction
11
+ - unstructured-mesh simulation
12
+
13
+ ---
14
+ <p align="center">
15
+ <strong>
16
+ <span style="font-size: 30px;">MeshGraphNet</span>
17
+ </strong>
18
+ </p>
19
+
20
+ # Model Overview
21
+
22
+ MeshGraphNets is a graph neural network developed by DeepMind for mesh-based physical simulation. It rapidly predicts the dynamics of complex physical systems, including fluids, structures, and cloth.
23
+
24
+ Paper: Learning Mesh-Based Simulation with Graph Networks
25
+ https://arxiv.org/abs/2010.03409
26
+
27
+ # Model Description
28
+ MeshGraphNets uses an encoder–processor–decoder graph-network architecture trained on trajectories from fluid, structural, and cloth simulations to perform long-horizon dynamical simulation of complex physical systems.
29
+
30
+ ## Use Cases
31
+
32
+ | Use Case | Description |
33
+ |---|---|
34
+ | External flow prediction | Predict velocity, pressure, and other flow variables at mesh nodes |
35
+ | Structural deformation simulation | Predict the displacement, stress, and deformation of loaded structures |
36
+ | Cloth dynamics | Simulate the motion of deformable objects such as flexible membranes and cloth |
37
+ | ModelScope/OneCode execution | Download the standalone model package, install its dependencies, and run the provided scripts |
38
+
39
+
40
+ # Usage
41
+
42
+ ## 1. OneCode
43
+
44
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
45
+
46
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
47
+
48
+ ## 2. Manual Setup
49
+
50
+ **Hardware Requirements**
51
+
52
+ - A GPU or DCU is recommended.
53
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
54
+
55
+
56
+ ### Download the Model Package
57
+
58
+ ```bash
59
+ modelscope download --model OneScience/MeshGraphNet --local_dir ./MeshGraphNet
60
+ cd MeshGraphNet
61
+ ```
62
+
63
+ ### Set Up the Runtime Environment
64
+
65
+
66
+ **DCU Environment**
67
+
68
+ ```bash
69
+ # Activate DTK and Conda first
70
+ conda create -n onescience311 python=3.11 -y
71
+ conda activate onescience311
72
+ # Installation with uv is also supported
73
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
74
+ ```
75
+
76
+ **GPU Environment**
77
+ ```bash
78
+ # Activate Conda first
79
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
80
+ conda activate onescience311
81
+ # Installation with uv is also supported
82
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
83
+ ```
84
+
85
+ ### Training Data
86
+
87
+ The OneScience community provides the `cylinder_flow` dataset for training. Download it with the command below and verify that the data path in `config/config.yaml` is configured correctly:
88
+
89
+ ```bash
90
+ modelscope download --dataset OneScience/cylinder_flow --local_dir ./data
91
+ ```
92
+
93
+ ### Training
94
+
95
+ Single GPU:
96
+
97
+ ```bash
98
+ python scripts/train.py
99
+ ```
100
+
101
+ Multiple GPUs:
102
+
103
+ ```bash
104
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
105
+ ```
106
+
107
+ Training saves `.pth` files under `weight/checkpoints`.
108
+
109
+ ### Model Weights
110
+ This repository will provide weights trained on the `cylinder_flow` dataset in the `weights/` directory. The weights will be uploaded soon.
111
+
112
+ ### Inference
113
+
114
+ ```bash
115
+ python scripts/inference.py
116
+ ```
117
+
118
+ Inference results are saved to `result/output/`.
119
+
120
+ ### Evaluation and Visualization
121
+
122
+ ```bash
123
+ python scripts/result.py
124
+ ```
125
+
126
+
127
+ # Official OneScience Resources
128
+
129
+ | Platform | OneScience Repository | Skills Repository |
130
+ | --- | --- | --- |
131
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
132
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
133
+
134
+ # Citations and License
135
+ - Original MeshGraphNet paper: [Learning Mesh-Based Simulation with Graph Networks](https://arxiv.org/abs/2010.03409).
136
+ - This repository retains source attribution and has been adapted for automated execution through OneScience and ModelScope.
config/config.yaml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ paths:
2
+ data_dir: data/cylinder_flow
3
+ fake_data_path: data/cylinder_flow/fake_cylinder_flow.pt
4
+ checkpoint_dir: weight/checkpoints
5
+ result_dir: results
6
+
7
+ datapipe:
8
+ source:
9
+ data_dir: data/cylinder_flow
10
+ stats_dir: data/cylinder_flow/stats
11
+ fake_data: true
12
+ fake_data_path: data/cylinder_flow/fake_cylinder_flow.pt
13
+ verbose: true
14
+ data:
15
+ train_samples: 4
16
+ train_steps: 5
17
+ val_samples: 2
18
+ val_steps: 5
19
+ test_samples: 2
20
+ test_steps: 5
21
+ noise_std: 0.0
22
+ dataloader:
23
+ batch_size: 1
24
+ num_workers: 0
25
+
26
+ model:
27
+ name: MeshGraphNet
28
+ specific_params:
29
+ MeshGraphNet:
30
+ num_input_features: 6
31
+ num_edge_features: 3
32
+ num_output_features: 3
33
+ processor_size: 2
34
+ hidden_dim_processor: 32
35
+ num_layers_node_processor: 2
36
+ num_layers_edge_processor: 2
37
+ hidden_dim_node_encoder: 32
38
+ hidden_dim_edge_encoder: 32
39
+ hidden_dim_node_decoder: 32
40
+ do_concat_trick: false
41
+ num_processor_checkpoint_segments: 0
42
+ recompute_activation: false
43
+
44
+ training:
45
+ device: cpu
46
+ max_epoch: 1
47
+ lr: 0.0001
48
+ lr_decay_rate: 0.999
49
+ amp: false
50
+ jit: false
51
+ checkpoint_dir: weight/checkpoints
52
+ loss_criterion: MSE
53
+ patience: 10
54
+ log_interval: 1
55
+ gpuid: 0
56
+
57
+ inference:
58
+ device: cpu
59
+ checkpoint_dir: weight/checkpoints
60
+ output_path: results/inference_results.npz
61
+ viz_vars: ["u", "v", "p"]
62
+ frame_skip: 1
63
+ frame_interval: 1
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other","model":"MeshGraphNet","domain":"cfd"}
model/meshgraphnet.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch import Tensor
4
+
5
+ try:
6
+ import dgl # noqa: F401 for docs
7
+ from dgl import DGLGraph
8
+ except ImportError:
9
+ raise ImportError(
10
+ "Mesh Graph Net requires the DGL library. Install the "
11
+ )
12
+ from dataclasses import dataclass
13
+ from itertools import chain
14
+ from typing import Callable, List, Tuple, Union
15
+
16
+ import onescience # noqa: F401 for docs
17
+ from onescience.modules.edge.mesh_edge_block import MeshEdgeBlock
18
+ from onescience.modules.mlp.mesh_graph_mlp import MeshGraphMLP
19
+ from onescience.modules.node.mesh_node_block import MeshNodeBlock
20
+
21
+ from onescience.modules.utils.gnnlayer_utils import CuGraphCSC, set_checkpoint_fn
22
+ from onescience.modules.layer.activations import get_activation
23
+ from onescience.modules.meta import ModelMetaData
24
+ from onescience.modules.module import Module
25
+
26
+
27
+ @dataclass
28
+ class MetaData(ModelMetaData):
29
+ name: str = "MeshGraphNet"
30
+ # Optimization, no JIT as DGLGraph causes trouble
31
+ jit: bool = False
32
+ cuda_graphs: bool = False
33
+ amp_cpu: bool = False
34
+ amp_gpu: bool = True
35
+ torch_fx: bool = False
36
+ # Inference
37
+ onnx: bool = False
38
+ # Physics informed
39
+ func_torch: bool = True
40
+ auto_grad: bool = True
41
+
42
+
43
+ class MeshGraphNet(Module):
44
+ """
45
+ MeshGraphNet 网络架构。
46
+
47
+ 该模型基于 "Learning mesh-based simulation with graph networks" (Pfaff et al., 2020) 实现。
48
+ 它采用 Encode-Process-Decode 架构:
49
+ 1. **Encoder**: 将节点和边的物理特征映射到高维隐空间。
50
+ 2. **Processor**: 通过多层消息传递(Message Passing)在图中传播信息,更新节点和边的隐状态。
51
+ 3. **Decoder**: 将处理后的节点特征解码回物理空间(例如加速度或速度增量)。
52
+
53
+ 本实现使用 MeshGraphMLP、MeshEdgeBlock 和 MeshNodeBlock 构建。
54
+
55
+ Args:
56
+ input_dim_nodes (int): 输入节点特征的维度。
57
+ input_dim_edges (int): 输入边特征的维度。
58
+ output_dim (int): 输出特征的维度(通常是节点状态的更新量)。
59
+ processor_size (int, optional): 消息传递块(Processor Block)的数量。默认值: 15。
60
+ mlp_activation_fn (Union[str, List[str]], optional): MLP 中使用的激活函数。默认值: 'relu'。
61
+ num_layers_node_processor (int, optional): 处理器中节点更新 MLP 的层数。默认值: 2。
62
+ num_layers_edge_processor (int, optional): 处理器中边更新 MLP 的层数。默认值: 2。
63
+ hidden_dim_processor (int, optional): 处理器中隐层的特征维度。默认值: 128。
64
+ hidden_dim_node_encoder (int, optional): 节点编码器的隐层维度。默认值: 128。
65
+ num_layers_node_encoder (Union[int, None], optional): 节点编码器的层数。如果为 None,则不使用编码器。默认值: 2。
66
+ hidden_dim_edge_encoder (int, optional): 边编码器的隐层维度。默认值: 128。
67
+ num_layers_edge_encoder (Union[int, None], optional): 边编码器的层数。如果为 None,则不使用编码器。默认值: 2。
68
+ hidden_dim_node_decoder (int, optional): 节点解码器的隐层维度。默认值: 128。
69
+ num_layers_node_decoder (Union[int, None], optional): 节点解码器的层数。如果为 None,则不使用解码器。默认值: 2。
70
+ aggregation (str, optional): 消息聚合方式,可选 "sum", "mean" 等。默认值: "sum"。
71
+ do_concat_trick (bool, optional): 是否使用拼接优化技巧 (MLP+idx+sum) 以节省显存。默认值: False。
72
+ num_processor_checkpoint_segments (int, optional): 梯度检查点 (Gradient Checkpointing) 的分段数。0 表示禁用。默认值: 0。
73
+ recompute_activation (bool, optional): 是否重计算激活函数以节省显存。默认值: False。
74
+
75
+ 形状:
76
+ 输入 node_features: (N, input_dim_nodes),其中 N 为节点总数。
77
+ 输入 edge_features: (M, input_dim_edges),其中 M 为边总数。
78
+ 输入 graph: DGLGraph 或 CuGraphCSC,定义图拓扑结构。
79
+ 输出: (N, output_dim),解码后的节点物理量。
80
+
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ input_dim_nodes: int,
86
+ input_dim_edges: int,
87
+ output_dim: int,
88
+ processor_size: int = 15,
89
+ mlp_activation_fn: Union[str, List[str]] = "relu",
90
+ num_layers_node_processor: int = 2,
91
+ num_layers_edge_processor: int = 2,
92
+ hidden_dim_processor: int = 128,
93
+ hidden_dim_node_encoder: int = 128,
94
+ num_layers_node_encoder: Union[int, None] = 2,
95
+ hidden_dim_edge_encoder: int = 128,
96
+ num_layers_edge_encoder: Union[int, None] = 2,
97
+ hidden_dim_node_decoder: int = 128,
98
+ num_layers_node_decoder: Union[int, None] = 2,
99
+ aggregation: str = "sum",
100
+ do_concat_trick: bool = False,
101
+ num_processor_checkpoint_segments: int = 0,
102
+ recompute_activation: bool = False,
103
+ ):
104
+ super().__init__(meta=MetaData())
105
+
106
+ activation_fn = get_activation(mlp_activation_fn)
107
+
108
+ # 1. Edge Encoder
109
+ self.edge_encoder = MeshGraphMLP(
110
+ input_dim=input_dim_edges,
111
+ output_dim=hidden_dim_processor,
112
+ hidden_dim=hidden_dim_edge_encoder,
113
+ hidden_layers=num_layers_edge_encoder,
114
+ activation_fn=activation_fn,
115
+ norm_type="LayerNorm",
116
+ recompute_activation=recompute_activation,
117
+ )
118
+
119
+ # 2. Node Encoder
120
+ self.node_encoder = MeshGraphMLP(
121
+ input_dim=input_dim_nodes,
122
+ output_dim=hidden_dim_processor,
123
+ hidden_dim=hidden_dim_node_encoder,
124
+ hidden_layers=num_layers_node_encoder,
125
+ activation_fn=activation_fn,
126
+ norm_type="LayerNorm",
127
+ recompute_activation=recompute_activation,
128
+ )
129
+
130
+ # 3. Node Decoder
131
+ self.node_decoder = MeshGraphMLP(
132
+ input_dim=hidden_dim_processor,
133
+ output_dim=output_dim,
134
+ hidden_dim=hidden_dim_node_decoder,
135
+ hidden_layers=num_layers_node_decoder,
136
+ activation_fn=activation_fn,
137
+ norm_type=None,
138
+ recompute_activation=recompute_activation,
139
+ )
140
+
141
+ # 4. Processor (Core GNN)
142
+ self.processor = MeshGraphNetProcessor(
143
+ processor_size=processor_size,
144
+ input_dim_node=hidden_dim_processor,
145
+ input_dim_edge=hidden_dim_processor,
146
+ num_layers_node=num_layers_node_processor,
147
+ num_layers_edge=num_layers_edge_processor,
148
+ aggregation=aggregation,
149
+ norm_type="LayerNorm",
150
+ activation_fn=activation_fn,
151
+ do_concat_trick=do_concat_trick,
152
+ num_processor_checkpoint_segments=num_processor_checkpoint_segments,
153
+ )
154
+
155
+ def forward(
156
+ self,
157
+ node_features: Tensor,
158
+ edge_features: Tensor,
159
+ graph: Union[DGLGraph, List[DGLGraph], CuGraphCSC],
160
+ ) -> Tensor:
161
+ edge_features = self.edge_encoder(edge_features)
162
+ node_features = self.node_encoder(node_features)
163
+ x = self.processor(node_features, edge_features, graph)
164
+ x = self.node_decoder(x)
165
+ return x
166
+
167
+
168
+ class MeshGraphNetProcessor(nn.Module):
169
+ """
170
+ MeshGraphNet 核心处理器 (Processor)。
171
+
172
+ 该模块由一系列堆叠的消息传递块 (Message Passing Blocks) 组成。
173
+ 每个块包含两个步骤:
174
+ 1. **Edge Block**: 使用 MeshEdgeBlock 更新边特征。
175
+ 2. **Node Block**: 使用 MeshNodeBlock 聚合边信息并更新节点特征。
176
+
177
+ 支持梯度检查点 (Gradient Checkpointing) 以减少大规模图训练时的显存占用。
178
+
179
+ Args:
180
+ processor_size (int, optional): 处理器包含的消息传递层数。默认值: 15。
181
+ input_dim_node (int, optional): 输入节点特征维度。默认值: 128。
182
+ input_dim_edge (int, optional): 输入边特征维度。默认值: 128。
183
+ num_layers_node (int, optional): 节点更新 MLP 的层数。默认值: 2。
184
+ num_layers_edge (int, optional): 边更新 MLP 的层数。默认值: 2。
185
+ aggregation (str, optional): 消息聚合方式 ("sum", "mean" 等)。默认值: "sum"。
186
+ norm_type (str, optional): 归一化类型。默认值: "LayerNorm"。
187
+ activation_fn (nn.Module, optional): 激活函数。默认值: nn.ReLU()。
188
+ do_concat_trick (bool, optional): 是否启用显存优化技巧。默认值: False。
189
+ num_processor_checkpoint_segments (int, optional): 梯度检查点分段数。默认值: 0 (禁用)。
190
+
191
+ 形状:
192
+ 输入 node_features: (N, input_dim_node)
193
+ 输入 edge_features: (M, input_dim_edge)
194
+ 输入 graph: DGLGraph
195
+ 输出: (N, input_dim_node) - 仅返回更新后的节点特征。
196
+
197
+ """
198
+
199
+ def __init__(
200
+ self,
201
+ processor_size: int = 15,
202
+ input_dim_node: int = 128,
203
+ input_dim_edge: int = 128,
204
+ num_layers_node: int = 2,
205
+ num_layers_edge: int = 2,
206
+ aggregation: str = "sum",
207
+ norm_type: str = "LayerNorm",
208
+ activation_fn: nn.Module = nn.ReLU(),
209
+ do_concat_trick: bool = False,
210
+ num_processor_checkpoint_segments: int = 0,
211
+ ):
212
+ super().__init__()
213
+ self.processor_size = processor_size
214
+ self.num_processor_checkpoint_segments = num_processor_checkpoint_segments
215
+
216
+ edge_blocks = []
217
+ node_blocks = []
218
+
219
+ for _ in range(self.processor_size):
220
+ edge_blocks.append(
221
+ MeshEdgeBlock(
222
+ input_dim_nodes=input_dim_node,
223
+ input_dim_edges=input_dim_edge,
224
+ output_dim=input_dim_edge,
225
+ hidden_dim=input_dim_edge,
226
+ hidden_layers=num_layers_edge,
227
+ activation_fn=activation_fn,
228
+ norm_type=norm_type,
229
+ do_concat_trick=do_concat_trick,
230
+ recompute_activation=False
231
+ )
232
+ )
233
+ node_blocks.append(
234
+ MeshNodeBlock(
235
+ aggregation=aggregation,
236
+ input_dim_nodes=input_dim_node,
237
+ input_dim_edges=input_dim_edge,
238
+ output_dim=input_dim_node,
239
+ hidden_dim=input_dim_node,
240
+ hidden_layers=num_layers_node,
241
+ activation_fn=activation_fn,
242
+ norm_type=norm_type,
243
+ recompute_activation=False
244
+ )
245
+ )
246
+
247
+ # 按照 Edge -> Node 的顺序交替排列
248
+ layers = list(chain(*zip(edge_blocks, node_blocks)))
249
+
250
+ self.processor_layers = nn.ModuleList(layers)
251
+ self.num_processor_layers = len(self.processor_layers)
252
+ self.set_checkpoint_segments(self.num_processor_checkpoint_segments)
253
+
254
+ def set_checkpoint_segments(self, checkpoint_segments: int):
255
+ if checkpoint_segments > 0:
256
+ if self.num_processor_layers % checkpoint_segments != 0:
257
+ raise ValueError(
258
+ "Processor layers must be a multiple of checkpoint_segments"
259
+ )
260
+ segment_size = self.num_processor_layers // checkpoint_segments
261
+ self.checkpoint_segments = []
262
+ for i in range(0, self.num_processor_layers, segment_size):
263
+ self.checkpoint_segments.append((i, i + segment_size))
264
+ self.checkpoint_fn = set_checkpoint_fn(True)
265
+ else:
266
+ self.checkpoint_fn = set_checkpoint_fn(False)
267
+ self.checkpoint_segments = [(0, self.num_processor_layers)]
268
+
269
+ def run_function(
270
+ self, segment_start: int, segment_end: int
271
+ ) -> Callable[
272
+ [Tensor, Tensor, Union[DGLGraph, List[DGLGraph]]], Tuple[Tensor, Tensor]
273
+ ]:
274
+ segment = self.processor_layers[segment_start:segment_end]
275
+
276
+ def custom_forward(
277
+ node_features: Tensor,
278
+ edge_features: Tensor,
279
+ graph: Union[DGLGraph, List[DGLGraph]],
280
+ ) -> Tuple[Tensor, Tensor]:
281
+ for module in segment:
282
+ edge_features, node_features = module(
283
+ edge_features, node_features, graph
284
+ )
285
+ return edge_features, node_features
286
+
287
+ return custom_forward
288
+
289
+ @torch.jit.unused
290
+ def forward(
291
+ self,
292
+ node_features: Tensor,
293
+ edge_features: Tensor,
294
+ graph: Union[DGLGraph, List[DGLGraph], CuGraphCSC],
295
+ ) -> Tensor:
296
+ for segment_start, segment_end in self.checkpoint_segments:
297
+ edge_features, node_features = self.checkpoint_fn(
298
+ self.run_function(segment_start, segment_end),
299
+ node_features,
300
+ edge_features,
301
+ graph,
302
+ use_reentrant=False,
303
+ preserve_rng_state=False,
304
+ )
305
+
306
+ return node_features
scripts/fake_data.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import dgl
6
+ import torch
7
+ from dgl.dataloading import GraphDataLoader
8
+ from torch.utils.data import Dataset
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(PROJECT_ROOT / "model"))
12
+
13
+ from onescience.utils.YParams import YParams
14
+
15
+
16
+ def make_graph(num_nodes: int = 12):
17
+ src = torch.arange(num_nodes, dtype=torch.int32)
18
+ dst = torch.roll(src, shifts=-1)
19
+ graph = dgl.to_bidirected(dgl.graph((src, dst), num_nodes=num_nodes, idtype=torch.int32))
20
+
21
+ pos = torch.stack(
22
+ (
23
+ torch.linspace(0.0, 1.0, num_nodes),
24
+ torch.sin(torch.linspace(0.0, 3.14159, num_nodes)) * 0.2,
25
+ ),
26
+ dim=1,
27
+ )
28
+ row, col = graph.edges()
29
+ disp = pos[row.long()] - pos[col.long()]
30
+ graph.edata["x"] = torch.cat(
31
+ (disp, torch.linalg.norm(disp, dim=-1, keepdim=True)),
32
+ dim=1,
33
+ )
34
+
35
+ velocity = torch.randn(num_nodes, 2) * 0.1
36
+ node_type = torch.zeros(num_nodes, 4)
37
+ node_type[:, 0] = 1.0
38
+ graph.ndata["x"] = torch.cat((velocity, node_type), dim=1)
39
+ graph.ndata["y"] = torch.cat(
40
+ (torch.randn(num_nodes, 2) * 0.01, torch.randn(num_nodes, 1) * 0.01),
41
+ dim=1,
42
+ )
43
+ graph.ndata["mesh_pos"] = pos
44
+
45
+ cells = torch.tensor(
46
+ [[i, i + 1, min(i + 2, num_nodes - 1)] for i in range(num_nodes - 2)],
47
+ dtype=torch.int64,
48
+ )
49
+ mask = torch.ones(num_nodes, 1, dtype=torch.bool)
50
+ return {"graph": graph, "cells": cells, "mask": mask}
51
+
52
+
53
+ class FakeGraphDataset(Dataset):
54
+ def __init__(self, samples):
55
+ self.samples = samples
56
+
57
+ def __len__(self):
58
+ return len(self.samples)
59
+
60
+ def __getitem__(self, index):
61
+ sample = self.samples[index]
62
+ if isinstance(sample, dict) and "graph" in sample:
63
+ return sample["graph"]
64
+ return sample
65
+
66
+
67
+ def _resolve_path(project_root: Path, path):
68
+ path = Path(path)
69
+ return path if path.is_absolute() else project_root / path
70
+
71
+
72
+ def _torch_load(path: Path):
73
+ try:
74
+ return torch.load(path, map_location="cpu", weights_only=False)
75
+ except TypeError:
76
+ return torch.load(path, map_location="cpu")
77
+
78
+
79
+ class FakeCylinderFlowDatapipe:
80
+ def __init__(self, params, project_root: Path):
81
+ self.params = params
82
+ fake_data_path = _resolve_path(project_root, params.source.fake_data_path)
83
+ if not fake_data_path.exists():
84
+ raise FileNotFoundError(
85
+ f"Fake data file not found: {fake_data_path}. Run scripts/fake_data.py first."
86
+ )
87
+
88
+ payload = _torch_load(fake_data_path)
89
+ self.train_dataset = FakeGraphDataset(payload["train"])
90
+ self.val_dataset = FakeGraphDataset(payload["val"])
91
+ self.test_dataset = FakeGraphDataset(payload["test"])
92
+ self.stats = payload.get("stats", {})
93
+
94
+ def _loader(self, dataset, shuffle=False, drop_last=False):
95
+ return GraphDataLoader(
96
+ dataset,
97
+ batch_size=self.params.dataloader.batch_size,
98
+ drop_last=drop_last,
99
+ num_workers=self.params.dataloader.num_workers,
100
+ pin_memory=True,
101
+ shuffle=shuffle,
102
+ )
103
+
104
+ def train_dataloader(self):
105
+ return self._loader(self.train_dataset, shuffle=True), None
106
+
107
+ def val_dataloader(self):
108
+ return self._loader(self.val_dataset), None
109
+
110
+ def test_dataloader(self):
111
+ return self._loader(self.test_dataset)
112
+
113
+
114
+ def use_fake_data(params):
115
+ return bool(getattr(params.source, "fake_data", False))
116
+
117
+
118
+ def build_cylinder_flow_datapipe(params, distributed: bool, project_root: Path):
119
+ if use_fake_data(params):
120
+ return FakeCylinderFlowDatapipe(params=params, project_root=project_root)
121
+
122
+ from onescience.datapipes.cfd import DeepMind_CylinderFlowDatapipe
123
+
124
+ return DeepMind_CylinderFlowDatapipe(params=params, distributed=distributed)
125
+
126
+
127
+ def main():
128
+ os.chdir(PROJECT_ROOT)
129
+ config_path = PROJECT_ROOT / "config" / "config.yaml"
130
+ cfg_data = YParams(config_path, "datapipe")
131
+ output_path = PROJECT_ROOT / cfg_data.source.fake_data_path
132
+ output_path.parent.mkdir(parents=True, exist_ok=True)
133
+
134
+ payload = {
135
+ "train": [
136
+ make_graph()
137
+ for _ in range(cfg_data.data.train_samples * (cfg_data.data.train_steps - 1))
138
+ ],
139
+ "val": [
140
+ make_graph()
141
+ for _ in range(cfg_data.data.val_samples * (cfg_data.data.val_steps - 1))
142
+ ],
143
+ "test": [
144
+ make_graph()
145
+ for _ in range(cfg_data.data.test_samples * (cfg_data.data.test_steps - 1))
146
+ ],
147
+ "stats": {
148
+ "edge_stats": {
149
+ "edge_mean": torch.zeros(3),
150
+ "edge_std": torch.ones(3),
151
+ },
152
+ "node_stats": {
153
+ "velocity_mean": torch.zeros(2),
154
+ "velocity_std": torch.ones(2),
155
+ "velocity_diff_mean": torch.zeros(2),
156
+ "velocity_diff_std": torch.ones(2),
157
+ "pressure_mean": torch.zeros(1),
158
+ "pressure_std": torch.ones(1),
159
+ },
160
+ },
161
+ }
162
+ torch.save(payload, output_path)
163
+ print(f"Fake data saved to {output_path.relative_to(PROJECT_ROOT)}")
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+
9
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(PROJECT_ROOT))
11
+
12
+
13
+ from model.meshgraphnet import MeshGraphNet
14
+ from onescience.utils.YParams import YParams
15
+ from onescience.launch.utils import load_checkpoint
16
+ from fake_data import build_cylinder_flow_datapipe
17
+
18
+
19
+ def build_model(model_params, device):
20
+ mlp_act = "silu" if model_params.recompute_activation else "relu"
21
+ return MeshGraphNet(
22
+ input_dim_nodes=model_params.num_input_features,
23
+ input_dim_edges=model_params.num_edge_features,
24
+ output_dim=model_params.num_output_features,
25
+ processor_size=model_params.processor_size,
26
+ hidden_dim_processor=model_params.hidden_dim_processor,
27
+ num_layers_node_processor=model_params.num_layers_node_processor,
28
+ num_layers_edge_processor=model_params.num_layers_edge_processor,
29
+ hidden_dim_node_encoder=model_params.hidden_dim_node_encoder,
30
+ hidden_dim_edge_encoder=model_params.hidden_dim_edge_encoder,
31
+ hidden_dim_node_decoder=model_params.hidden_dim_node_decoder,
32
+ mlp_activation_fn=mlp_act,
33
+ do_concat_trick=model_params.do_concat_trick,
34
+ num_processor_checkpoint_segments=model_params.num_processor_checkpoint_segments,
35
+ recompute_activation=model_params.recompute_activation,
36
+ ).to(device)
37
+
38
+
39
+ def resolve_device(device_name: str):
40
+ if device_name == "cpu":
41
+ return torch.device("cpu")
42
+ if device_name in ("cuda", "gpu"):
43
+ if not torch.cuda.is_available():
44
+ raise RuntimeError("Config requested cuda device, but torch.cuda.is_available() is false.")
45
+ return torch.device("cuda:0")
46
+ return torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
47
+
48
+
49
+ def main():
50
+ os.chdir(PROJECT_ROOT)
51
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
52
+ logger = logging.getLogger("mesh_graph_net.inference")
53
+
54
+ config_path = PROJECT_ROOT / "config" / "config.yaml"
55
+ cfg_model = YParams(config_path, "model")
56
+ cfg_data = YParams(config_path, "datapipe")
57
+ cfg_train = YParams(config_path, "training")
58
+ cfg_inference = YParams(config_path, "inference")
59
+ model_params = cfg_model.specific_params[cfg_model.name]
60
+
61
+ device = resolve_device(getattr(cfg_inference, "device", "auto"))
62
+ logger.info("Using device: %s", device)
63
+ datapipe = build_cylinder_flow_datapipe(
64
+ params=cfg_data,
65
+ distributed=False,
66
+ project_root=PROJECT_ROOT,
67
+ )
68
+ loader = datapipe.test_dataloader()
69
+ model = build_model(model_params, device)
70
+ checkpoint_dir = PROJECT_ROOT / getattr(cfg_inference, "checkpoint_dir", cfg_train.checkpoint_dir)
71
+ epoch = load_checkpoint(checkpoint_dir, models=model, device=device)
72
+ if epoch == 0:
73
+ logger.warning("No checkpoint found in %s; running with randomly initialized weights", checkpoint_dir)
74
+ model.eval()
75
+
76
+ predictions, targets = [], []
77
+ with torch.no_grad():
78
+ for batch in loader:
79
+ graph = batch[0] if isinstance(batch, (tuple, list)) else batch
80
+ graph = graph.to(device)
81
+ pred = model(graph.ndata["x"], graph.edata["x"], graph)
82
+ predictions.append(pred.cpu().numpy())
83
+ targets.append(graph.ndata["y"].cpu().numpy())
84
+
85
+ output_path = PROJECT_ROOT / cfg_inference.output_path
86
+ output_path.parent.mkdir(parents=True, exist_ok=True)
87
+ np.savez(output_path, prediction=np.concatenate(predictions, axis=0), target=np.concatenate(targets, axis=0))
88
+ logger.info("Saved inference results to %s", output_path)
89
+
90
+
91
+ if __name__ == "__main__":
92
+ main()
scripts/result.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
8
+ sys.path.insert(0, str(PROJECT_ROOT / "model"))
9
+
10
+ from onescience.utils.YParams import YParams
11
+
12
+
13
+ def main():
14
+ os.chdir(PROJECT_ROOT)
15
+ cfg = YParams(PROJECT_ROOT / "config" / "config.yaml", "inference")
16
+ result_path = PROJECT_ROOT / cfg.output_path
17
+ if not result_path.exists():
18
+ raise FileNotFoundError(f"Result file not found: {result_path}. Run scripts/inference.py first.")
19
+ data = np.load(result_path)
20
+ prediction = data["prediction"]
21
+ target = data["target"]
22
+ mse = float(np.mean((prediction - target) ** 2))
23
+ print(f"result_path: {result_path.relative_to(PROJECT_ROOT)}")
24
+ print(f"prediction_shape: {prediction.shape}")
25
+ print(f"target_shape: {target.shape}")
26
+ print(f"mse: {mse:.8f}")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
scripts/train.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch.amp import GradScaler, autocast
10
+ from torch.nn.parallel import DistributedDataParallel
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(PROJECT_ROOT))
14
+
15
+ from model.meshgraphnet import MeshGraphNet
16
+ from onescience.distributed import DistributedManager
17
+ from onescience.utils.YParams import YParams
18
+ from onescience.launch.utils import load_checkpoint, save_checkpoint
19
+ from fake_data import build_cylinder_flow_datapipe
20
+
21
+
22
+ def setup_logging(rank: int):
23
+ level = logging.INFO if rank == 0 else logging.WARNING
24
+ logging.basicConfig(level=level, format="%(asctime)s - %(levelname)s - %(message)s")
25
+ return logging.getLogger("mesh_graph_net.train")
26
+
27
+
28
+ def build_model(model_params, device):
29
+ mlp_act = "silu" if model_params.recompute_activation else "relu"
30
+ return MeshGraphNet(
31
+ input_dim_nodes=model_params.num_input_features,
32
+ input_dim_edges=model_params.num_edge_features,
33
+ output_dim=model_params.num_output_features,
34
+ processor_size=model_params.processor_size,
35
+ hidden_dim_processor=model_params.hidden_dim_processor,
36
+ num_layers_node_processor=model_params.num_layers_node_processor,
37
+ num_layers_edge_processor=model_params.num_layers_edge_processor,
38
+ hidden_dim_node_encoder=model_params.hidden_dim_node_encoder,
39
+ hidden_dim_edge_encoder=model_params.hidden_dim_edge_encoder,
40
+ hidden_dim_node_decoder=model_params.hidden_dim_node_decoder,
41
+ mlp_activation_fn=mlp_act,
42
+ do_concat_trick=model_params.do_concat_trick,
43
+ num_processor_checkpoint_segments=model_params.num_processor_checkpoint_segments,
44
+ recompute_activation=model_params.recompute_activation,
45
+ ).to(device)
46
+
47
+
48
+ def graph_from_batch(batch):
49
+ return batch[0] if isinstance(batch, (tuple, list)) else batch
50
+
51
+
52
+ def resolve_device(device_name: str, manager: DistributedManager, gpuid: int):
53
+ if manager.world_size > 1:
54
+ return manager.device
55
+ if device_name == "cpu":
56
+ return torch.device("cpu")
57
+ if device_name in ("cuda", "gpu"):
58
+ if not torch.cuda.is_available():
59
+ raise RuntimeError("Config requested cuda device, but torch.cuda.is_available() is false.")
60
+ return torch.device(f"cuda:{gpuid}")
61
+ return torch.device(f"cuda:{gpuid}" if torch.cuda.is_available() else "cpu")
62
+
63
+
64
+ def main():
65
+ os.chdir(PROJECT_ROOT)
66
+ DistributedManager.initialize()
67
+ manager = DistributedManager()
68
+ logger = setup_logging(manager.rank)
69
+
70
+ config_path = PROJECT_ROOT / "config" / "config.yaml"
71
+ cfg_model = YParams(config_path, "model")
72
+ cfg_data = YParams(config_path, "datapipe")
73
+ cfg_train = YParams(config_path, "training")
74
+ model_params = cfg_model.specific_params[cfg_model.name]
75
+
76
+ datapipe = build_cylinder_flow_datapipe(
77
+ params=cfg_data,
78
+ distributed=(manager.world_size > 1),
79
+ project_root=PROJECT_ROOT,
80
+ )
81
+ train_loader, train_sampler = datapipe.train_dataloader()
82
+ val_loader, val_sampler = datapipe.val_dataloader()
83
+
84
+ device = resolve_device(getattr(cfg_train, "device", "auto"), manager, cfg_train.gpuid)
85
+ logger.info("Using device: %s", device)
86
+ model = build_model(model_params, device)
87
+ if manager.world_size > 1:
88
+ model = DistributedDataParallel(model, device_ids=[manager.local_rank], output_device=manager.local_rank)
89
+
90
+ optimizer = torch.optim.Adam(model.parameters(), lr=cfg_train.lr)
91
+ scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda step: cfg_train.lr_decay_rate**step)
92
+ loss_criterion = nn.MSELoss() if cfg_train.loss_criterion == "MSE" else nn.L1Loss()
93
+ scaler = GradScaler(enabled=bool(cfg_train.amp))
94
+
95
+ checkpoint_dir = PROJECT_ROOT / cfg_train.checkpoint_dir
96
+ epoch_init = load_checkpoint(checkpoint_dir, models=model, optimizer=optimizer, scheduler=scheduler, scaler=scaler, device=device)
97
+ best_valid_loss = float("inf")
98
+ best_loss_epoch = epoch_init
99
+
100
+ logger.info("Starting training")
101
+ for epoch in range(epoch_init, cfg_train.max_epoch):
102
+ if train_sampler is not None:
103
+ train_sampler.set_epoch(epoch)
104
+ start = time.time()
105
+ model.train()
106
+ train_loss = 0.0
107
+ for idx, batch in enumerate(train_loader):
108
+ graph = graph_from_batch(batch).to(device)
109
+ optimizer.zero_grad(set_to_none=True)
110
+ with autocast(device_type=device.type, enabled=bool(cfg_train.amp)):
111
+ pred = model(graph.ndata["x"], graph.edata["x"], graph)
112
+ loss = loss_criterion(pred, graph.ndata["y"])
113
+ scaler.scale(loss).backward()
114
+ scaler.step(optimizer)
115
+ scaler.update()
116
+ scheduler.step()
117
+ train_loss += loss.item()
118
+ if manager.rank == 0 and (idx + 1) % cfg_train.log_interval == 0:
119
+ logger.info("Epoch %s/%s batch %s/%s loss %.6f", epoch + 1, cfg_train.max_epoch, idx + 1, len(train_loader), loss.item())
120
+
121
+ train_loss /= max(len(train_loader), 1)
122
+ model.eval()
123
+ valid_loss = 0.0
124
+ with torch.no_grad():
125
+ for batch in val_loader:
126
+ graph = graph_from_batch(batch).to(device)
127
+ with autocast(device_type=device.type, enabled=bool(cfg_train.amp)):
128
+ pred = model(graph.ndata["x"], graph.edata["x"], graph)
129
+ loss = loss_criterion(pred, graph.ndata["y"])
130
+ valid_loss += loss.item()
131
+ valid_loss /= max(len(val_loader), 1)
132
+
133
+ if manager.rank == 0:
134
+ logger.info(
135
+ "Epoch %s finished in %.2fs train_loss %.6f valid_loss %.6f",
136
+ epoch + 1,
137
+ time.time() - start,
138
+ train_loss,
139
+ valid_loss,
140
+ )
141
+ if valid_loss < best_valid_loss:
142
+ best_valid_loss = valid_loss
143
+ best_loss_epoch = epoch
144
+ save_checkpoint(checkpoint_dir, models=model, optimizer=optimizer, scheduler=scheduler, scaler=scaler, epoch=epoch + 1)
145
+ logger.info("Checkpoint saved to %s", checkpoint_dir)
146
+ if (epoch - best_loss_epoch) > cfg_train.patience:
147
+ logger.warning("Early stopping after %s stale epochs", cfg_train.patience)
148
+ break
149
+
150
+ logger.info("Training finished")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()