OneScience commited on
Commit
911bf32
·
verified ·
1 Parent(s): e177425

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,49 @@
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
  *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
  *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
  *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
11
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
17
+
 
 
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.tar filter=lfs diff=lfs merge=lfs -text
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
  *tfevents* filter=lfs diff=lfs merge=lfs -text
48
+
49
+ *.pt filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - pytorch
4
+ language:
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - GFNO
10
+ - neural-operator
11
+ - group-equivariance
12
+ - Fourier-neural-operator
13
+ - Navier-Stokes
14
+ tasks:
15
+ - time-series-prediction
16
+ ---
17
+ <p align="center">
18
+ <strong>
19
+ <span style="font-size: 30px;">GFNO</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Overview
24
+
25
+ GFNO (Group Equivariant Fourier Neural Operator) is a Fourier neural operator augmented with group-equivariant structure for learning physical systems with geometric symmetries. This model predicts Navier–Stokes time series on a two-dimensional regular grid. By default, it uses the C4 rotation group and the first 10 time steps to predict the subsequent 10.
26
+
27
+ Paper: Group Equivariant Fourier Neural Operators for Partial Differential Equations
28
+ https://openreview.net/forum?id=kgAOY5x4fi
29
+
30
+ # Model Description
31
+
32
+ GFNO adds group-equivariant weight sharing to the spectral convolutions of a Fourier Neural Operator, producing consistent responses under rotations or reflections and making more effective use of geometric symmetries in Navier–Stokes flow fields. The model supports the C4 rotation group and D4 dihedral group and performs multistep flow prediction autoregressively.
33
+
34
+ # Use Cases
35
+
36
+ | Use Case | Description |
37
+ | :---: | :--- |
38
+ | Flow-field time-series prediction | Predict future Navier–Stokes states from historical vorticity fields |
39
+ | Geometric-equivariance research | Compare a standard FNO with C4- and D4-equivariant operators |
40
+ | CFD surrogate modeling | Learn mappings from historical to future physical fields on regular grids |
41
+ | Pipeline validation | Validate model training, inference, and result visualization with a small-scale configuration |
42
+
43
+ # Usage
44
+
45
+ ## 1. OneCode
46
+
47
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
48
+
49
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
50
+
51
+ ## 2. Manual Setup
52
+
53
+ **Hardware Requirements**
54
+
55
+ - A GPU or DCU is recommended for training and inference.
56
+ - A CPU can be used for small-scale pipeline validation, but full training will be slow.
57
+ - DCU users must install DTK and a PyTorch environment compatible with the target cluster.
58
+
59
+ ### Download the Model Package
60
+
61
+ ```bash
62
+ modelscope download --model OneScience/GFNO --local_dir ./GFNO
63
+ cd GFNO
64
+ ```
65
+
66
+ ### Set Up the Runtime Environment
67
+
68
+ **DCU Environment**
69
+
70
+ ```bash
71
+ # Activate DTK and Conda first
72
+ conda create -n onescience311 python=3.11 -y
73
+ conda activate onescience311
74
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
75
+ ```
76
+
77
+ **GPU Environment**
78
+
79
+ ```bash
80
+ # Activate Conda first
81
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
82
+ conda activate onescience311
83
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
84
+ ```
85
+
86
+ ### Training Data
87
+
88
+ The model uses the standard Navier–Stokes dataset `NavierStokes_V1e-5_N1200_T20.mat`, in which the data variable `u` has shape `[1200, 64, 64, 20]`. Download the data with the following command and verify that the data path in `conf/config.yaml` is configured correctly:
89
+
90
+ ```bash
91
+ modelscope download --dataset OneScience/cfd_benchmark data/ns/NavierStokes_V1e-5_N1200_T20.mat --local_dir ./data
92
+ ```
93
+
94
+ ### Training
95
+
96
+ ```bash
97
+ python scripts/train.py
98
+ ```
99
+
100
+ The default weights are saved to `weight/gfno_navier_stokes.pt`.
101
+
102
+ ### Model Weights
103
+
104
+ This repository provides weights trained on the standard Navier–Stokes dataset in the `weight/` directory.
105
+
106
+ ### Inference, Evaluation, and Visualization
107
+
108
+ After training, run:
109
+
110
+ ```bash
111
+ python scripts/inference.py
112
+ ```
113
+
114
+ The script loads `weight/gfno_navier_stokes.pt` by default. Predicted tensors and visualizations are saved to the `result/` directory. Training and inference parameters can be modified in `conf/config.yaml`.
115
+
116
+ # Official OneScience Resources
117
+
118
+ | Platform | OneScience Repository | Skills Repository |
119
+ | --- | --- | --- |
120
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
121
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
122
+
123
+ # Citations and License
124
+
125
+ - Romero, D. W., Bekkers, E. J., Tomczak, J. M., and Hoogendoorn, M. Group Equivariant Fourier Neural Operators for Partial Differential Equations.
126
+ - Li, Z. et al. Fourier Neural Operator for Parametric Partial Differential Equations. arXiv:2010.08895, 2020.
127
+ - This model package is released under the Apache-2.0 license and retains attribution to the original paper and data sources.
conf/config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root:
2
+ common:
3
+ device: "auto"
4
+ seed: 0
5
+
6
+ datapipe:
7
+ source:
8
+ data_dir: "/public/share/sugonhpcapp01/onestore/onedatasets/CFD_Benchmark/ns"
9
+ file_name: "NavierStokes_V1e-5_N1200_T20.mat"
10
+ data:
11
+ ntrain: 1000
12
+ ntest: 200
13
+ t_in: 10
14
+ t_out: 10
15
+ out_dim: 1
16
+ downsamplex: 1
17
+ downsampley: 1
18
+ normalize: false
19
+ dataloader:
20
+ batch_size: 8
21
+ num_workers: 4
22
+ pin_memory: true
23
+
24
+ model:
25
+ in_dim: 10
26
+ out_dim: 1
27
+ hidden_dim: 64
28
+ modes: 12
29
+ num_layers: 4
30
+ space_dim: 2
31
+ include_pos: true
32
+ activation: "gelu"
33
+ reflection: false
34
+ pad_to_multiple: 16
35
+
36
+ training:
37
+ epochs: 500
38
+ lr: 0.001
39
+ weight_decay: 0.00001
40
+ step_size: 100
41
+ gamma: 0.5
42
+ eval_interval: 1
43
+ patience: 100
44
+ teacher_forcing: false
45
+ max_grad_norm: null
46
+ max_train_batches: null
47
+ max_eval_batches: null
48
+ weight_dir: "weight"
49
+ checkpoint_name: "gfno_navier_stokes.pt"
50
+
51
+ inference:
52
+ result_dir: "result"
53
+ num_samples: 4
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
data/.gitkeep ADDED
File without changes
model/__pycache__/gfno.cpython-311.pyc ADDED
Binary file (25.1 kB). View file
 
