OneScience commited on
Commit
e9b4f6f
·
verified ·
1 Parent(s): 2d87bfc

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - fluid dynamics
8
+ - automotive aerodynamic design
9
+ - CFD surrogate modeling
10
+ frameworks: PyTorch
11
+ ---
12
+ <p align="center">
13
+ <strong>
14
+ <span style="font-size: 30px;">Transolver-Car-Design</span>
15
+ </strong>
16
+ </p>
17
+
18
+ # Model Overview
19
+
20
+ Transolver-Car-Design is a three-dimensional automotive external-flow prediction model built on Transolver and Transolver++ by Tsinghua University's THUML group. It provides surrogate modeling of vehicle flow fields and rapid prediction of drag coefficients.
21
+
22
+ Paper: [Transolver: A Fast Transformer Solver for PDEs on General Geometries](https://arxiv.org/pdf/2402.02366)
23
+
24
+ # Model Description
25
+ Transolver-Car-Design uses a Transformer architecture with Physics-Attention and is trained on ShapeNet-Car automotive aerodynamic simulation data. It predicts velocity fields, pressure fields, and drag coefficients for complex vehicle geometries.
26
+
27
+ ## Use Cases
28
+
29
+ | Use Case | Description |
30
+ | :--- | :--- |
31
+ | Automotive aerodynamic design | Rapidly predict external-flow velocities and surface pressures around vehicles |
32
+ | CFD surrogate modeling | Approximate fluid simulation on complex unstructured meshes with a neural network |
33
+ | Simulation acceleration | Provide a lightweight evaluation pipeline for large-scale candidate design screening |
34
+
35
+ # Usage
36
+
37
+ ## 1. OneCode
38
+
39
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
40
+
41
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
42
+
43
+ ## 2. Manual Setup
44
+
45
+ **Hardware Requirements**
46
+
47
+ - A GPU or DCU is recommended.
48
+ - A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
49
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
50
+
51
+ ### Download the Model Package
52
+
53
+ ```bash
54
+ modelscope download --model OneScience/Transolver-Car-Design --local_dir ./Transolver-Car-Design
55
+ cd Transolver-Car-Design
56
+ ```
57
+
58
+ ### Set Up the Runtime Environment
59
+
60
+
61
+ **DCU Environment**
62
+
63
+ ```bash
64
+ # Activate DTK and Conda first
65
+ conda create -n onescience311 python=3.11 -y
66
+ conda activate onescience311
67
+ # Installation with uv is also supported
68
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
69
+ ```
70
+
71
+ **GPU Environment**
72
+ ```bash
73
+ # Activate Conda first
74
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
75
+ conda activate onescience311
76
+ # Installation with uv is also supported
77
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
78
+ ```
79
+
80
+ ### Training Data
81
+
82
+ The OneScience community provides the `ShapeNetCar` dataset for training. Download it with the command below and verify that the data path in `config/config.yaml` is configured correctly:
83
+
84
+ ```bash
85
+ modelscope download --dataset OneScience/ShapeNetCar --local_dir ./data
86
+ ```
87
+
88
+ ### Training
89
+
90
+ ```bash
91
+ python scripts/train.py
92
+ ```
93
+
94
+ Training saves `Transolver_plus.pth` under `weight/`:
95
+
96
+ ```text
97
+ ./weight/Transolver_plus.pth
98
+ ```
99
+ ### Model Weights
100
+ This repository will provide model weights pretrained on ShapeNetCar data in the `weights/` directory. The weights will be uploaded soon.
101
+
102
+ ### Inference
103
+
104
+ ```bash
105
+ python scripts/inference.py
106
+ ```
107
+
108
+ ### Evaluation and Visualization
109
+
110
+ ```bash
111
+ python scripts/result.py
112
+ ```
113
+
114
+ # Official OneScience Resources
115
+
116
+ | Platform | OneScience Repository | Skills Repository |
117
+ | --- | --- | --- |
118
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
119
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
120
+
121
+ # Citations and License
122
+
123
+ - Original Transolver paper: [Transolver: A Fast Transformer Solver for PDEs on General Geometries](https://arxiv.org/pdf/2402.02366).
124
+ - Original Transolver++ paper: [Transolver++: An Accurate Neural Solver for PDEs on Million-Scale Geometries](https://arxiv.org/abs/2502.02414)
125
+ - This repository retains source attribution and has been adapted for automated execution through OneScience and ModelScope.
conf/config.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ name: "Transolver_plus"
3
+ specific_params:
4
+ Transolver: &params
5
+ n_hidden: 16
6
+ n_layers: 1
7
+ space_dim: 7
8
+ fun_dim: 0
9
+ n_head: 2
10
+ mlp_ratio: 2
11
+ out_dim: 4
12
+ slice_num: 4
13
+ unified_pos: 0
14
+ cfd_mesh: true
15
+ r: 0.2
16
+ Transolver_plus: *params
17
+
18
+ datapipe:
19
+ name: "ShapeNetCar"
20
+ task: "Transolver-Car-Design"
21
+ verbose: true
22
+ source:
23
+ data_dir: "./data/mlcfd_data/training_data"
24
+ preprocessed_save_dir: "./data/mlcfd_data/preprocessed_data"
25
+ stats_dir: "./data/mlcfd_data/stats"
26
+ preprocessed: 1
27
+ data:
28
+ splits:
29
+ fold_id: 0
30
+ dataloader:
31
+ batch_size: 1
32
+ num_workers: 0
33
+ pin_memory: false
34
+
35
+ training:
36
+ gpuid: -1
37
+ max_epoch: 1
38
+ patience: 50
39
+ lr: 0.001
40
+ loss_criterion: "MSE"
41
+ loss_weight: 0.5
42
+ checkpoint_dir: "./weight"
43
+ val_iter: 1
44
+
45
+ inference:
46
+ gpuid: -1
47
+ result_dir: "./results/ShapeNetCar"
48
+ save_vtk: false
49
+ visualize: false
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
model/Transolver3D.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+ from timm.layers import trunc_normal_
5
+ from einops import rearrange, repeat
6
+ from onescience.modules.mlp.MLP import StandardMLP
7
+ from onescience.modules.transformer.Transolver_block import Transolver_block
8
+
9
+ class Transolver3D(nn.Module):
10
+ """
11
+ Transolver3D 模型。
12
+
13
+ 该模型专为处理三维物理场问题(如 CFD)而设计。它利用 TransolverBlock 堆叠而成的深层网络来捕捉复杂的物理依赖关系。
14
+ 模型首先通过一个预处理 MLP 将输入的物理状态(和可选的统一位置编码)映射到隐空间,然后经过多层 Transolver Block 进行特征提取和交互,
15
+ 最后输出预测的物理场。
16
+
17
+ Args:
18
+ space_dim (int): 空间维度(例如 1, 2, 3)。默认值: 1。
19
+ n_layers (int): Transolver Block 的层数。默认值: 5。
20
+ n_hidden (int): 隐藏层特征维度。默认值: 256。
21
+ dropout (float): Dropout 概率。默认值: 0。
22
+ n_head (int): 注意力头数。默认值: 8。
23
+ act (str): 激活函数类型。默认值: 'gelu'。
24
+ mlp_ratio (float): MLP 膨胀比率。默认值: 1。
25
+ fun_dim (int): 输入物理场的特征维度。默认值: 1。
26
+ out_dim (int): 输出特征维度。默认值: 1。
27
+ slice_num (int): 物理注意力中的切片数量。默认值: 32。
28
+ ref (int): 统一位置编码的参考网格分辨率。默认值: 8。
29
+ unified_pos (bool): 是否使用统一位置编码。默认值: False。
30
+
31
+ 形状:
32
+ 输入 data: 包含 x (物理状态) 和 pos (坐标) 的数据对象。
33
+ 输出: (N, out_dim),预测的物理场。
34
+ """
35
+ def __init__(self,
36
+ space_dim=1,
37
+ n_layers=5,
38
+ n_hidden=256,
39
+ dropout=0,
40
+ n_head=8,
41
+ act='gelu',
42
+ mlp_ratio=1,
43
+ fun_dim=1,
44
+ out_dim=1,
45
+ slice_num=32,
46
+ ref=8,
47
+ unified_pos=False
48
+ ):
49
+ super(Transolver3D, self).__init__()
50
+ self.__name__ = 'Transolver3D'
51
+ self.ref = ref
52
+ self.unified_pos = unified_pos
53
+
54
+ if self.unified_pos:
55
+ input_dim = fun_dim + self.ref * self.ref * self.ref
56
+ else:
57
+ input_dim = fun_dim + space_dim
58
+
59
+ self.preprocess = StandardMLP(
60
+ input_dim=input_dim,
61
+ hidden_dims=[n_hidden * 2],
62
+ output_dim=n_hidden,
63
+ activation=act,
64
+ use_bias=True,
65
+ use_skip_connection=False
66
+ )
67
+
68
+ self.n_hidden = n_hidden
69
+ self.space_dim = space_dim
70
+
71
+ self.blocks = nn.ModuleList([
72
+ Transolver_block(
73
+ num_heads=n_head,
74
+ hidden_dim=n_hidden,
75
+ dropout=dropout,
76
+ act=act,
77
+ mlp_ratio=mlp_ratio,
78
+ out_dim=out_dim,
79
+ slice_num=slice_num,
80
+ last_layer=(_ == n_layers - 1),
81
+ geotype='unstructured'
82
+ )
83
+ for _ in range(n_layers)
84
+ ])
85
+
86
+ self.initialize_weights()
87
+ self.placeholder = nn.Parameter((1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float))
88
+
89
+ def initialize_weights(self):
90
+ self.apply(self._init_weights)
91
+
92
+ def _init_weights(self, m):
93
+ if isinstance(m, nn.Linear):
94
+ trunc_normal_(m.weight, std=0.02)
95
+ if isinstance(m, nn.Linear) and m.bias is not None:
96
+ nn.init.constant_(m.bias, 0)
97
+ elif isinstance(m, (nn.LayerNorm, nn.BatchNorm1d)):
98
+ nn.init.constant_(m.bias, 0)
99
+ nn.init.constant_(m.weight, 1.0)
100
+
101
+ def get_grid(self, my_pos):
102
+ # my_pos 1 N 3
103
+ batchsize = my_pos.shape[0]
104
+
105
+ gridx = torch.tensor(np.linspace(-1.5, 1.5, self.ref), dtype=torch.float)
106
+ gridx = gridx.reshape(1, self.ref, 1, 1, 1).repeat([batchsize, 1, self.ref, self.ref, 1])
107
+ gridy = torch.tensor(np.linspace(0, 2, self.ref), dtype=torch.float)
108
+ gridy = gridy.reshape(1, 1, self.ref, 1, 1).repeat([batchsize, self.ref, 1, self.ref, 1])
109
+ gridz = torch.tensor(np.linspace(-4, 4, self.ref), dtype=torch.float)
110
+ gridz = gridz.reshape(1, 1, 1, self.ref, 1).repeat([batchsize, self.ref, self.ref, 1, 1])
111
+
112
+ grid_ref = torch.cat((gridx, gridy, gridz), dim=-1).to(my_pos.device).reshape(batchsize, self.ref ** 3, 3) # B 4 4 4 3
113
+
114
+ pos = torch.sqrt(
115
+ torch.sum((my_pos[:, :, None, :] - grid_ref[:, None, :, :]) ** 2,
116
+ dim=-1)). \
117
+ reshape(batchsize, my_pos.shape[1], self.ref * self.ref * self.ref).contiguous()
118
+ return pos
119
+
120
+ def forward(self, data):
121
+ cfd_data = data
122
+ x, fx, T = cfd_data.x, None, None
123
+ x = x[None, :, :] # [1, N, C]
124
+
125
+ if self.unified_pos:
126
+ new_pos = self.get_grid(cfd_data.pos[None, :, :])
127
+ x = torch.cat((x, new_pos), dim=-1)
128
+
129
+ if fx is not None:
130
+ fx = torch.cat((x, fx), -1)
131
+ fx = self.preprocess(fx)
132
+ else:
133
+ fx = self.preprocess(x)
134
+ fx = fx + self.placeholder[None, None, :]
135
+
136
+ for block in self.blocks:
137
+ fx = block(fx)
138
+
139
+ return fx[0]
model/Transolver3D_plus.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+ from timm.layers import trunc_normal_
5
+ from einops import rearrange, repeat
6
+ from onescience.modules.mlp.MLP import StandardMLP
7
+ from onescience.modules.transformer.Transolver_block import Transolver_block
8
+
9
+ class Transolver3D_plus(nn.Module):
10
+ """
11
+ Transolver3D_plus 模型。
12
+
13
+ 这是 Transolver3D 的增强版本。
14
+ 它使用了 'unstructured_plus' 类型的注意力机制,
15
+ 这种机制引入了更复杂的特征交互(如 Gumbel Softmax 加权切片),旨在提升对三维不规则网格物理场的建模能力。
16
+
17
+ Args:
18
+ space_dim (int): 空间维度。默认值: 1。
19
+ n_layers (int): Block 层数。默认值: 5。
20
+ n_hidden (int): 隐藏层维度。默认值: 256。
21
+ dropout (float): Dropout 概率。默认值: 0。
22
+ n_head (int): 注意力头数。默认值: 8。
23
+ act (str): 激活函数。默认值: 'gelu'。
24
+ mlp_ratio (float): MLP 膨胀比率。默认值: 1。
25
+ fun_dim (int): 输入物理场特征维度。默认值: 1。
26
+ out_dim (int): 输出特征维度。默认值: 1。
27
+ slice_num (int): 切片数量。默认值: 32。
28
+ ref (int): 统一位置编码参考分辨率。默认值: 8。
29
+ unified_pos (bool): 是否使用统一位置编码。默认值: False。
30
+ """
31
+ def __init__(self,
32
+ space_dim=1,
33
+ n_layers=5,
34
+ n_hidden=256,
35
+ dropout=0,
36
+ n_head=8,
37
+ act='gelu',
38
+ mlp_ratio=1,
39
+ fun_dim=1,
40
+ out_dim=1,
41
+ slice_num=32,
42
+ ref=8,
43
+ unified_pos=False
44
+ ):
45
+ super(Transolver3D_plus, self).__init__()
46
+ self.__name__ = 'Transolver3D'
47
+ self.ref = ref
48
+ self.unified_pos = unified_pos
49
+
50
+ if self.unified_pos:
51
+ input_dim = fun_dim + self.ref * self.ref * self.ref
52
+ else:
53
+ input_dim = fun_dim + space_dim
54
+
55
+ self.preprocess = StandardMLP(
56
+ input_dim=input_dim,
57
+ hidden_dims=[n_hidden * 2],
58
+ output_dim=n_hidden,
59
+ activation=act,
60
+ use_bias=True,
61
+ use_skip_connection=False
62
+ )
63
+
64
+ self.n_hidden = n_hidden
65
+ self.space_dim = space_dim
66
+
67
+ self.blocks = nn.ModuleList([
68
+ Transolver_block(
69
+ num_heads=n_head,
70
+ hidden_dim=n_hidden,
71
+ dropout=dropout,
72
+ act=act,
73
+ mlp_ratio=mlp_ratio,
74
+ out_dim=out_dim,
75
+ slice_num=slice_num,
76
+ last_layer=(_ == n_layers - 1),
77
+ geotype='unstructured_plus'
78
+ )
79
+ for _ in range(n_layers)
80
+ ])
81
+
82
+ self.initialize_weights()
83
+ self.placeholder = nn.Parameter((1 / (n_hidden)) * torch.rand(n_hidden, dtype=torch.float))
84
+
85
+ def initialize_weights(self):
86
+ self.apply(self._init_weights)
87
+
88
+ def _init_weights(self, m):
89
+ if isinstance(m, nn.Linear):
90
+ trunc_normal_(m.weight, std=0.02)
91
+ if isinstance(m, nn.Linear) and m.bias is not None:
92
+ nn.init.constant_(m.bias, 0)
93
+ elif isinstance(m, (nn.LayerNorm, nn.BatchNorm1d)):
94
+ nn.init.constant_(m.bias, 0)
95
+ nn.init.constant_(m.weight, 1.0)
96
+
97
+ def get_grid(self, my_pos):
98
+ batchsize = my_pos.shape[0]
99
+
100
+ # 3D Grid Generation Logic
101
+ gridx = torch.tensor(np.linspace(-1.5, 1.5, self.ref), dtype=torch.float)
102
+ gridx = gridx.reshape(1, self.ref, 1, 1, 1).repeat([batchsize, 1, self.ref, self.ref, 1])
103
+ gridy = torch.tensor(np.linspace(0, 2, self.ref), dtype=torch.float)
104
+ gridy = gridy.reshape(1, 1, self.ref, 1, 1).repeat([batchsize, self.ref, 1, self.ref, 1])
105
+ gridz = torch.tensor(np.linspace(-4, 4, self.ref), dtype=torch.float)
106
+ gridz = gridz.reshape(1, 1, 1, self.ref, 1).repeat([batchsize, self.ref, self.ref, 1, 1])
107
+
108
+ grid_ref = torch.cat((gridx, gridy, gridz), dim=-1).to(my_pos.device).reshape(batchsize, self.ref ** 3, 3)
109
+
110
+ pos = torch.sqrt(
111
+ torch.sum((my_pos[:, :, None, :] - grid_ref[:, None, :, :]) ** 2,
112
+ dim=-1)). \
113
+ reshape(batchsize, my_pos.shape[1], self.ref * self.ref * self.ref).contiguous()
114
+ return pos
115
+
116
+ def forward(self, data):
117
+ cfd_data = data
118
+ x, fx, T = cfd_data.x, None, None
119
+ x = x[None, :, :]
120
+
121
+ if self.unified_pos:
122
+ new_pos = self.get_grid(cfd_data.pos[None, :, :])
123
+ x = torch.cat((x, new_pos), dim=-1)
124
+
125
+ if fx is not None:
126
+ fx = torch.cat((x, fx), -1)
127
+ fx = self.preprocess(fx)
128
+ else:
129
+ fx = self.preprocess(x)
130
+ fx = fx + self.placeholder[None, None, :]
131
+
132
+ for block in self.blocks:
133
+ fx = block(fx)
134
+
135
+ return fx[0]
model/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .Transolver3D import Transolver3D
2
+ from .Transolver3D_plus import Transolver3D_plus
3
+
4
+ __all__ = ["Transolver3D", "Transolver3D_plus"]
scripts/fake_data.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(ROOT))
11
+ os.chdir(ROOT)
12
+
13
+ from onescience.utils.YParams import YParams
14
+
15
+
16
+ def make_edges(num_nodes: int) -> np.ndarray:
17
+ src = np.arange(num_nodes, dtype=np.int64)
18
+ dst = (src + 1) % num_nodes
19
+ edges = np.stack(
20
+ [np.concatenate([src, dst]), np.concatenate([dst, src])],
21
+ axis=0,
22
+ )
23
+ return edges
24
+
25
+
26
+ def write_placeholder_training_files(param_dir: Path, rng: np.random.Generator) -> None:
27
+ param_dir.mkdir(parents=True, exist_ok=True)
28
+ np.save(param_dir / "Cd.npy", rng.normal(size=(1,)).astype(np.float32))
29
+ np.save(param_dir / "I1.npy", rng.normal(size=(8, 3)).astype(np.float32))
30
+ np.save(param_dir / "I2.npy", rng.normal(size=(8, 3)).astype(np.float32))
31
+ np.save(param_dir / "Press.npy", rng.normal(size=(8, 1)).astype(np.float32))
32
+ np.save(param_dir / "Velo.npy", rng.normal(size=(8, 3)).astype(np.float32))
33
+
34
+
35
+ def write_sample(sample_dir: Path, num_nodes: int, rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]:
36
+ sample_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ pos = rng.uniform(low=-1.0, high=1.0, size=(num_nodes, 3)).astype(np.float32)
39
+ sdf = rng.uniform(low=0.0, high=0.2, size=(num_nodes, 1)).astype(np.float32)
40
+ normals = rng.normal(size=(num_nodes, 3)).astype(np.float32)
41
+ normals /= np.linalg.norm(normals, axis=1, keepdims=True) + 1e-6
42
+ x = np.concatenate([pos, sdf, normals], axis=1).astype(np.float32)
43
+
44
+ y = np.concatenate(
45
+ [
46
+ 0.1 * pos + rng.normal(scale=0.01, size=(num_nodes, 3)),
47
+ rng.normal(scale=0.05, size=(num_nodes, 1)),
48
+ ],
49
+ axis=1,
50
+ ).astype(np.float32)
51
+
52
+ surf = np.zeros((num_nodes,), dtype=np.bool_)
53
+ surf[num_nodes // 2 :] = True
54
+ edge_index = make_edges(num_nodes)
55
+
56
+ np.save(sample_dir / "x.npy", x)
57
+ np.save(sample_dir / "y.npy", y)
58
+ np.save(sample_dir / "pos.npy", pos)
59
+ np.save(sample_dir / "surf.npy", surf)
60
+ np.save(sample_dir / "edge_index.npy", edge_index)
61
+ return x, y
62
+
63
+
64
+ def main() -> None:
65
+ cfg = YParams(str(ROOT / "conf/config.yaml"), "datapipe")
66
+ data_dir = ROOT / cfg.source.data_dir
67
+ preprocessed_dir = ROOT / cfg.source.preprocessed_save_dir
68
+ stats_dir = ROOT / cfg.source.stats_dir
69
+ stats_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ rng = np.random.default_rng(42)
72
+ all_x: list[np.ndarray] = []
73
+ all_y: list[np.ndarray] = []
74
+
75
+ for fold_id in range(9):
76
+ param_name = f"param{fold_id}"
77
+ param_dir = data_dir / param_name
78
+ write_placeholder_training_files(param_dir, rng)
79
+
80
+ sample_name = "sample_000"
81
+ (param_dir / sample_name).mkdir(parents=True, exist_ok=True)
82
+ x, y = write_sample(preprocessed_dir / param_name / sample_name, num_nodes=8, rng=rng)
83
+ all_x.append(x)
84
+ all_y.append(y)
85
+
86
+ x_all = np.concatenate(all_x, axis=0)
87
+ y_all = np.concatenate(all_y, axis=0)
88
+ np.save(stats_dir / "mean_in.npy", x_all.mean(axis=0).astype(np.float32))
89
+ np.save(stats_dir / "std_in.npy", (x_all.std(axis=0) + 1e-6).astype(np.float32))
90
+ np.save(stats_dir / "mean_out.npy", y_all.mean(axis=0).astype(np.float32))
91
+ np.save(stats_dir / "std_out.npy", (y_all.std(axis=0) + 1e-6).astype(np.float32))
92
+
93
+ print(f"Fake ShapeNetCar data generated under {ROOT / 'data/mlcfd_data'}")
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ import time
7
+ import importlib.util
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import torch
12
+ import torch.nn as nn
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(ROOT))
16
+ os.chdir(ROOT)
17
+
18
+ from model import Transolver3D, Transolver3D_plus
19
+ import onescience
20
+ from onescience.distributed.manager import DistributedManager
21
+ from onescience.utils.YParams import YParams
22
+ from onescience.utils.transolver import cal_coefficient, save_prediction_to_vtk, visualize_prediction
23
+
24
+
25
+ def load_shapenet_car_datapipe():
26
+ module_path = Path(onescience.__file__).resolve().parent / "datapipes/cfd/ShapeNetCar.py"
27
+ spec = importlib.util.spec_from_file_location("_onescience_shapenetcar", module_path)
28
+ if spec is None or spec.loader is None:
29
+ raise ImportError(f"Unable to load ShapeNetCarDatapipe from {module_path}")
30
+ module = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(module)
32
+ return module.ShapeNetCarDatapipe
33
+
34
+
35
+ def setup_logging(rank: int) -> logging.Logger:
36
+ level = logging.INFO if rank == 0 else logging.WARNING
37
+ logging.basicConfig(
38
+ level=level,
39
+ format="%(asctime)s - %(levelname)s - %(message)s",
40
+ datefmt="%Y-%m-%d %H:%M:%S",
41
+ )
42
+ logging.getLogger().setLevel(level)
43
+ return logging.getLogger(__name__)
44
+
45
+
46
+ def build_model(model_name: str, model_params, device: torch.device) -> torch.nn.Module:
47
+ model_cls = {
48
+ "Transolver": Transolver3D,
49
+ "Transolver_plus": Transolver3D_plus,
50
+ }.get(model_name)
51
+ if model_cls is None:
52
+ raise NotImplementedError(f"Model {model_name} initialization not implemented.")
53
+ return model_cls(
54
+ n_hidden=model_params.n_hidden,
55
+ n_layers=model_params.n_layers,
56
+ space_dim=model_params.space_dim,
57
+ fun_dim=model_params.fun_dim,
58
+ n_head=model_params.n_head,
59
+ mlp_ratio=model_params.mlp_ratio,
60
+ out_dim=model_params.out_dim,
61
+ slice_num=model_params.slice_num,
62
+ unified_pos=model_params.unified_pos,
63
+ ).to(device)
64
+
65
+
66
+ def resolve_device(gpuid: int) -> torch.device:
67
+ if torch.cuda.is_available() and int(gpuid) >= 0:
68
+ return torch.device(f"cuda:{gpuid}")
69
+ return torch.device("cpu")
70
+
71
+
72
+ def maybe_calculate_coefficient(data_dir: Path, pred_press: np.ndarray, pred_velo: np.ndarray, gt_press: np.ndarray, gt_velo: np.ndarray):
73
+ if not (data_dir / "quadpress_smpl.vtk").exists() or not (data_dir / "hexvelo_smpl.vtk").exists():
74
+ return None, None
75
+ pred_coef = cal_coefficient(str(data_dir), pred_press[:, None], pred_velo)
76
+ gt_coef = cal_coefficient(str(data_dir), gt_press[:, None], gt_velo)
77
+ return pred_coef, gt_coef
78
+
79
+
80
+ def main() -> None:
81
+ DistributedManager.initialize()
82
+ manager = DistributedManager()
83
+ logger = setup_logging(manager.rank)
84
+ if manager.rank != 0:
85
+ logger.warning("Inference should run on a single process; exiting non-zero rank.")
86
+ return
87
+
88
+ config_file_path = str(ROOT / "conf/config.yaml")
89
+ cfg = YParams(config_file_path, "model")
90
+ cfg_data = YParams(config_file_path, "datapipe")
91
+ cfg_train = YParams(config_file_path, "training")
92
+ cfg_test = YParams(config_file_path, "inference")
93
+
94
+ model_name = cfg.name
95
+ model_params = cfg.specific_params[model_name]
96
+ cfg_data.model_hparams = model_params
97
+
98
+ device = resolve_device(cfg_test.gpuid)
99
+ logger.info("Using device: %s", device)
100
+
101
+ ShapeNetCarDatapipe = load_shapenet_car_datapipe()
102
+ datapipe = ShapeNetCarDatapipe(params=cfg_data, distributed=False)
103
+ val_dataset = datapipe.val_dataset
104
+ coef_norm = datapipe.coef_norm
105
+ val_names = val_dataset.data_list_names
106
+ test_loader, _ = datapipe.val_dataloader()
107
+ logger.info("Loaded %d validation samples.", len(val_dataset))
108
+
109
+ model = build_model(model_name, model_params, device)
110
+ checkpoint_path = Path(cfg_train.checkpoint_dir) / f"{model_name}.pth"
111
+ if not checkpoint_path.exists():
112
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
113
+ checkpoint = torch.load(checkpoint_path, map_location=device)
114
+ model.load_state_dict(checkpoint["model_state_dict"])
115
+ model.eval()
116
+
117
+ result_root = Path(cfg_test.result_dir) / model_name
118
+ npy_dir = result_root / "npy"
119
+ vtk_dir = result_root / "vtk"
120
+ vis_dir = result_root / "vis"
121
+ npy_dir.mkdir(parents=True, exist_ok=True)
122
+ if cfg_test.save_vtk:
123
+ vtk_dir.mkdir(parents=True, exist_ok=True)
124
+ if cfg_test.visualize:
125
+ vis_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ criterion_func = nn.MSELoss(reduction="none")
128
+ l2errs_press, l2errs_velo, mses_press, mses_velo_var, times = [], [], [], [], []
129
+ gt_coef_list, pred_coef_list = [], []
130
+
131
+ mean = torch.tensor(coef_norm[2], dtype=torch.float32, device=device)
132
+ std = torch.tensor(coef_norm[3], dtype=torch.float32, device=device)
133
+
134
+ with torch.no_grad():
135
+ for index, data in enumerate(test_loader):
136
+ if index >= len(val_names):
137
+ break
138
+ sample_name = val_names[index]
139
+ data = data.to(device)
140
+ tic = time.time()
141
+ out = model(data)
142
+ times.append(time.time() - tic)
143
+ targets = data.y
144
+
145
+ pred_press = out[data.surf, -1] * std[-1] + mean[-1]
146
+ gt_press = targets[data.surf, -1] * std[-1] + mean[-1]
147
+ pred_velo = out[~data.surf, :-1] * std[:-1] + mean[:-1]
148
+ gt_velo = targets[~data.surf, :-1] * std[:-1] + mean[:-1]
149
+ out_denorm = out * std + mean
150
+ y_denorm = targets * std + mean
151
+
152
+ safe_name = sample_name.replace("/", "_")
153
+ np.save(npy_dir / f"{index}_{safe_name}_pred.npy", out_denorm.cpu().numpy())
154
+ np.save(npy_dir / f"{index}_{safe_name}_gt.npy", y_denorm.cpu().numpy())
155
+
156
+ data_dir = ROOT / cfg_data.source.data_dir / sample_name
157
+ pred_coef, gt_coef = maybe_calculate_coefficient(
158
+ data_dir,
159
+ pred_press.cpu().numpy(),
160
+ pred_velo.cpu().numpy(),
161
+ gt_press.cpu().numpy(),
162
+ gt_velo.cpu().numpy(),
163
+ )
164
+ if pred_coef is not None and gt_coef is not None:
165
+ pred_coef_list.append(pred_coef)
166
+ gt_coef_list.append(gt_coef)
167
+
168
+ l2errs_press.append((torch.norm(pred_press - gt_press) / (torch.norm(gt_press) + 1e-8)).cpu().numpy())
169
+ l2errs_velo.append((torch.norm(pred_velo - gt_velo) / (torch.norm(gt_velo) + 1e-8)).cpu().numpy())
170
+ mses_press.append(criterion_func(out[data.surf, -1], targets[data.surf, -1]).mean().cpu().numpy())
171
+ mses_velo_var.append(criterion_func(out[~data.surf, :-1], targets[~data.surf, :-1]).mean().cpu().numpy())
172
+
173
+ if cfg_test.save_vtk and (data_dir / "quadpress_smpl.vtk").exists():
174
+ save_prediction_to_vtk(
175
+ out_denorm=out_denorm,
176
+ targets=targets,
177
+ cfd_data=data,
178
+ sample_name=sample_name,
179
+ output_dir=str(vtk_dir),
180
+ index=index,
181
+ data_dir=str(ROOT / cfg_data.source.data_dir),
182
+ )
183
+ if cfg_test.visualize and cfg_test.save_vtk:
184
+ visualize_prediction(output_dir=str(vtk_dir), vis_dir=str(vis_dir), index=index)
185
+
186
+ logger.info("Results saved to: %s", result_root)
187
+ logger.info("Relative L2 pressure: %.6f", float(np.mean(l2errs_press)))
188
+ logger.info("Relative L2 velocity: %.6f", float(np.mean(l2errs_velo)))
189
+ logger.info("RMSE pressure: %.6f", float(np.sqrt(np.mean(mses_press)) * coef_norm[3][-1]))
190
+ rmse_velo = np.sqrt(np.mean(mses_velo_var, axis=0)) * coef_norm[3][:-1]
191
+ logger.info("Combined velocity RMSE: %.6f", float(np.sqrt(np.mean(np.square(rmse_velo)))))
192
+ logger.info("Mean inference time (s): %.6f", float(np.mean(times)))
193
+ if gt_coef_list:
194
+ coef_error = np.mean(np.abs(np.array(pred_coef_list) - np.array(gt_coef_list)) / (np.array(gt_coef_list) + 1e-8))
195
+ logger.info("Mean relative CD error: %.6f", float(coef_error))
196
+ else:
197
+ logger.info("Skipped drag coefficient metrics because VTK geometry files were not present.")
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
scripts/result.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ sys.path.insert(0, str(ROOT))
9
+ os.chdir(ROOT)
10
+
11
+ from onescience.utils.YParams import YParams
12
+
13
+
14
+ def main() -> None:
15
+ cfg = YParams(str(ROOT / "conf/config.yaml"), "model")
16
+ cfg_test = YParams(str(ROOT / "conf/config.yaml"), "inference")
17
+ result_root = ROOT / cfg_test.result_dir / cfg.name
18
+ npy_dir = result_root / "npy"
19
+ pred_files = sorted(npy_dir.glob("*_pred.npy")) if npy_dir.exists() else []
20
+ gt_files = sorted(npy_dir.glob("*_gt.npy")) if npy_dir.exists() else []
21
+
22
+ print(f"Result directory: {result_root}")
23
+ print(f"Prediction files: {len(pred_files)}")
24
+ print(f"Ground-truth files: {len(gt_files)}")
25
+ if pred_files:
26
+ print(f"First prediction: {pred_files[0]}")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
scripts/train.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ import time
7
+ import importlib.util
8
+ from pathlib import Path
9
+
10
+ import torch
11
+ import torch.distributed as dist
12
+ import torch.nn as nn
13
+ from torch.nn.parallel import DistributedDataParallel
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(ROOT))
17
+ os.chdir(ROOT)
18
+
19
+ from model import Transolver3D, Transolver3D_plus
20
+ import onescience
21
+ from onescience.distributed.manager import DistributedManager
22
+ from onescience.utils.YParams import YParams
23
+
24
+
25
+ def load_shapenet_car_datapipe():
26
+ module_path = Path(onescience.__file__).resolve().parent / "datapipes/cfd/ShapeNetCar.py"
27
+ spec = importlib.util.spec_from_file_location("_onescience_shapenetcar", module_path)
28
+ if spec is None or spec.loader is None:
29
+ raise ImportError(f"Unable to load ShapeNetCarDatapipe from {module_path}")
30
+ module = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(module)
32
+ return module.ShapeNetCarDatapipe
33
+
34
+
35
+ def setup_logging(rank: int) -> logging.Logger:
36
+ level = logging.INFO if rank == 0 else logging.WARNING
37
+ logging.basicConfig(
38
+ level=level,
39
+ format="%(asctime)s - %(levelname)s - %(message)s",
40
+ datefmt="%Y-%m-%d %H:%M:%S",
41
+ )
42
+ logging.getLogger().setLevel(level)
43
+ return logging.getLogger(__name__)
44
+
45
+
46
+ def build_model(model_name: str, model_params, device: torch.device) -> torch.nn.Module:
47
+ model_cls = {
48
+ "Transolver": Transolver3D,
49
+ "Transolver_plus": Transolver3D_plus,
50
+ }.get(model_name)
51
+ if model_cls is None:
52
+ raise NotImplementedError(f"Model {model_name} initialization not implemented.")
53
+
54
+ return model_cls(
55
+ n_hidden=model_params.n_hidden,
56
+ n_layers=model_params.n_layers,
57
+ space_dim=model_params.space_dim,
58
+ fun_dim=model_params.fun_dim,
59
+ n_head=model_params.n_head,
60
+ mlp_ratio=model_params.mlp_ratio,
61
+ out_dim=model_params.out_dim,
62
+ slice_num=model_params.slice_num,
63
+ unified_pos=model_params.unified_pos,
64
+ ).to(device)
65
+
66
+
67
+ def resolve_device(gpuid: int) -> torch.device:
68
+ if torch.cuda.is_available() and int(gpuid) >= 0:
69
+ return torch.device(f"cuda:{gpuid}")
70
+ return torch.device("cpu")
71
+
72
+
73
+ def save_checkpoint(model, optimizer, scheduler, epoch: int, loss: float, ckp_dir: str, model_name: str) -> None:
74
+ Path(ckp_dir).mkdir(parents=True, exist_ok=True)
75
+ model_to_save = model.module if hasattr(model, "module") else model
76
+ torch.save(
77
+ {
78
+ "model_state_dict": model_to_save.state_dict(),
79
+ "optimizer_state_dict": optimizer.state_dict(),
80
+ "scheduler_state_dict": scheduler.state_dict(),
81
+ "epoch": epoch,
82
+ "loss": loss,
83
+ },
84
+ Path(ckp_dir) / f"{model_name}.pth",
85
+ )
86
+
87
+
88
+ def main() -> None:
89
+ DistributedManager.initialize()
90
+ manager = DistributedManager()
91
+ logger = setup_logging(manager.rank)
92
+
93
+ config_file_path = str(ROOT / "conf/config.yaml")
94
+ cfg = YParams(config_file_path, "model")
95
+ cfg_data = YParams(config_file_path, "datapipe")
96
+ cfg_train = YParams(config_file_path, "training")
97
+
98
+ model_name = cfg.name
99
+ if model_name not in cfg.specific_params:
100
+ raise ValueError(f"Model '{model_name}' not found in model.specific_params.")
101
+ model_params = cfg.specific_params[model_name]
102
+ cfg_data.model_hparams = model_params
103
+
104
+ logger.info("Initializing ShapeNetCar datapipe...")
105
+ ShapeNetCarDatapipe = load_shapenet_car_datapipe()
106
+ datapipe = ShapeNetCarDatapipe(params=cfg_data, distributed=(manager.world_size > 1))
107
+ train_dataloader, train_sampler = datapipe.train_dataloader()
108
+ val_dataloader, val_sampler = datapipe.val_dataloader()
109
+
110
+ if manager.world_size > 1:
111
+ device = torch.device(f"cuda:{manager.local_rank}" if torch.cuda.is_available() else "cpu")
112
+ else:
113
+ device = resolve_device(cfg_train.gpuid)
114
+
115
+ model = build_model(model_name, model_params, device)
116
+ if manager.rank == 0:
117
+ total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
118
+ logger.info("Model: %s, trainable params: %.2fM", model_name, total_params / 1e6)
119
+
120
+ if manager.world_size > 1:
121
+ model = DistributedDataParallel(
122
+ model,
123
+ device_ids=[manager.local_rank],
124
+ output_device=manager.local_rank,
125
+ find_unused_parameters=True,
126
+ )
127
+
128
+ optimizer = torch.optim.Adam(model.parameters(), lr=cfg_train.lr)
129
+ scheduler = torch.optim.lr_scheduler.OneCycleLR(
130
+ optimizer,
131
+ max_lr=cfg_train.lr,
132
+ total_steps=max(1, len(train_dataloader) * cfg_train.max_epoch),
133
+ )
134
+ if cfg_train.loss_criterion == "MSE":
135
+ loss_criterion = nn.MSELoss(reduction="none")
136
+ elif cfg_train.loss_criterion == "MAE":
137
+ loss_criterion = nn.L1Loss(reduction="none")
138
+ else:
139
+ raise ValueError(f"Unknown loss criterion: {cfg_train.loss_criterion}")
140
+
141
+ best_valid_loss = 1.0e6
142
+ best_loss_epoch = 0
143
+ logger.info("Starting training...")
144
+
145
+ for epoch in range(cfg_train.max_epoch):
146
+ epoch_start_time = time.time()
147
+ if manager.world_size > 1:
148
+ train_sampler.set_epoch(epoch)
149
+ if val_sampler is not None:
150
+ val_sampler.set_epoch(epoch)
151
+
152
+ model.train()
153
+ train_loss = train_loss_press = train_loss_velo = 0.0
154
+ for data in train_dataloader:
155
+ data = data.to(device)
156
+ optimizer.zero_grad()
157
+ out = model(data)
158
+ targets = data.y
159
+ loss_press = loss_criterion(out[data.surf, -1], targets[data.surf, -1]).mean()
160
+ loss_velo = loss_criterion(out[:, :-1], targets[:, :-1]).mean()
161
+ loss = loss_velo + cfg_train.loss_weight * loss_press
162
+ loss.backward()
163
+ optimizer.step()
164
+ scheduler.step()
165
+ train_loss += loss.item()
166
+ train_loss_press += loss_press.item()
167
+ train_loss_velo += loss_velo.item()
168
+
169
+ train_loss /= max(1, len(train_dataloader))
170
+ train_loss_press /= max(1, len(train_dataloader))
171
+ train_loss_velo /= max(1, len(train_dataloader))
172
+
173
+ valid_loss = valid_loss_press = valid_loss_velo = 0.0
174
+ if (epoch + 1) % cfg_train.val_iter == 0 or epoch == cfg_train.max_epoch - 1:
175
+ model.eval()
176
+ with torch.no_grad():
177
+ for data in val_dataloader:
178
+ data = data.to(device)
179
+ out = model(data)
180
+ targets = data.y
181
+ loss_press = loss_criterion(out[data.surf, -1], targets[data.surf, -1]).mean()
182
+ loss_velo = loss_criterion(out[:, :-1], targets[:, :-1]).mean()
183
+ loss = loss_velo + cfg_train.loss_weight * loss_press
184
+ if manager.world_size > 1:
185
+ dist.all_reduce(loss, op=dist.ReduceOp.AVG)
186
+ dist.all_reduce(loss_press, op=dist.ReduceOp.AVG)
187
+ dist.all_reduce(loss_velo, op=dist.ReduceOp.AVG)
188
+ valid_loss += loss.item()
189
+ valid_loss_press += loss_press.item()
190
+ valid_loss_velo += loss_velo.item()
191
+
192
+ valid_loss /= max(1, len(val_dataloader))
193
+ valid_loss_press /= max(1, len(val_dataloader))
194
+ valid_loss_velo /= max(1, len(val_dataloader))
195
+
196
+ if manager.rank == 0:
197
+ logger.info(
198
+ "Epoch [%d/%d] | Time: %.2fs | Train: %.6f (velo %.6f, press %.6f) | "
199
+ "Valid: %.6f (velo %.6f, press %.6f)",
200
+ epoch + 1,
201
+ cfg_train.max_epoch,
202
+ time.time() - epoch_start_time,
203
+ train_loss,
204
+ train_loss_velo,
205
+ train_loss_press,
206
+ valid_loss,
207
+ valid_loss_velo,
208
+ valid_loss_press,
209
+ )
210
+ if valid_loss > 0 and valid_loss < best_valid_loss:
211
+ best_valid_loss = valid_loss
212
+ best_loss_epoch = epoch
213
+ save_checkpoint(model, optimizer, scheduler, epoch, valid_loss, cfg_train.checkpoint_dir, model_name)
214
+ logger.info("New best checkpoint saved to %s/%s.pth", cfg_train.checkpoint_dir, model_name)
215
+ if epoch - best_loss_epoch > cfg_train.patience:
216
+ logger.warning("Validation loss has not improved for %d epochs. Stopping.", cfg_train.patience)
217
+ break
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
weight/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+