Upload folder using huggingface_hub
Browse files- README.md +125 -0
- conf/config.yaml +49 -0
- configuration.json +1 -0
- model/Transolver3D.py +139 -0
- model/Transolver3D_plus.py +135 -0
- model/__init__.py +4 -0
- scripts/fake_data.py +97 -0
- scripts/inference.py +201 -0
- scripts/result.py +30 -0
- scripts/train.py +221 -0
- weight/.gitkeep +1 -0
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: ¶ms
|
| 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 |
+
|