model/gfno.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections.abc import Sequence
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+
10
+
11
+ def _activation(name: str) -> nn.Module:
12
+ activations = {
13
+ "gelu": nn.GELU,
14
+ "relu": nn.ReLU,
15
+ "silu": nn.SiLU,
16
+ "tanh": nn.Tanh,
17
+ }
18
+ try:
19
+ return activations[name.lower()]()
20
+ except KeyError as error:
21
+ raise ValueError(f"Unsupported activation: {name}") from error
22
+
23
+
24
+ class MLP(nn.Module):
25
+ def __init__(
26
+ self,
27
+ input_dim: int,
28
+ hidden_dim: int,
29
+ output_dim: int,
30
+ activation: str,
31
+ ) -> None:
32
+ super().__init__()
33
+ self.layers = nn.Sequential(
34
+ nn.Linear(input_dim, hidden_dim),
35
+ _activation(activation),
36
+ nn.Linear(hidden_dim, output_dim),
37
+ )
38
+
39
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
40
+ return self.layers(x)
41
+
42
+
43
+ class GroupEquivariantConv2d(nn.Module):
44
+ """C4/D4 group convolution used by the GFNO lifting and channel mixers."""
45
+
46
+ def __init__(
47
+ self,
48
+ in_channels: int,
49
+ out_channels: int,
50
+ kernel_size: int,
51
+ bias: bool = True,
52
+ first_layer: bool = False,
53
+ last_layer: bool = False,
54
+ spectral: bool = False,
55
+ hermitian: bool = False,
56
+ reflection: bool = False,
57
+ ) -> None:
58
+ super().__init__()
59
+ if kernel_size % 2 != 1:
60
+ raise ValueError("kernel_size must be odd")
61
+ self.in_channels = int(in_channels)
62
+ self.out_channels = int(out_channels)
63
+ self.reflection = bool(reflection)
64
+ self.rotation_group_size = 4
65
+ self.group_size = 4 * (1 + int(self.reflection))
66
+ self.kernel_size_y = int(kernel_size)
67
+ self.kernel_size_x = kernel_size // 2 + 1 if hermitian else kernel_size
68
+ self.first_layer = bool(first_layer)
69
+ self.last_layer = bool(last_layer)
70
+ self.hermitian = bool(hermitian)
71
+ dtype = torch.cfloat if spectral else torch.float32
72
+
73
+ if self.first_layer or self.last_layer:
74
+ self.weight = nn.Parameter(
75
+ torch.empty(
76
+ self.out_channels,
77
+ 1,
78
+ self.in_channels,
79
+ self.kernel_size_y,
80
+ self.kernel_size_x,
81
+ dtype=dtype,
82
+ )
83
+ )
84
+ elif self.hermitian:
85
+ self.weight = nn.ParameterDict(
86
+ {
87
+ "vertical": nn.Parameter(
88
+ torch.empty(
89
+ self.out_channels,
90
+ 1,
91
+ self.in_channels,
92
+ self.group_size,
93
+ self.kernel_size_x - 1,
94
+ 1,
95
+ dtype=dtype,
96
+ )
97
+ ),
98
+ "positive": nn.Parameter(
99
+ torch.empty(
100
+ self.out_channels,
101
+ 1,
102
+ self.in_channels,
103
+ self.group_size,
104
+ self.kernel_size_y,
105
+ self.kernel_size_x - 1,
106
+ dtype=dtype,
107
+ )
108
+ ),
109
+ "origin": nn.Parameter(
110
+ torch.empty(
111
+ self.out_channels,
112
+ 1,
113
+ self.in_channels,
114
+ self.group_size,
115
+ 1,
116
+ 1,
117
+ )
118
+ ),
119
+ }
120
+ )
121
+ else:
122
+ self.weight = nn.Parameter(
123
+ torch.empty(
124
+ self.out_channels,
125
+ 1,
126
+ self.in_channels,
127
+ self.group_size,
128
+ self.kernel_size_y,
129
+ self.kernel_size_x,
130
+ dtype=dtype,
131
+ )
132
+ )
133
+ self.base_bias = (
134
+ nn.Parameter(torch.empty(1, self.out_channels, 1, 1))
135
+ if bias
136
+ else None
137
+ )
138
+ self.reset_parameters()
139
+
140
+ def reset_parameters(self) -> None:
141
+ if isinstance(self.weight, nn.ParameterDict):
142
+ for parameter in self.weight.values():
143
+ nn.init.kaiming_uniform_(parameter, a=math.sqrt(5))
144
+ else:
145
+ nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
146
+ if self.base_bias is not None:
147
+ nn.init.kaiming_uniform_(self.base_bias, a=math.sqrt(5))
148
+
149
+ def transformed_weight(self) -> tuple[torch.Tensor, torch.Tensor | None]:
150
+ if self.hermitian:
151
+ vertical = self.weight["vertical"]
152
+ weights = torch.cat(
153
+ (
154
+ vertical,
155
+ self.weight["origin"].cfloat(),
156
+ vertical.flip(dims=(-2,)).conj(),
157
+ ),
158
+ dim=-2,
159
+ )
160
+ weights = torch.cat((weights, self.weight["positive"]), dim=-1)
161
+ weights = torch.cat(
162
+ (weights[..., 1:].conj().rot90(k=2, dims=(-2, -1)), weights),
163
+ dim=-1,
164
+ )
165
+ else:
166
+ weights = self.weight
167
+
168
+ if self.first_layer or self.last_layer:
169
+ weights = weights.repeat(1, self.group_size, 1, 1, 1)
170
+ for rotation in range(1, self.rotation_group_size):
171
+ weights[:, rotation] = weights[:, rotation].rot90(
172
+ k=rotation, dims=(-2, -1)
173
+ )
174
+ if self.reflection:
175
+ weights[:, self.rotation_group_size :] = weights[
176
+ :, : self.rotation_group_size
177
+ ].flip(dims=(-2,))
178
+
179
+ if self.first_layer:
180
+ weights = weights.reshape(
181
+ self.out_channels * self.group_size,
182
+ self.in_channels,
183
+ self.kernel_size_y,
184
+ self.kernel_size_y,
185
+ )
186
+ bias = (
187
+ self.base_bias.repeat_interleave(self.group_size, dim=1).reshape(-1)
188
+ if self.base_bias is not None
189
+ else None
190
+ )
191
+ else:
192
+ weights = weights.transpose(2, 1).reshape(
193
+ self.out_channels,
194
+ self.in_channels * self.group_size,
195
+ self.kernel_size_y,
196
+ self.kernel_size_y,
197
+ )
198
+ bias = self.base_bias.reshape(-1) if self.base_bias is not None else None
199
+ else:
200
+ weights = weights.repeat(1, self.group_size, 1, 1, 1, 1)
201
+ for rotation in range(1, self.rotation_group_size):
202
+ weights[:, rotation] = weights[:, rotation - 1].rot90(
203
+ dims=(-2, -1)
204
+ )
205
+ if self.reflection:
206
+ weights[:, rotation] = torch.cat(
207
+ (
208
+ weights[
209
+ :, rotation, :, self.rotation_group_size - 1
210
+ ].unsqueeze(2),
211
+ weights[
212
+ :, rotation, :, : self.rotation_group_size - 1
213
+ ],
214
+ weights[
215
+ :, rotation, :, self.rotation_group_size + 1 :
216
+ ],
217
+ weights[
218
+ :, rotation, :, self.rotation_group_size
219
+ ].unsqueeze(2),
220
+ ),
221
+ dim=2,
222
+ )
223
+ else:
224
+ weights[:, rotation] = torch.cat(
225
+ (
226
+ weights[:, rotation, :, -1].unsqueeze(2),
227
+ weights[:, rotation, :, :-1],
228
+ ),
229
+ dim=2,
230
+ )
231
+ if self.reflection:
232
+ weights[:, self.rotation_group_size :] = torch.cat(
233
+ (
234
+ weights[
235
+ :, : self.rotation_group_size, :, self.rotation_group_size :
236
+ ],
237
+ weights[
238
+ :, : self.rotation_group_size, :, : self.rotation_group_size
239
+ ],
240
+ ),
241
+ dim=3,
242
+ ).flip(dims=(-2,))
243
+
244
+ weights = weights.reshape(
245
+ self.out_channels * self.group_size,
246
+ self.in_channels * self.group_size,
247
+ self.kernel_size_y,
248
+ self.kernel_size_y,
249
+ )
250
+ bias = (
251
+ self.base_bias.repeat_interleave(self.group_size, dim=1).reshape(-1)
252
+ if self.base_bias is not None
253
+ else None
254
+ )
255
+
256
+ if self.hermitian:
257
+ weights = weights[..., -self.kernel_size_x :]
258
+ return weights, bias
259
+
260
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
261
+ weights, bias = self.transformed_weight()
262
+ return F.conv2d(x, weights, bias=bias)
263
+
264
+
265
+ class GroupSpectralConv2d(nn.Module):
266
+ def __init__(
267
+ self,
268
+ in_channels: int,
269
+ out_channels: int,
270
+ modes: int,
271
+ reflection: bool,
272
+ ) -> None:
273
+ super().__init__()
274
+ self.modes = (int(modes), int(modes))
275
+ kernel_size = 2 * max(self.modes) - 1
276
+ self.group_conv = GroupEquivariantConv2d(
277
+ in_channels,
278
+ out_channels,
279
+ kernel_size,
280
+ bias=False,
281
+ spectral=True,
282
+ hermitian=True,
283
+ reflection=reflection,
284
+ )
285
+
286
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
287
+ batch_size, _, height, width = x.shape
288
+ modes_y, modes_x = self.modes
289
+ zero_y = height // 2
290
+ start_y = zero_y - modes_y + 1
291
+ stop_y = zero_y + modes_y
292
+ if start_y < 0 or stop_y > height or modes_x > width // 2 + 1:
293
+ raise ValueError(
294
+ f"modes={self.modes} exceed the FFT grid {(height, width)}"
295
+ )
296
+
297
+ weights, _ = self.group_conv.transformed_weight()
298
+ weights = weights.transpose(0, 1)
299
+ x_ft = torch.fft.fftshift(torch.fft.rfft2(x), dim=-2)
300
+ filtered = x_ft[..., start_y:stop_y, :modes_x]
301
+ out_ft = torch.zeros(
302
+ batch_size,
303
+ weights.shape[1],
304
+ height,
305
+ width // 2 + 1,
306
+ dtype=torch.cfloat,
307
+ device=x.device,
308
+ )
309
+ out_ft[..., start_y:stop_y, :modes_x] = torch.einsum(
310
+ "bixy,ioxy->boxy", filtered, weights
311
+ )
312
+ return torch.fft.irfft2(
313
+ torch.fft.ifftshift(out_ft, dim=-2), s=(height, width)
314
+ )
315
+
316
+
317
+ class GroupMLP2d(nn.Module):
318
+ def __init__(
319
+ self,
320
+ in_channels: int,
321
+ out_channels: int,
322
+ mid_channels: int,
323
+ reflection: bool,
324
+ last_layer: bool = False,
325
+ ) -> None:
326
+ super().__init__()
327
+ self.first = GroupEquivariantConv2d(
328
+ in_channels, mid_channels, 1, reflection=reflection
329
+ )
330
+ self.second = GroupEquivariantConv2d(
331
+ mid_channels,
332
+ out_channels,
333
+ 1,
334
+ reflection=reflection,
335
+ last_layer=last_layer,
336
+ )
337
+
338
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
339
+ return self.second(F.gelu(self.first(x)))
340
+
341
+
342
+ class GroupNorm(nn.Module):
343
+ def __init__(self, width: int, group_size: int) -> None:
344
+ super().__init__()
345
+ self.group_size = group_size
346
+ self.norm = nn.InstanceNorm3d(width)
347
+
348
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
349
+ batch_size, _, height, width = x.shape
350
+ x = x.reshape(batch_size, -1, self.group_size, height, width)
351
+ x = self.norm(x)
352
+ return x.reshape(batch_size, -1, height, width)
353
+
354
+
355
+ class GFNO(nn.Module):
356
+ """C4/D4 group-equivariant Fourier neural operator for regular 2D grids."""
357
+
358
+ def __init__(
359
+ self,
360
+ in_dim: int,
361
+ out_dim: int,
362
+ spatial_shape: Sequence[int],
363
+ hidden_dim: int = 64,
364
+ modes: int = 12,
365
+ num_layers: int = 4,
366
+ space_dim: int = 2,
367
+ include_pos: bool = True,
368
+ activation: str = "gelu",
369
+ reflection: bool = False,
370
+ pad_to_multiple: int = 16,
371
+ ) -> None:
372
+ super().__init__()
373
+ self.in_dim = int(in_dim)
374
+ self.out_dim = int(out_dim)
375
+ self.spatial_shape = tuple(int(value) for value in spatial_shape)
376
+ self.hidden_dim = int(hidden_dim)
377
+ self.modes = int(modes)
378
+ self.num_layers = int(num_layers)
379
+ self.space_dim = int(space_dim)
380
+ self.include_pos = bool(include_pos)
381
+ self.reflection = bool(reflection)
382
+ self.pad_to_multiple = int(pad_to_multiple)
383
+ if len(self.spatial_shape) != 2 or self.space_dim != 2:
384
+ raise ValueError("GFNO supports two-dimensional structured grids")
385
+ if min(self.hidden_dim, self.modes, self.num_layers, self.pad_to_multiple) < 1:
386
+ raise ValueError(
387
+ "hidden_dim, modes, num_layers and pad_to_multiple must be positive"
388
+ )
389
+
390
+ feature_dim = self.in_dim + (self.space_dim if self.include_pos else 0)
391
+ self.preprocess = MLP(
392
+ feature_dim, self.hidden_dim * 2, self.hidden_dim, activation
393
+ )
394
+ self.group_size = 4 * (1 + int(self.reflection))
395
+ self.padding = tuple(
396
+ (self.pad_to_multiple - size % self.pad_to_multiple)
397
+ % self.pad_to_multiple
398
+ for size in self.spatial_shape
399
+ )
400
+ augmented_shape = tuple(
401
+ size + padding
402
+ for size, padding in zip(self.spatial_shape, self.padding)
403
+ )
404
+ if self.modes > augmented_shape[0] // 2:
405
+ raise ValueError(
406
+ f"modes={self.modes} exceed padded grid {augmented_shape}"
407
+ )
408
+
409
+ self.lifting = GroupEquivariantConv2d(
410
+ self.hidden_dim,
411
+ self.hidden_dim,
412
+ 1,
413
+ reflection=self.reflection,
414
+ first_layer=True,
415
+ )
416
+ self.spectral_layers = nn.ModuleList(
417
+ GroupSpectralConv2d(
418
+ self.hidden_dim,
419
+ self.hidden_dim,
420
+ self.modes,
421
+ self.reflection,
422
+ )
423
+ for _ in range(self.num_layers)
424
+ )
425
+ self.mlp_layers = nn.ModuleList(
426
+ GroupMLP2d(
427
+ self.hidden_dim,
428
+ self.hidden_dim,
429
+ self.hidden_dim,
430
+ self.reflection,
431
+ )
432
+ for _ in range(self.num_layers)
433
+ )
434
+ self.residual_layers = nn.ModuleList(
435
+ GroupEquivariantConv2d(
436
+ self.hidden_dim,
437
+ self.hidden_dim,
438
+ 1,
439
+ reflection=self.reflection,
440
+ )
441
+ for _ in range(self.num_layers)
442
+ )
443
+ self.norm = GroupNorm(self.hidden_dim, self.group_size)
444
+ self.output = GroupMLP2d(
445
+ self.hidden_dim,
446
+ self.out_dim,
447
+ self.hidden_dim * 4,
448
+ self.reflection,
449
+ last_layer=True,
450
+ )
451
+
452
+ def _pad(self, x: torch.Tensor) -> torch.Tensor:
453
+ pad_height, pad_width = self.padding
454
+ return F.pad(x, (0, pad_width, 0, pad_height)) if any(self.padding) else x
455
+
456
+ def _unpad(self, x: torch.Tensor) -> torch.Tensor:
457
+ height, width = self.spatial_shape
458
+ return x[..., :height, :width]
459
+
460
+ def forward(
461
+ self, pos: torch.Tensor, field: torch.Tensor | None = None
462
+ ) -> torch.Tensor:
463
+ batch_size, point_count, coordinate_dim = pos.shape
464
+ expected_points = self.spatial_shape[0] * self.spatial_shape[1]
465
+ if point_count != expected_points or coordinate_dim != self.space_dim:
466
+ raise ValueError(
467
+ f"Expected pos [B, {expected_points}, {self.space_dim}], "
468
+ f"got {tuple(pos.shape)}"
469
+ )
470
+ if field is None:
471
+ if self.in_dim:
472
+ raise ValueError("field is required when in_dim > 0")
473
+ features = pos if self.include_pos else pos.new_empty(batch_size, point_count, 0)
474
+ else:
475
+ if field.shape != (batch_size, point_count, self.in_dim):
476
+ raise ValueError(
477
+ f"Expected field [B, {point_count}, {self.in_dim}], "
478
+ f"got {tuple(field.shape)}"
479
+ )
480
+ features = torch.cat((pos, field), dim=-1) if self.include_pos else field
481
+
482
+ x = self.preprocess(features)
483
+ x = x.permute(0, 2, 1).reshape(
484
+ batch_size, self.hidden_dim, *self.spatial_shape
485
+ )
486
+ x = self.lifting(self._pad(x))
487
+ for layer_index, (spectral, mlp, residual) in enumerate(
488
+ zip(self.spectral_layers, self.mlp_layers, self.residual_layers)
489
+ ):
490
+ update = self.norm(spectral(self.norm(x)))
491
+ x = mlp(update) + residual(x)
492
+ if layer_index != self.num_layers - 1:
493
+ x = F.gelu(x)
494
+ x = self.output(self._unpad(x))
495
+ return x.reshape(batch_size, self.out_dim, -1).permute(0, 2, 1)
scripts/__pycache__/common.cpython-311.pyc ADDED
Binary file (7.31 kB). View file
 
scripts/common.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from collections.abc import Mapping
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import torch
10
+ import yaml
11
+
12
+
13
+ class AttrDict(dict):
14
+ """Dictionary with recursive attribute access for OneScience datapipes."""
15
+
16
+ def __getattr__(self, key: str) -> Any:
17
+ try:
18
+ return self[key]
19
+ except KeyError as error:
20
+ raise AttributeError(key) from error
21
+
22
+ def __setattr__(self, key: str, value: Any) -> None:
23
+ self[key] = value
24
+
25
+
26
+ def to_attr_dict(value: Any) -> Any:
27
+ if isinstance(value, Mapping):
28
+ return AttrDict({key: to_attr_dict(item) for key, item in value.items()})
29
+ if isinstance(value, list):
30
+ return [to_attr_dict(item) for item in value]
31
+ return value
32
+
33
+
34
+ def to_plain_dict(value: Any) -> Any:
35
+ if isinstance(value, Mapping):
36
+ return {key: to_plain_dict(item) for key, item in value.items()}
37
+ if isinstance(value, list):
38
+ return [to_plain_dict(item) for item in value]
39
+ return value
40
+
41
+
42
+ def load_config(path: Path) -> dict[str, Any]:
43
+ with path.open("r", encoding="utf-8") as stream:
44
+ config = yaml.safe_load(stream)
45
+ if not isinstance(config, dict) or not isinstance(config.get("root"), dict):
46
+ raise ValueError(f"Config must contain a root mapping: {path}")
47
+ return config["root"]
48
+
49
+
50
+ def project_path(value: str | Path, project_root: Path) -> Path:
51
+ path = Path(value).expanduser()
52
+ return path if path.is_absolute() else project_root / path
53
+
54
+
55
+ def resolve_device(requested: str) -> torch.device:
56
+ if requested == "auto":
57
+ return torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
58
+ device = torch.device(requested)
59
+ if device.type == "cuda" and not torch.cuda.is_available():
60
+ raise RuntimeError("CUDA/DCU was requested but torch.cuda.is_available() is false")
61
+ return device
62
+
63
+
64
+ def seed_everything(seed: int) -> None:
65
+ random.seed(seed)
66
+ np.random.seed(seed)
67
+ torch.manual_seed(seed)
68
+ if torch.cuda.is_available():
69
+ torch.cuda.manual_seed_all(seed)
70
+
71
+
72
+ def rollout(
73
+ model: torch.nn.Module,
74
+ pos: torch.Tensor,
75
+ state: torch.Tensor,
76
+ t_out: int,
77
+ out_dim: int,
78
+ teacher_forcing: bool = False,
79
+ target: torch.Tensor | None = None,
80
+ ) -> torch.Tensor:
81
+ predictions = []
82
+ for step in range(t_out):
83
+ prediction = model(pos, state)
84
+ predictions.append(prediction)
85
+ next_frame = (
86
+ target[..., step * out_dim : (step + 1) * out_dim]
87
+ if teacher_forcing and target is not None
88
+ else prediction
89
+ )
90
+ state = torch.cat((state[..., out_dim:], next_frame), dim=-1)
91
+ return torch.cat(predictions, dim=-1)
92
+
93
+
94
+ def relative_l2(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
95
+ prediction = prediction.reshape(prediction.shape[0], -1)
96
+ target = target.reshape(target.shape[0], -1)
97
+ numerator = torch.linalg.vector_norm(prediction - target, dim=1)
98
+ denominator = torch.linalg.vector_norm(target, dim=1).clamp_min(1.0e-8)
99
+ return numerator / denominator
scripts/inference.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import matplotlib
8
+
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ import torch
12
+ from matplotlib.ticker import FormatStrFormatter
13
+
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(PROJECT_ROOT))
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ LOCAL_WORKSPACE = PROJECT_ROOT.parents[1]
19
+ if (LOCAL_WORKSPACE / "onescience" / "src" / "onescience" / "datapipes").is_dir():
20
+ sys.path.insert(0, str(LOCAL_WORKSPACE))
21
+
22
+ from common import ( # noqa: E402
23
+ load_config,
24
+ project_path,
25
+ relative_l2,
26
+ resolve_device,
27
+ rollout,
28
+ to_attr_dict,
29
+ )
30
+ from model.gfno import GFNO # noqa: E402
31
+ from onescience.datapipes.cfd import NavierStokesDatapipe # noqa: E402
32
+ from onescience.distributed.manager import DistributedManager # noqa: E402
33
+
34
+
35
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
36
+
37
+
38
+ def prepare_config(config: dict[str, Any]) -> None:
39
+ datapipe = config["datapipe"]
40
+ datapipe["source"]["data_dir"] = str(
41
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
42
+ )
43
+
44
+
45
+ def validate_checkpoint_config(
46
+ datapipe_config: dict[str, Any],
47
+ checkpoint: dict[str, Any],
48
+ ) -> None:
49
+ checkpoint_data = checkpoint["datapipe_config"]["data"]
50
+ current_data = datapipe_config["data"]
51
+ for key in ("t_in", "t_out", "out_dim", "downsamplex", "downsampley", "normalize"):
52
+ if current_data[key] != checkpoint_data[key]:
53
+ raise ValueError(
54
+ f"Config value datapipe.data.{key}={current_data[key]} does not match "
55
+ f"checkpoint value {checkpoint_data[key]}"
56
+ )
57
+
58
+
59
+ def build_model(model_config: dict[str, Any], spatial_shape: tuple[int, int]) -> GFNO:
60
+ return GFNO(
61
+ in_dim=int(model_config["in_dim"]),
62
+ out_dim=int(model_config["out_dim"]),
63
+ spatial_shape=spatial_shape,
64
+ hidden_dim=int(model_config["hidden_dim"]),
65
+ modes=int(model_config["modes"]),
66
+ num_layers=int(model_config["num_layers"]),
67
+ space_dim=int(model_config["space_dim"]),
68
+ include_pos=bool(model_config["include_pos"]),
69
+ activation=str(model_config["activation"]),
70
+ reflection=bool(model_config["reflection"]),
71
+ pad_to_multiple=int(model_config["pad_to_multiple"]),
72
+ )
73
+
74
+
75
+ def save_visualization(
76
+ prediction: torch.Tensor,
77
+ target: torch.Tensor,
78
+ spatial_shape: tuple[int, int],
79
+ output_path: Path,
80
+ ) -> None:
81
+ prediction = prediction.detach().cpu()
82
+ target = target.detach().cpu()
83
+ height, width = spatial_shape
84
+ frame_count = prediction.shape[-1]
85
+ frames = sorted({0, frame_count // 2, frame_count - 1})
86
+ sample_prediction = prediction[0].reshape(height, width, frame_count)
87
+ sample_target = target[0].reshape(height, width, frame_count)
88
+ rows = (
89
+ ("Target", sample_target),
90
+ ("Prediction", sample_prediction),
91
+ ("Abs Error", (sample_prediction - sample_target).abs()),
92
+ )
93
+ figure, axes = plt.subplots(3, len(frames), figsize=(4 * len(frames), 9), squeeze=False)
94
+ for row_index, (title, values) in enumerate(rows):
95
+ for column_index, frame in enumerate(frames):
96
+ axis = axes[row_index][column_index]
97
+ image = axis.imshow(values[..., frame], origin="lower", cmap="viridis")
98
+ axis.set_title(f"{title} t+{frame + 1}")
99
+ axis.set_xticks([])
100
+ axis.set_yticks([])
101
+ colorbar = figure.colorbar(image, ax=axis, fraction=0.046, pad=0.04)
102
+ colorbar.formatter = FormatStrFormatter("%.3g")
103
+ colorbar.update_ticks()
104
+ figure.tight_layout()
105
+ figure.savefig(output_path, dpi=150)
106
+ plt.close(figure)
107
+
108
+
109
+ def main() -> None:
110
+ config_path = DEFAULT_CONFIG.resolve()
111
+ config = load_config(config_path)
112
+ prepare_config(config)
113
+ training_config = config["training"]
114
+ inference_config = config["inference"]
115
+ datapipe_config = config["datapipe"]
116
+ num_samples = int(inference_config["num_samples"])
117
+ if num_samples < 1:
118
+ raise ValueError("num_samples must be positive")
119
+ checkpoint_path = (
120
+ project_path(training_config["weight_dir"], PROJECT_ROOT).resolve()
121
+ / str(training_config["checkpoint_name"])
122
+ )
123
+ if not checkpoint_path.exists():
124
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
125
+
126
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
127
+ model_config = dict(checkpoint["model_config"])
128
+ validate_checkpoint_config(datapipe_config, checkpoint)
129
+ data_file = (
130
+ Path(datapipe_config["source"]["data_dir"])
131
+ / datapipe_config["source"]["file_name"]
132
+ )
133
+ device = resolve_device(str(config["common"]["device"]))
134
+ result_dir = project_path(
135
+ inference_config["result_dir"], PROJECT_ROOT
136
+ ).resolve()
137
+
138
+ print(f"Checkpoint: {checkpoint_path}")
139
+ print(f"Data: {data_file}")
140
+ print(f"Device: {device}")
141
+ DistributedManager.initialize()
142
+ try:
143
+ datapipe = NavierStokesDatapipe(
144
+ to_attr_dict(datapipe_config),
145
+ distributed=False,
146
+ normalizer_state=checkpoint.get("normalizer"),
147
+ )
148
+ test_loader, _ = datapipe.test_dataloader()
149
+ spatial_shape = tuple(checkpoint["spatial_shape"])
150
+ if tuple(datapipe.spatial_shape) != spatial_shape:
151
+ raise ValueError(
152
+ f"Checkpoint grid {spatial_shape} does not match data grid {datapipe.spatial_shape}"
153
+ )
154
+ model = build_model(model_config, spatial_shape).to(device)
155
+ model.load_state_dict(checkpoint["model_state"])
156
+ model.eval()
157
+
158
+ batch = next(iter(test_loader))
159
+ pos = batch["pos"].to(device)
160
+ state = batch["x"].to(device)
161
+ target = batch["y"].to(device)
162
+ with torch.no_grad():
163
+ prediction = rollout(
164
+ model,
165
+ pos,
166
+ state,
167
+ int(datapipe_config["data"]["t_out"]),
168
+ int(datapipe_config["data"]["out_dim"]),
169
+ )
170
+ prediction = datapipe.decode_solution(prediction)
171
+ target = datapipe.decode_solution(target)
172
+ error = relative_l2(prediction, target).mean().item()
173
+ sample_count = min(num_samples, prediction.shape[0])
174
+ result_dir.mkdir(parents=True, exist_ok=True)
175
+ tensor_path = result_dir / "prediction_sample.pt"
176
+ image_path = result_dir / "prediction_sample.png"
177
+ torch.save(
178
+ {
179
+ "prediction": prediction[:sample_count].cpu(),
180
+ "target": target[:sample_count].cpu(),
181
+ "spatial_shape": spatial_shape,
182
+ "relative_l2": error,
183
+ },
184
+ tensor_path,
185
+ )
186
+ save_visualization(prediction[:1], target[:1], spatial_shape, image_path)
187
+ print(f"Relative L2: {error:.6e}")
188
+ print(f"Saved tensor: {tensor_path}")
189
+ print(f"Saved figure: {image_path}")
190
+ finally:
191
+ DistributedManager.cleanup()
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
scripts/train.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from tqdm import tqdm
11
+
12
+
13
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
14
+ sys.path.insert(0, str(PROJECT_ROOT))
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+ LOCAL_WORKSPACE = PROJECT_ROOT.parents[1]
17
+ if (LOCAL_WORKSPACE / "onescience" / "src" / "onescience" / "datapipes").is_dir():
18
+ sys.path.insert(0, str(LOCAL_WORKSPACE))
19
+
20
+ from common import ( # noqa: E402
21
+ load_config,
22
+ project_path,
23
+ relative_l2,
24
+ resolve_device,
25
+ rollout,
26
+ seed_everything,
27
+ to_attr_dict,
28
+ to_plain_dict,
29
+ )
30
+ from model.gfno import GFNO # noqa: E402
31
+ from onescience.datapipes.cfd import NavierStokesDatapipe # noqa: E402
32
+ from onescience.distributed.manager import DistributedManager # noqa: E402
33
+
34
+
35
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
36
+
37
+
38
+ def prepare_config(config: dict[str, Any]) -> None:
39
+ datapipe = config["datapipe"]
40
+ data = datapipe["data"]
41
+ model = config["model"]
42
+ datapipe["source"]["data_dir"] = str(
43
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
44
+ )
45
+ model["in_dim"] = int(data["t_in"]) * int(data["out_dim"])
46
+ model["out_dim"] = int(data["out_dim"])
47
+
48
+
49
+ def validate_config(config: dict[str, Any]) -> None:
50
+ data = config["datapipe"]["data"]
51
+ loader = config["datapipe"]["dataloader"]
52
+ model = config["model"]
53
+ training = config["training"]
54
+ positive_values = {
55
+ "ntrain": data["ntrain"],
56
+ "ntest": data["ntest"],
57
+ "t_in": data["t_in"],
58
+ "t_out": data["t_out"],
59
+ "downsamplex": data["downsamplex"],
60
+ "downsampley": data["downsampley"],
61
+ "batch_size": loader["batch_size"],
62
+ "hidden_dim": model["hidden_dim"],
63
+ "modes": model["modes"],
64
+ "num_layers": model["num_layers"],
65
+ "epochs": training["epochs"],
66
+ "eval_interval": training["eval_interval"],
67
+ }
68
+ for name, value in positive_values.items():
69
+ if int(value) < 1:
70
+ raise ValueError(f"{name} must be positive, got {value}")
71
+ if int(loader["num_workers"]) < 0:
72
+ raise ValueError("num_workers cannot be negative")
73
+ for name in ("max_train_batches", "max_eval_batches"):
74
+ value = training.get(name)
75
+ if value is not None and int(value) < 1:
76
+ raise ValueError(f"{name} must be positive when set")
77
+
78
+
79
+ def build_model(model_config: dict[str, Any], spatial_shape: tuple[int, int]) -> GFNO:
80
+ return GFNO(
81
+ in_dim=int(model_config["in_dim"]),
82
+ out_dim=int(model_config["out_dim"]),
83
+ spatial_shape=spatial_shape,
84
+ hidden_dim=int(model_config["hidden_dim"]),
85
+ modes=int(model_config["modes"]),
86
+ num_layers=int(model_config["num_layers"]),
87
+ space_dim=int(model_config["space_dim"]),
88
+ include_pos=bool(model_config["include_pos"]),
89
+ activation=str(model_config["activation"]),
90
+ reflection=bool(model_config["reflection"]),
91
+ pad_to_multiple=int(model_config["pad_to_multiple"]),
92
+ )
93
+
94
+
95
+ def evaluate(
96
+ model: torch.nn.Module,
97
+ loader: torch.utils.data.DataLoader,
98
+ device: torch.device,
99
+ t_out: int,
100
+ out_dim: int,
101
+ max_batches: int | None,
102
+ ) -> tuple[float, float]:
103
+ model.eval()
104
+ squared_error = 0.0
105
+ element_count = 0
106
+ relative_error = 0.0
107
+ sample_count = 0
108
+ with torch.no_grad():
109
+ for batch_index, batch in enumerate(tqdm(loader, desc="Evaluating")):
110
+ if max_batches is not None and batch_index >= max_batches:
111
+ break
112
+ pos = batch["pos"].to(device)
113
+ state = batch["x"].to(device)
114
+ target = batch["y"].to(device)
115
+ prediction = rollout(model, pos, state, t_out, out_dim)
116
+ squared_error += F.mse_loss(prediction, target, reduction="sum").item()
117
+ element_count += target.numel()
118
+ relative_error += relative_l2(prediction, target).sum().item()
119
+ sample_count += target.shape[0]
120
+ return (
121
+ squared_error / max(element_count, 1),
122
+ relative_error / max(sample_count, 1),
123
+ )
124
+
125
+
126
+ def main() -> None:
127
+ config_path = DEFAULT_CONFIG.resolve()
128
+ config = load_config(config_path)
129
+ prepare_config(config)
130
+ validate_config(config)
131
+
132
+ common = config["common"]
133
+ data_config = config["datapipe"]
134
+ model_config = config["model"]
135
+ training = config["training"]
136
+ device = resolve_device(str(common["device"]))
137
+ seed_everything(int(common["seed"]))
138
+ weight_dir = project_path(training["weight_dir"], PROJECT_ROOT).resolve()
139
+ checkpoint_path = weight_dir / str(training["checkpoint_name"])
140
+
141
+ print(f"Config: {config_path}")
142
+ print(f"Data: {Path(data_config['source']['data_dir']) / data_config['source']['file_name']}")
143
+ print(f"Device: {device}")
144
+ print(
145
+ f"Samples: train={data_config['data']['ntrain']} test={data_config['data']['ntest']} "
146
+ f"t_in={data_config['data']['t_in']} t_out={data_config['data']['t_out']}"
147
+ )
148
+
149
+ DistributedManager.initialize()
150
+ started = time.time()
151
+ try:
152
+ datapipe = NavierStokesDatapipe(to_attr_dict(data_config), distributed=False)
153
+ train_loader, _ = datapipe.train_dataloader()
154
+ test_loader, _ = datapipe.test_dataloader()
155
+ model = build_model(model_config, datapipe.spatial_shape).to(device)
156
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
157
+ print(f"Spatial shape: {datapipe.spatial_shape}")
158
+ print(f"Parameters: {parameter_count:,}")
159
+
160
+ optimizer = torch.optim.AdamW(
161
+ model.parameters(),
162
+ lr=float(training["lr"]),
163
+ weight_decay=float(training["weight_decay"]),
164
+ )
165
+ scheduler = torch.optim.lr_scheduler.StepLR(
166
+ optimizer,
167
+ step_size=int(training["step_size"]),
168
+ gamma=float(training["gamma"]),
169
+ )
170
+ t_out = int(data_config["data"]["t_out"])
171
+ out_dim = int(data_config["data"]["out_dim"])
172
+ max_train_batches = training.get("max_train_batches")
173
+ max_train_batches = None if max_train_batches is None else int(max_train_batches)
174
+ max_eval_batches = training.get("max_eval_batches")
175
+ max_eval_batches = None if max_eval_batches is None else int(max_eval_batches)
176
+ best_relative_l2 = float("inf")
177
+ stale_evaluations = 0
178
+
179
+ for epoch in range(1, int(training["epochs"]) + 1):
180
+ model.train()
181
+ epoch_loss = 0.0
182
+ batch_count = 0
183
+ iterator = tqdm(train_loader, desc=f"Epoch {epoch}")
184
+ for batch_index, batch in enumerate(iterator):
185
+ if max_train_batches is not None and batch_index >= max_train_batches:
186
+ break
187
+ pos = batch["pos"].to(device)
188
+ state = batch["x"].to(device)
189
+ target = batch["y"].to(device)
190
+ optimizer.zero_grad(set_to_none=True)
191
+ prediction = rollout(
192
+ model,
193
+ pos,
194
+ state,
195
+ t_out,
196
+ out_dim,
197
+ teacher_forcing=bool(training["teacher_forcing"]),
198
+ target=target,
199
+ )
200
+ loss = F.mse_loss(prediction, target)
201
+ if not torch.isfinite(loss):
202
+ raise FloatingPointError(f"Non-finite training loss at epoch {epoch}")
203
+ loss.backward()
204
+ if training.get("max_grad_norm") is not None:
205
+ torch.nn.utils.clip_grad_norm_(
206
+ model.parameters(), float(training["max_grad_norm"])
207
+ )
208
+ optimizer.step()
209
+ epoch_loss += loss.item()
210
+ batch_count += 1
211
+ iterator.set_postfix(loss=f"{loss.item():.3e}")
212
+ scheduler.step()
213
+ if batch_count == 0:
214
+ raise RuntimeError("No training batches were processed")
215
+
216
+ should_evaluate = (
217
+ epoch % int(training["eval_interval"]) == 0
218
+ or epoch == int(training["epochs"])
219
+ )
220
+ if not should_evaluate:
221
+ continue
222
+ validation_mse, validation_relative_l2 = evaluate(
223
+ model,
224
+ test_loader,
225
+ device,
226
+ t_out,
227
+ out_dim,
228
+ max_eval_batches,
229
+ )
230
+ print(
231
+ f"epoch={epoch:4d} train_mse={epoch_loss / batch_count:.6e} "
232
+ f"val_mse={validation_mse:.6e} val_relative_l2={validation_relative_l2:.6e}"
233
+ )
234
+ if validation_relative_l2 < best_relative_l2:
235
+ best_relative_l2 = validation_relative_l2
236
+ stale_evaluations = 0
237
+ weight_dir.mkdir(parents=True, exist_ok=True)
238
+ torch.save(
239
+ {
240
+ "epoch": epoch,
241
+ "model_state": model.state_dict(),
242
+ "model_config": to_plain_dict(model_config),
243
+ "datapipe_config": to_plain_dict(data_config),
244
+ "training_config": to_plain_dict(training),
245
+ "spatial_shape": tuple(datapipe.spatial_shape),
246
+ "normalizer": datapipe.get_normalizer_state(),
247
+ "best_relative_l2": best_relative_l2,
248
+ },
249
+ checkpoint_path,
250
+ )
251
+ print(f"Saved checkpoint: {checkpoint_path}")
252
+ else:
253
+ stale_evaluations += 1
254
+ if stale_evaluations >= int(training["patience"]):
255
+ print("Early stopping triggered")
256
+ break
257
+
258
+ print(f"Elapsed: {time.time() - started:.1f}s")
259
+ finally:
260
+ DistributedManager.cleanup()
261
+
262
+
263
+ if __name__ == "__main__":
264
+ main()
weight/.gitkeep ADDED
File without changes
weight/gfno_navier_stokes.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b1fb0e6c0dbfbd1767a78d2d65594b9a61397a0543554eb87c5758c08db5812
3
+ size 139803378