OneScience commited on
Commit
e2703dc
·
verified ·
1 Parent(s): 09fac6e

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - PDE
8
+ - CFD
9
+ - neural-operator
10
+ - PDEBench
11
+ frameworks: PyTorch
12
+
13
+ datasets:
14
+ - OneScience/pdenneval
15
+ ---
16
+ <p align="center">
17
+ <strong>
18
+ <span style="font-size: 30px;">PDENNEval</span>
19
+ </strong>
20
+ </p>
21
+
22
+ # Model Overview
23
+ PDENNEval is a comprehensive benchmark developed by researchers at Sun Yat-sen University for evaluating neural-network methods for solving partial differential equations. It enables systematic comparison of function-learning and operator-learning approaches across a broad range of PDE tasks.
24
+
25
+ Paper: [PDENNEval: A Comprehensive Evaluation of Neural Network Methods for Solving PDEs](https://www.ijcai.org/proceedings/2024/0573.pdf)
26
+
27
+ # Model Description
28
+ PDENNEval provides a unified framework for evaluating neural-network PDE solvers. It contains 16 datasets generated with high-accuracy conventional scientific computing methods, covering 19 PDE problem classes from fluid dynamics, materials science, finance, electromagnetics, and other domains. The benchmark evaluates the solution accuracy, computational efficiency, and robustness of 12 neural-network methods.
29
+
30
+ ## Use Cases
31
+
32
+ | Use Case | Description |
33
+ | --- | --- |
34
+ | PDE neural-operator validation | Complete a minimal FNO training and inference workflow using PDEBench-style HDF5 data |
35
+ | Neural PDE method development | Extend the model collection in `model/`, which already includes FNO, DeepONet, PINO-FNO, UNO, MPNN, and UNet definitions |
36
+ | ModelScope/OneCode execution | Download the standalone model package, install its dependencies, and run the provided scripts |
37
+ | Rapid pipeline checks | Use `fake_data.py` and `smoke_models.py` to verify the data, models, and scripts |
38
+
39
+ # Usage
40
+
41
+ ## 1. OneCode
42
+
43
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
44
+
45
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
46
+
47
+ ## 2. Manual Setup
48
+
49
+ **Hardware Requirements**
50
+
51
+ - A GPU or DCU is recommended.
52
+ - A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
53
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
54
+
55
+
56
+ ### Download the Model Package
57
+
58
+ ```bash
59
+ modelscope download --model OneScience/PDENNEval --local_dir ./PDENNEval
60
+ cd PDENNEval
61
+ ```
62
+
63
+ ### Set Up the Runtime Environment
64
+
65
+
66
+ **DCU Environment**
67
+
68
+ ```bash
69
+ # Activate DTK and Conda first
70
+ conda create -n onescience311 python=3.11 -y
71
+ conda activate onescience311
72
+ # Installation with uv is also supported
73
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
74
+ ```
75
+
76
+ **GPU Environment**
77
+ ```bash
78
+ # Activate Conda first
79
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
80
+ conda activate onescience311
81
+ # Installation with uv is also supported
82
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
83
+ ```
84
+
85
+ ### Training Data
86
+
87
+ The OneScience community provides the `pdenneval` dataset for training. Download it with the command below and verify that the data path in `conf/config.yaml` is configured correctly:
88
+
89
+ ```bash
90
+ modelscope download --dataset OneScience/pdenneval --local_dir ./data
91
+ ```
92
+
93
+ ### Training
94
+
95
+ ```bash
96
+ python scripts/train.py
97
+ ```
98
+
99
+ ### Model Weights
100
+ This repository will provide weights trained on the `pdenneval` dataset in the `weights/` directory. The weights will be uploaded soon.
101
+
102
+ ### Inference
103
+
104
+ ```bash
105
+ python scripts/inference.py
106
+ ```
107
+
108
+ By default, inference loads `weight/best_model.pt` and writes `.npz` results to `result/output/`.
109
+
110
+ ### Evaluation and Visualization
111
+
112
+ ```bash
113
+ python scripts/result.py
114
+ ```
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
+ - Original PDENNEval paper: [PDENNEval: A Comprehensive Evaluation of Neural Network Methods for Solving PDEs](https://www.ijcai.org/proceedings/2024/0573.pdf).
126
+ - This repository retains the relevant source and attribution notices. Follow all applicable license requirements when using, modifying, or distributing its contents.
conf/config.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fno_config:
2
+ datapipe:
3
+ source:
4
+ data_dir: "./data"
5
+ file_name: "2D_DarcyFlow_beta0.1_Train.hdf5"
6
+ verbose: true
7
+ data:
8
+ single_file: true
9
+ reduced_resolution: 1
10
+ reduced_resolution_t: 1
11
+ reduced_batch: 1
12
+ initial_step: 1
13
+ test_ratio: 0.1
14
+ pde_name: "2D_Darcy_Flow"
15
+ dataloader:
16
+ batch_size: 2
17
+ num_workers: 0
18
+ pin_memory: false
19
+
20
+ model:
21
+ name: "FNO"
22
+ num_channels: 1
23
+ modes: 4
24
+ width: 8
25
+
26
+ training:
27
+ output_dir: "./weight"
28
+ if_training: true
29
+ continue_training: false
30
+ model_path: null
31
+ save_period: 1
32
+ seed: 0
33
+ training_type: "single"
34
+ t_train: 2
35
+ epochs: 1
36
+ optimizer:
37
+ name: "Adam"
38
+ lr: 1.0e-3
39
+ weight_decay: 1.0e-4
40
+ scheduler:
41
+ name: "StepLR"
42
+ step_size: 100
43
+ gamma: 0.5
44
+
45
+ inference:
46
+ checkpoint: "./weight/best_model.pt"
47
+ output_dir: "./result/output"
48
+ max_batches: 2
49
+
50
+ result:
51
+ prediction_dir: "./result/output"
52
+ output_dir: "./result"
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
model/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .fno import FNO1d, FNO2d, FNO3d, FNO_maxwell
2
+
3
+ __all__ = ["FNO1d", "FNO2d", "FNO3d", "FNO_maxwell"]
model/deeponet.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ import torch
3
+ import torch.nn as nn
4
+ from onescience.utils.pdenneval.deeponet_utils import _get_act, _get_initializer
5
+
6
+
7
+ class MLP(nn.Module):
8
+ """Fully-connected neural network."""
9
+
10
+ def __init__(self, layer_sizes, activation, kernel_initializer):
11
+ super().__init__()
12
+ self.activation = _get_act(activation)
13
+ initializer = _get_initializer(kernel_initializer)
14
+ initializer_zero = _get_initializer("zeros")
15
+
16
+ self.linears = torch.nn.ModuleList()
17
+ for i in range(1, len(layer_sizes)):
18
+ self.linears.append(
19
+ torch.nn.Linear(
20
+ layer_sizes[i - 1], layer_sizes[i], dtype=torch.float32
21
+ )
22
+ )
23
+ initializer(self.linears[-1].weight)
24
+ initializer_zero(self.linears[-1].bias)
25
+
26
+ def forward(self, inputs):
27
+ x = inputs
28
+ for linear in self.linears[:-1]:
29
+ x = self.activation(linear(x))
30
+ x = self.linears[-1](x)
31
+ return x
32
+
33
+ class Modified_MLP(nn.Module):
34
+ def __init__(self, layer_sizes, activation, kernel_initializer) -> None:
35
+ super().__init__()
36
+ self.activation = _get_act(activation)
37
+ initializer = _get_initializer(kernel_initializer)
38
+ initializer_zero = _get_initializer("zeros")
39
+ self.linears = torch.nn.ModuleList()
40
+ for i in range(1, len(layer_sizes)):
41
+ self.linears.append(
42
+ torch.nn.Linear(
43
+ layer_sizes[i - 1], layer_sizes[i], dtype=torch.float32
44
+ )
45
+ )
46
+ initializer(self.linears[-1].weight)
47
+ initializer_zero(self.linears[-1].bias)
48
+ self.linear1=torch.nn.Linear(layer_sizes[0], layer_sizes[1], dtype=torch.float32)
49
+ self.linear2=torch.nn.Linear(layer_sizes[0], layer_sizes[1], dtype=torch.float32)
50
+ initializer(self.linear1.weight),initializer(self.linear2.weight)
51
+ initializer_zero(self.linear1.bias),initializer_zero(self.linear2.bias)
52
+ def forward(self, inputs):
53
+ U = self.activation(self.linear1(inputs))
54
+ V = self.activation(self.linear2(inputs))
55
+ for linear in self.linears[:-1]:
56
+ outputs=torch.sigmoid(linear(inputs))
57
+ inputs= outputs*U + (1-outputs)* V
58
+ outputs = self.linears[-1](inputs)
59
+ return outputs
60
+
61
+
62
+ class DeepONet(nn.Module):
63
+ """Deep operator network.
64
+
65
+ Args:
66
+ layer_sizes_branch: A list of integers as the width of a fully connected network,
67
+ or `(dim, f)` where `dim` is the input dimension and `f` is a network
68
+ function. The width of the last layer in the branch and trunk net should be
69
+ equal.
70
+ layer_sizes_trunk (list): A list of integers as the width of a fully connected
71
+ network.
72
+ activation: If `activation` is a ``string``, then the same activation is used in
73
+ both trunk and branch nets. If `activation` is a ``dict``, then the trunk
74
+ net uses the activation `activation["trunk"]`, and the branch net uses
75
+ `activation["branch"]`.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ layer_sizes_branch,
81
+ layer_sizes_trunk,
82
+ activation,
83
+ kernel_initializer,
84
+ ):
85
+ super().__init__()
86
+ if isinstance(activation, dict):
87
+ activation_branch = _get_act(activation["branch"])
88
+ self.activation_trunk = _get_act(activation["trunk"])
89
+ else:
90
+ activation_branch = self.activation_trunk = _get_act(activation)
91
+ if callable(layer_sizes_branch[0]):
92
+ # User-defined network
93
+ self.branch = layer_sizes_branch[0]
94
+ else:
95
+ # Fully connected network
96
+ self.branch = MLP(layer_sizes_branch, activation_branch, kernel_initializer)
97
+ self.trunk = MLP(layer_sizes_trunk, self.activation_trunk, kernel_initializer)
98
+ self.b = torch.nn.parameter.Parameter(torch.tensor(0.0))
99
+
100
+ def forward(self, inputs):
101
+ x_func = inputs[0]
102
+ x_loc = inputs[1]
103
+ # Branch net to encode the input function
104
+ x_func = self.branch(x_func)
105
+ # Trunk net to encode the domain of the output function
106
+ x_loc = self.activation_trunk(self.trunk(x_loc))
107
+ # Dot product
108
+ if x_func.shape[-1] != x_loc.shape[-1]:
109
+ raise AssertionError(
110
+ "Output sizes of branch net and trunk net do not match."
111
+ )
112
+ x = torch.einsum("bi,bi->b", x_func, x_loc)
113
+ x = torch.unsqueeze(x, 1)
114
+ # Add bias
115
+ x += self.b
116
+ return x
117
+
118
+ class DeepONetCartesianProd(nn.Module):
119
+ """Deep operator network for dataset in the format of Cartesian product.
120
+
121
+ Args:
122
+ layer_sizes_branch: A list of integers as the width of a fully connected network,
123
+ or `(dim, f)` where `dim` is the input dimension and `f` is a network
124
+ function. The width of the last layer in the branch and trunk net should be
125
+ equal.
126
+ layer_sizes_trunk (list): A list of integers as the width of a fully connected
127
+ network.
128
+ activation: If `activation` is a ``string``, then the same activation is used in
129
+ both trunk and branch nets. If `activation` is a ``dict``, then the trunk
130
+ net uses the activation `activation["trunk"]`, and the branch net uses
131
+ `activation["branch"]`.
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ layer_sizes_branch,
137
+ layer_sizes_trunk,
138
+ activation,
139
+ kernel_initializer,
140
+ base_model = "MLP" # or Modified_MLP
141
+ ):
142
+ super().__init__()
143
+ if isinstance(activation, dict):
144
+ activation_branch = _get_act(activation["branch"])
145
+ self.activation_trunk = _get_act(activation["trunk"])
146
+ else:
147
+ activation_branch = self.activation_trunk = _get_act(activation)
148
+ base_model= MLP if base_model=="MLP" else Modified_MLP
149
+ if callable(layer_sizes_branch[0]):
150
+ # User-defined network
151
+ self.branch = layer_sizes_branch[0]
152
+ else:
153
+ self.branch = base_model(layer_sizes_branch, activation_branch, kernel_initializer)
154
+ self.trunk = base_model(layer_sizes_trunk, self.activation_trunk, kernel_initializer)
155
+ self.b = torch.nn.parameter.Parameter(torch.tensor(0.0))
156
+
157
+ def forward(self, inputs):
158
+ x_func = inputs[0]
159
+ x_loc = inputs[1]
160
+ # Branch net to encode the input function
161
+ x_func = self.branch(x_func)
162
+ # Trunk net to encode the domain of the output function
163
+ x_loc = self.activation_trunk(self.trunk(x_loc))
164
+ # Dot product
165
+ if x_func.shape[-1] != x_loc.shape[-1]:
166
+ raise AssertionError(
167
+ "Output sizes of branch net and trunk net do not match."
168
+ )
169
+ x = torch.einsum("bi,ni->bn", x_func, x_loc)
170
+ # Add bias
171
+ x += self.b
172
+ return x
173
+
174
+ class DeepONetCartesianProd2D(DeepONetCartesianProd):
175
+ # For multiple outputs, we choose the second approach mentioned in "https://arxiv.org/abs/2111.05512", i.e. split
176
+ # the output of both the branch and the trunk into n groups, and the k-th groups outputs the k-th solution.
177
+ def __init__(self,
178
+ size: int,
179
+ in_channel_branch: int,
180
+ query_dim: int ,
181
+ out_channel: int,
182
+ activation: str = "relu",
183
+ kernel_initializer: str = "Glorot normal",
184
+ base_model = "MLP"):
185
+ layer_sizes_branch = [in_channel_branch*size**2]+[128]*4+[128*out_channel]
186
+ layer_sizes_trunk= [query_dim]+[128]*4+[128*out_channel]
187
+ super().__init__(layer_sizes_branch,layer_sizes_trunk,activation,kernel_initializer,base_model)
188
+ self.out_channel = out_channel
189
+ self.query_dim=query_dim
190
+ self.b = torch.nn.parameter.Parameter(torch.zeros(out_channel,dtype=torch.float32))
191
+
192
+ def forward(self, inputs):
193
+ x_func = inputs[0]
194
+ x_loc = inputs[1]
195
+ batchsize=x_func.shape[0]
196
+ x_func = x_func.reshape([batchsize,-1])
197
+ grid_shape = x_loc.shape[:-1]
198
+ x_loc = x_loc.reshape([-1,self.query_dim]) #(num_point, query_dim)
199
+ num_points=x_loc.shape[0]
200
+ # Branch net to encode the input function
201
+ x_func = self.branch(x_func.reshape([batchsize,-1]))
202
+ # Trunk net to encode the domain of the output function
203
+ x_loc = self.activation_trunk(self.trunk(x_loc))
204
+ # Dot product
205
+ if x_func.shape[-1] != x_loc.shape[-1]:
206
+ raise AssertionError(
207
+ "Output sizes of branch net and trunk net do not match."
208
+ )
209
+ x_func = x_func.reshape([batchsize,self.out_channel,-1])
210
+ x_loc = x_loc.reshape([num_points,self.out_channel,-1])
211
+ x = torch.einsum("bci,nci->bnc", x_func, x_loc)
212
+ # Add bias
213
+ x += self.b
214
+ return x.reshape([-1,*grid_shape,self.out_channel])
215
+
216
+ class DeepONetCartesianProd1D(DeepONetCartesianProd):
217
+ # For multiple outputs, we choose the second approach mentioned in "https://arxiv.org/abs/2111.05512", i.e. split
218
+ # the output of both the branch and the trunk into n groups, and the k-th groups outputs the k-th solution.
219
+ def __init__(self,
220
+ size :int,
221
+ in_channel_branch: int,
222
+ query_dim: int ,
223
+ out_channel: int,
224
+ activation: str = "relu",
225
+ kernel_initializer: str = "Glorot normal",
226
+ base_model="MLP"):
227
+ layer_sizes_branch= [in_channel_branch*size]+[128]*4+[128*out_channel]
228
+ layer_sizes_trunk= [query_dim]+[128]*3+[128*out_channel]
229
+ super().__init__(layer_sizes_branch,layer_sizes_trunk,activation,kernel_initializer,base_model)
230
+ self.out_channel = out_channel
231
+ self.b = torch.nn.parameter.Parameter(torch.zeros(out_channel,dtype=torch.float32))
232
+ self.query_dim=query_dim
233
+
234
+ def forward(self, inputs):
235
+ x_func = inputs[0]
236
+ x_loc = inputs[1]
237
+ grid_shape = x_loc.shape[:-1]
238
+ x_loc = x_loc.reshape([-1,self.query_dim]) #(num_point, query_dim)
239
+ num_points=x_loc.shape[0]
240
+ batchsize=x_func.shape[0]
241
+ x_func = x_func.reshape([batchsize,-1])
242
+ # Branch net to encode the input function
243
+
244
+ x_func = self.branch(x_func)
245
+ # Trunk net to encode the domain of the output function
246
+ x_loc = self.activation_trunk(self.trunk(x_loc))
247
+ # Dot product
248
+ if x_func.shape[-1] != x_loc.shape[-1]:
249
+ raise AssertionError(
250
+ "Output sizes of branch net and trunk net do not match."
251
+ )
252
+ x_func = x_func.reshape([batchsize,self.out_channel,-1])
253
+ x_loc = x_loc.reshape([num_points,self.out_channel,-1])
254
+ x = torch.einsum("bci,nci->bnc", x_func, x_loc)
255
+ # Add bias
256
+ x += self.b
257
+ return x.reshape([-1,*grid_shape,self.out_channel])
model/fno.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import numpy as np
4
+ import torch.nn.functional as F
5
+
6
+
7
+ class SpectralConv1d(nn.Module):
8
+ def __init__(self, in_channels, out_channels, modes1):
9
+ super(SpectralConv1d, self).__init__()
10
+
11
+ """
12
+ 1D Fourier layer. It does FFT, linear transform, and Inverse FFT.
13
+ """
14
+
15
+ self.in_channels = in_channels
16
+ self.out_channels = out_channels
17
+ self.modes1 = modes1 #Number of Fourier modes to multiply, at most floor(N/2) + 1
18
+
19
+ self.scale = (1 / (in_channels*out_channels))
20
+ self.weights1 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, dtype=torch.cfloat))
21
+
22
+ # Complex multiplication
23
+ def compl_mul1d(self, input, weights):
24
+ # (batch, in_channel, x ), (in_channel, out_channel, x) -> (batch, out_channel, x)
25
+ return torch.einsum("bix,iox->box", input, weights)
26
+
27
+ def forward(self, x):
28
+ batchsize = x.shape[0]
29
+ #Compute Fourier coeffcients up to factor of e^(- something constant)
30
+ x_ft = torch.fft.rfft(x)
31
+
32
+ # Multiply relevant Fourier modes
33
+ out_ft = torch.zeros(batchsize, self.out_channels, x.size(-1)//2 + 1, device=x.device, dtype=torch.cfloat)
34
+ out_ft[:, :, :self.modes1] = self.compl_mul1d(x_ft[:, :, :self.modes1], self.weights1)
35
+
36
+ #Return to physical space
37
+ x = torch.fft.irfft(out_ft, n=x.size(-1))
38
+ return x
39
+
40
+ class FNO1d(nn.Module):
41
+ def __init__(self, num_channels, modes=16, width=64, initial_step=10):
42
+ super(FNO1d, self).__init__()
43
+
44
+ """
45
+ The overall network. It contains 4 layers of the Fourier layer.
46
+ 1. Lift the input to the desire channel dimension by self.fc0 .
47
+ 2. 4 layers of the integral operators u' = (W + K)(u).
48
+ W defined by self.w; K defined by self.conv .
49
+ 3. Project from the channel space to the output space by self.fc1 and self.fc2 .
50
+
51
+ input: the solution of the initial condition and location (a(x), x)
52
+ input shape: (batchsize, x=s, c=2)
53
+ output: the solution of a later timestep
54
+ output shape: (batchsize, x=s, c=1)
55
+ """
56
+
57
+ self.modes1 = modes
58
+ self.width = width
59
+ self.padding = 2 # pad the domain if input is non-periodic
60
+ self.fc0 = nn.Linear(initial_step*num_channels+1, self.width) # input channel is 2: (a(x), x)
61
+
62
+ self.conv0 = SpectralConv1d(self.width, self.width, self.modes1)
63
+ self.conv1 = SpectralConv1d(self.width, self.width, self.modes1)
64
+ self.conv2 = SpectralConv1d(self.width, self.width, self.modes1)
65
+ self.conv3 = SpectralConv1d(self.width, self.width, self.modes1)
66
+ self.w0 = nn.Conv1d(self.width, self.width, 1)
67
+ self.w1 = nn.Conv1d(self.width, self.width, 1)
68
+ self.w2 = nn.Conv1d(self.width, self.width, 1)
69
+ self.w3 = nn.Conv1d(self.width, self.width, 1)
70
+
71
+ self.fc1 = nn.Linear(self.width, 128)
72
+ self.fc2 = nn.Linear(128, num_channels)
73
+
74
+ def forward(self, x, grid):
75
+ # x dim = [b, x1, t*v]
76
+ x = torch.cat((x, grid), dim=-1)
77
+ x = self.fc0(x)
78
+ x = x.permute(0, 2, 1)
79
+
80
+ x = F.pad(x, [0, self.padding]) # pad the domain if input is non-periodic
81
+
82
+ x1 = self.conv0(x)
83
+ x2 = self.w0(x)
84
+ x = x1 + x2
85
+ x = F.gelu(x)
86
+
87
+ x1 = self.conv1(x)
88
+ x2 = self.w1(x)
89
+ x = x1 + x2
90
+ x = F.gelu(x)
91
+
92
+ x1 = self.conv2(x)
93
+ x2 = self.w2(x)
94
+ x = x1 + x2
95
+ x = F.gelu(x)
96
+
97
+ x1 = self.conv3(x)
98
+ x2 = self.w3(x)
99
+ x = x1 + x2
100
+
101
+ x = x[..., :-self.padding]
102
+ x = x.permute(0, 2, 1)
103
+ x = self.fc1(x)
104
+ x = F.gelu(x)
105
+ x = self.fc2(x)
106
+ return x.unsqueeze(-2)
107
+
108
+
109
+ class SpectralConv2d_fast(nn.Module):
110
+ def __init__(self, in_channels, out_channels, modes1, modes2):
111
+ super(SpectralConv2d_fast, self).__init__()
112
+
113
+ """
114
+ 2D Fourier layer. It does FFT, linear transform, and Inverse FFT.
115
+ """
116
+
117
+ self.in_channels = in_channels
118
+ self.out_channels = out_channels
119
+ self.modes1 = modes1 #Number of Fourier modes to multiply, at most floor(N/2) + 1
120
+ self.modes2 = modes2
121
+
122
+ self.scale = (1 / (in_channels * out_channels))
123
+ self.weights1 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, dtype=torch.cfloat))
124
+ self.weights2 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, dtype=torch.cfloat))
125
+
126
+ # Complex multiplication
127
+ def compl_mul2d(self, input, weights):
128
+ # (batch, in_channel, x,y ), (in_channel, out_channel, x,y) -> (batch, out_channel, x,y)
129
+ return torch.einsum("bixy,ioxy->boxy", input, weights)
130
+
131
+ def forward(self, x):
132
+ batchsize = x.shape[0]
133
+ #Compute Fourier coeffcients up to factor of e^(- something constant)
134
+ x_ft = torch.fft.rfft2(x)
135
+
136
+ # Multiply relevant Fourier modes
137
+ out_ft = torch.zeros(batchsize, self.out_channels, x.size(-2), x.size(-1)//2 + 1, dtype=torch.cfloat, device=x.device)
138
+ out_ft[:, :, :self.modes1, :self.modes2] = \
139
+ self.compl_mul2d(x_ft[:, :, :self.modes1, :self.modes2], self.weights1)
140
+ out_ft[:, :, -self.modes1:, :self.modes2] = \
141
+ self.compl_mul2d(x_ft[:, :, -self.modes1:, :self.modes2], self.weights2)
142
+
143
+ #Return to physical space
144
+ x = torch.fft.irfft2(out_ft, s=(x.size(-2), x.size(-1)))
145
+ return x
146
+
147
+ class FNO2d(nn.Module):
148
+ def __init__(self, num_channels, modes1=12, modes2=12, width=20, initial_step=10):
149
+ super(FNO2d, self).__init__()
150
+
151
+ """
152
+ The overall network. It contains 4 layers of the Fourier layer.
153
+ 1. Lift the input to the desire channel dimension by self.fc0 .
154
+ 2. 4 layers of the integral operators u' = (W + K)(u).
155
+ W defined by self.w; K defined by self.conv .
156
+ 3. Project from the channel space to the output space by self.fc1 and self.fc2 .
157
+
158
+ input: the solution of the previous 10 timesteps + 2 locations (u(t-10, x, y), ..., u(t-1, x, y), x, y)
159
+ input shape: (batchsize, x, y, c)
160
+ output: the solution of the next timestep
161
+ output shape: (batchsize, x, y, c)
162
+ """
163
+
164
+ self.modes1 = modes1
165
+ self.modes2 = modes2
166
+ self.width = width
167
+ self.padding = 2 # pad the domain if input is non-periodic
168
+ self.fc0 = nn.Linear(initial_step*num_channels+2, self.width)
169
+ # input channel is 12: the solution of the previous 10 timesteps + 2 locations (u(t-10, x, y), ..., u(t-1, x, y), x, y)
170
+
171
+ self.conv0 = SpectralConv2d_fast(self.width, self.width, self.modes1, self.modes2)
172
+ self.conv1 = SpectralConv2d_fast(self.width, self.width, self.modes1, self.modes2)
173
+ self.conv2 = SpectralConv2d_fast(self.width, self.width, self.modes1, self.modes2)
174
+ self.conv3 = SpectralConv2d_fast(self.width, self.width, self.modes1, self.modes2)
175
+ self.w0 = nn.Conv2d(self.width, self.width, 1)
176
+ self.w1 = nn.Conv2d(self.width, self.width, 1)
177
+ self.w2 = nn.Conv2d(self.width, self.width, 1)
178
+ self.w3 = nn.Conv2d(self.width, self.width, 1)
179
+
180
+ self.fc1 = nn.Linear(self.width, 128)
181
+ self.fc2 = nn.Linear(128, num_channels)
182
+
183
+ def forward(self, x, grid):
184
+ # x dim = [b, x1, x2, t*v]
185
+ x = torch.cat((x, grid), dim=-1)
186
+ x = self.fc0(x)
187
+ x = x.permute(0, 3, 1, 2)
188
+
189
+ # Pad tensor with boundary condition
190
+ x = F.pad(x, [0, self.padding, 0, self.padding])
191
+
192
+ x1 = self.conv0(x)
193
+ x2 = self.w0(x)
194
+ x = x1 + x2
195
+ x = F.gelu(x)
196
+
197
+ x1 = self.conv1(x)
198
+ x2 = self.w1(x)
199
+ x = x1 + x2
200
+ x = F.gelu(x)
201
+
202
+ x1 = self.conv2(x)
203
+ x2 = self.w2(x)
204
+ x = x1 + x2
205
+ x = F.gelu(x)
206
+
207
+ x1 = self.conv3(x)
208
+ x2 = self.w3(x)
209
+ x = x1 + x2
210
+
211
+ x = x[..., :-self.padding, :-self.padding] # Unpad the tensor
212
+ x = x.permute(0, 2, 3, 1)
213
+ x = self.fc1(x)
214
+ x = F.gelu(x)
215
+ x = self.fc2(x)
216
+
217
+ return x.unsqueeze(-2)
218
+
219
+
220
+ class SpectralConv3d(nn.Module):
221
+ def __init__(self, in_channels, out_channels, modes1, modes2, modes3):
222
+ super(SpectralConv3d, self).__init__()
223
+
224
+ """
225
+ 3D Fourier layer. It does FFT, linear transform, and Inverse FFT.
226
+ """
227
+
228
+ self.in_channels = in_channels
229
+ self.out_channels = out_channels
230
+ self.modes1 = modes1 #Number of Fourier modes to multiply, at most floor(N/2) + 1
231
+ self.modes2 = modes2
232
+ self.modes3 = modes3
233
+
234
+ self.scale = (1 / (in_channels * out_channels))
235
+ self.weights1 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
236
+ self.weights2 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
237
+ self.weights3 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
238
+ self.weights4 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
239
+
240
+ # Complex multiplication
241
+ def compl_mul3d(self, input, weights):
242
+ # (batch, in_channel, x,y,t ), (in_channel, out_channel, x,y,t) -> (batch, out_channel, x,y,t)
243
+ return torch.einsum("bixyz,ioxyz->boxyz", input, weights)
244
+
245
+ def forward(self, x):
246
+ batchsize = x.shape[0]
247
+ #Compute Fourier coeffcients up to factor of e^(- something constant)
248
+ x_ft = torch.fft.rfftn(x, dim=[-3,-2,-1])
249
+
250
+ # Multiply relevant Fourier modes
251
+ out_ft = torch.zeros(batchsize, self.out_channels, x.size(-3), x.size(-2), x.size(-1)//2 + 1, dtype=torch.cfloat, device=x.device)
252
+ out_ft[:, :, :self.modes1, :self.modes2, :self.modes3] = \
253
+ self.compl_mul3d(x_ft[:, :, :self.modes1, :self.modes2, :self.modes3], self.weights1)
254
+ out_ft[:, :, -self.modes1:, :self.modes2, :self.modes3] = \
255
+ self.compl_mul3d(x_ft[:, :, -self.modes1:, :self.modes2, :self.modes3], self.weights2)
256
+ out_ft[:, :, :self.modes1, -self.modes2:, :self.modes3] = \
257
+ self.compl_mul3d(x_ft[:, :, :self.modes1, -self.modes2:, :self.modes3], self.weights3)
258
+ out_ft[:, :, -self.modes1:, -self.modes2:, :self.modes3] = \
259
+ self.compl_mul3d(x_ft[:, :, -self.modes1:, -self.modes2:, :self.modes3], self.weights4)
260
+
261
+ #Return to physical space
262
+ x = torch.fft.irfftn(out_ft, s=(x.size(-3), x.size(-2), x.size(-1)))
263
+ return x
264
+
265
+ class FNO3d(nn.Module):
266
+ def __init__(self, num_channels, modes1=8, modes2=8, modes3=8, width=20, initial_step=10):
267
+ super(FNO3d, self).__init__()
268
+
269
+ """
270
+ The overall network. It contains 4 layers of the Fourier layer.
271
+ 1. Lift the input to the desire channel dimension by self.fc0 .
272
+ 2. 4 layers of the integral operators u' = (W + K)(u).
273
+ W defined by self.w; K defined by self.conv .
274
+ 3. Project from the channel space to the output space by self.fc1 and self.fc2 .
275
+
276
+ input: the solution of the first 10 timesteps + 3 locations (u(1, x, y), ..., u(10, x, y), x, y, t). It's a constant function in time, except for the last index.
277
+ input shape: (batchsize, x=64, y=64, t=40, c=13)
278
+ output: the solution of the next 40 timesteps
279
+ output shape: (batchsize, x=64, y=64, t=40, c=1)
280
+ """
281
+
282
+ self.modes1 = modes1
283
+ self.modes2 = modes2
284
+ self.modes3 = modes3
285
+ self.width = width
286
+ self.padding = 6 # pad the domain if input is non-periodic
287
+ self.fc0 = nn.Linear(initial_step*num_channels+3, self.width)
288
+ # input channel is 12: the solution of the first 10 timesteps + 3 locations (u(1, x, y), ..., u(10, x, y), x, y, t)
289
+
290
+ self.conv0 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
291
+ self.conv1 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
292
+ self.conv2 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
293
+ self.conv3 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
294
+ self.w0 = nn.Conv3d(self.width, self.width, 1)
295
+ self.w1 = nn.Conv3d(self.width, self.width, 1)
296
+ self.w2 = nn.Conv3d(self.width, self.width, 1)
297
+ self.w3 = nn.Conv3d(self.width, self.width, 1)
298
+ self.bn0 = torch.nn.BatchNorm3d(self.width)
299
+ self.bn1 = torch.nn.BatchNorm3d(self.width)
300
+ self.bn2 = torch.nn.BatchNorm3d(self.width)
301
+ self.bn3 = torch.nn.BatchNorm3d(self.width)
302
+
303
+ self.fc1 = nn.Linear(self.width, 128)
304
+ self.fc2 = nn.Linear(128, num_channels)
305
+
306
+ def forward(self, x, grid):
307
+ # x dim = [b, x1, x2, x3, t*v]
308
+ x = torch.cat((x, grid), dim=-1)
309
+ x = self.fc0(x)
310
+ x = x.permute(0, 4, 1, 2, 3)
311
+
312
+ x = F.pad(x, [0, self.padding]) # pad the domain if input is non-periodic
313
+
314
+ x1 = self.conv0(x)
315
+ x2 = self.w0(x)
316
+ x = x1 + x2
317
+ x = F.gelu(x)
318
+
319
+ x1 = self.conv1(x)
320
+ x2 = self.w1(x)
321
+ x = x1 + x2
322
+ x = F.gelu(x)
323
+
324
+ x1 = self.conv2(x)
325
+ x2 = self.w2(x)
326
+ x = x1 + x2
327
+ x = F.gelu(x)
328
+
329
+ x1 = self.conv3(x)
330
+ x2 = self.w3(x)
331
+ x = x1 + x2
332
+
333
+ x = x[..., :-self.padding]
334
+ x = x.permute(0, 2, 3, 4, 1) # pad the domain if input is non-periodic
335
+ x = self.fc1(x)
336
+ x = F.gelu(x)
337
+ x = self.fc2(x)
338
+ return x.unsqueeze(-2)
339
+
340
+ class FNO_maxwell(nn.Module):
341
+ def __init__(self, num_channels, modes1=8, modes2=8, modes3=8, width=20, initial_step=10):
342
+ super(FNO_maxwell, self).__init__()
343
+
344
+ """
345
+ The overall network. It contains 4 layers of the Fourier layer.
346
+ 1. Lift the input to the desire channel dimension by self.fc0 .
347
+ 2. 4 layers of the integral operators u' = (W + K)(u).
348
+ W defined by self.w; K defined by self.conv .
349
+ 3. Project from the channel space to the output space by self.fc1 and self.fc2 .
350
+
351
+ input: the solution of the first 10 timesteps + 3 locations (u(1, x, y), ..., u(10, x, y), x, y, t). It's a constant function in time, except for the last index.
352
+ input shape: (batchsize, x=64, y=64, t=40, c=13)
353
+ output: the solution of the next 40 timesteps
354
+ output shape: (batchsize, x=64, y=64, t=40, c=1)
355
+ """
356
+
357
+ self.modes1 = modes1
358
+ self.modes2 = modes2
359
+ self.modes3 = modes3
360
+ self.width = width
361
+ self.padding = 6 # pad the domain if input is non-periodic
362
+ self.fc0 = nn.Linear(initial_step*num_channels, self.width)
363
+ # input channel is 12: the solution of the first 10 timesteps + 3 locations (u(1, x, y), ..., u(10, x, y), x, y, t)
364
+
365
+ self.conv0 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
366
+ self.conv1 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
367
+ self.conv2 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
368
+ self.conv3 = SpectralConv3d(self.width, self.width, self.modes1, self.modes2, self.modes3)
369
+ self.w0 = nn.Conv3d(self.width, self.width, 1)
370
+ self.w1 = nn.Conv3d(self.width, self.width, 1)
371
+ self.w2 = nn.Conv3d(self.width, self.width, 1)
372
+ self.w3 = nn.Conv3d(self.width, self.width, 1)
373
+ self.bn0 = torch.nn.BatchNorm3d(self.width)
374
+ self.bn1 = torch.nn.BatchNorm3d(self.width)
375
+ self.bn2 = torch.nn.BatchNorm3d(self.width)
376
+ self.bn3 = torch.nn.BatchNorm3d(self.width)
377
+
378
+ self.fc1 = nn.Linear(self.width, 128)
379
+ self.fc2 = nn.Linear(128, num_channels)
380
+
381
+ def forward(self, x, grid):
382
+ # x dim = [b, x1, x2, x3, t*v]
383
+ x = self.fc0(x)
384
+ x = x.permute(0, 4, 1, 2, 3)
385
+
386
+ x = F.pad(x, [0, self.padding]) # pad the domain if input is non-periodic
387
+
388
+ x1 = self.conv0(x)
389
+ x2 = self.w0(x)
390
+ x = x1 + x2
391
+ x = F.gelu(x)
392
+
393
+ x1 = self.conv1(x)
394
+ x2 = self.w1(x)
395
+ x = x1 + x2
396
+ x = F.gelu(x)
397
+
398
+ x1 = self.conv2(x)
399
+ x2 = self.w2(x)
400
+ x = x1 + x2
401
+ x = F.gelu(x)
402
+
403
+ x1 = self.conv3(x)
404
+ x2 = self.w3(x)
405
+ x = x1 + x2
406
+
407
+ x = x[..., :-self.padding]
408
+ x = x.permute(0, 2, 3, 4, 1) # pad the domain if input is non-periodic
409
+ x = self.fc1(x)
410
+ x = F.gelu(x)
411
+ x = self.fc2(x)
412
+ return x.unsqueeze(-2)
model/mpnn.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import torch
7
+ from torch import nn
8
+ from torch_geometric.data import Data
9
+ from torch_geometric.nn import MessagePassing, InstanceNorm
10
+
11
+ if TYPE_CHECKING:
12
+ from onescience.utils.pdenneval.mpnn_utils import PDE
13
+
14
+ class Swish(nn.Module):
15
+ """Swish activation function
16
+ """
17
+ def __init__(self, beta=1):
18
+ super(Swish, self).__init__()
19
+ self.beta = beta
20
+
21
+ def forward(self, x):
22
+ return x * torch.sigmoid(self.beta*x)
23
+
24
+
25
+ class GNN_Layer(MessagePassing):
26
+ """Message passing layer
27
+ """
28
+ def __init__(self,
29
+ in_features: int,
30
+ out_features: int,
31
+ hidden_features: int,
32
+ time_window: int,
33
+ spatial_dim: int,
34
+ n_variables: int):
35
+ """Initialize message passing layers
36
+ Args:
37
+ in_features (int): number of node input features
38
+ out_features (int): number of node output features
39
+ hidden_features (int): number of hidden features
40
+ time_window (int): number of input/output timesteps (temporal bundling)
41
+ spatial_dim (int): number of dimension of spatial domain
42
+ n_variables (int): number of equation specific parameters used in the solver
43
+ """
44
+ super(GNN_Layer, self).__init__(node_dim=-2, aggr='mean') # node_dim: The axis along which to propagate. (default: -2)
45
+ self.in_features = in_features
46
+ self.out_features = out_features
47
+ self.hidden_features = hidden_features
48
+ assert (spatial_dim == 1 or spatial_dim == 2 or spatial_dim == 3)
49
+
50
+ self.message_net_1 = nn.Sequential(nn.Linear(2 * in_features + time_window + spatial_dim + n_variables, hidden_features),
51
+ Swish()
52
+ )
53
+ self.message_net_2 = nn.Sequential(nn.Linear(hidden_features, hidden_features),
54
+ Swish()
55
+ )
56
+ self.update_net_1 = nn.Sequential(nn.Linear(in_features + hidden_features + n_variables, hidden_features),
57
+ Swish()
58
+ )
59
+ self.update_net_2 = nn.Sequential(nn.Linear(hidden_features, out_features),
60
+ Swish()
61
+ )
62
+ self.norm = InstanceNorm(hidden_features)
63
+
64
+ def forward(self, x, u, pos, variables, edge_index, batch):
65
+ """Propagate messages along edges
66
+ """
67
+ x = self.propagate(edge_index, x=x, u=u, pos=pos, variables=variables)
68
+ x = self.norm(x, batch)
69
+ return x
70
+
71
+ def message(self, x_i, x_j, u_i, u_j, pos_i, pos_j, variables_i):
72
+ """Message update following formula 8 of the paper
73
+ """
74
+ message = self.message_net_1(torch.cat((x_i, x_j, u_i - u_j, pos_i - pos_j, variables_i), dim=-1))
75
+ message = self.message_net_2(message)
76
+ return message
77
+
78
+ def update(self, message, x, variables):
79
+ """Node update following formula 9 of the paper
80
+ """
81
+ update = self.update_net_1(torch.cat((x, message, variables), dim=-1))
82
+ update = self.update_net_2(update)
83
+ if self.in_features == self.out_features:
84
+ return x + update
85
+ else:
86
+ return update
87
+
88
+
89
+
90
+ class MPNN(nn.Module):
91
+ def __init__(self,
92
+ pde: PDE,
93
+ time_window: int = 25,
94
+ hidden_features: int = 128,
95
+ hidden_layers: int = 6,
96
+ eq_variables: dict = {}):
97
+ """Initialize MPNN
98
+ It contains 6 MP-PDE layers with skip connections
99
+ The input graph to the forward pass has the shape [batch*n_nodes, time_window].
100
+ The output graph has the shape [batch*n_nodes, time_window].
101
+ Args:
102
+ pde (PDE): PDE to solve
103
+ time_window (int): number of input/output timesteps (temporal bundling)
104
+ hidden features (int): number of hidden features
105
+ hidden_layer (int): number of hidden layers
106
+ eq_variables (dict): dictionary of equation specific parameters
107
+ """
108
+ super(MPNN, self).__init__()
109
+ # arguments
110
+ self.pde = pde
111
+ self.out_features = time_window
112
+ self.hidden_features = hidden_features
113
+ self.hidden_layers = hidden_layers
114
+ self.time_window = time_window
115
+ self.eq_variables = eq_variables
116
+
117
+ # encoder
118
+ self.embedding_mlp = nn.Sequential(
119
+ nn.Linear(self.time_window+self.pde.spatial_dim+1+len(self.eq_variables), self.hidden_features),
120
+ Swish(),
121
+ nn.Linear(self.hidden_features, self.hidden_features),
122
+ Swish()
123
+ )
124
+
125
+ # processor
126
+ self.gnn_layers = torch.nn.ModuleList(modules=(GNN_Layer(
127
+ in_features=self.hidden_features,
128
+ hidden_features=self.hidden_features,
129
+ out_features=self.hidden_features,
130
+ time_window=self.time_window,
131
+ spatial_dim=self.pde.spatial_dim,
132
+ n_variables=len(self.eq_variables)+1 # (time is treated as equation variable)
133
+ ) for _ in range(self.hidden_layers)))
134
+
135
+ # decoder
136
+ if(self.time_window==10): # NEW ADD
137
+ self.output_mlp = nn.Sequential(
138
+ nn.Conv1d(1, 8, 18, stride=5),
139
+ Swish(),
140
+ nn.Conv1d(8, 1, 14, stride=1)
141
+ )
142
+ if(self.time_window==20):
143
+ self.output_mlp = nn.Sequential(
144
+ nn.Conv1d(1, 8, 15, stride=4),
145
+ Swish(),
146
+ nn.Conv1d(8, 1, 10, stride=1)
147
+ )
148
+ if (self.time_window==25):
149
+ self.output_mlp = nn.Sequential(
150
+ nn.Conv1d(1, 8, 16, stride=3),
151
+ Swish(),
152
+ nn.Conv1d(8, 1, 14, stride=1)
153
+ )
154
+ if(self.time_window==50):
155
+ self.output_mlp = nn.Sequential(
156
+ nn.Conv1d(1, 8, 12, stride=2),
157
+ Swish(),
158
+ nn.Conv1d(8, 1, 10, stride=1)
159
+ )
160
+
161
+ def forward(self, data: Data, v: int=0) -> torch.Tensor:
162
+ """Forward pass of MP-PDE solver class.
163
+ The input graph has the shape [batch*n_nodes, time_window, v].
164
+ The output tensor has the shape [batch*n_nodes, time_window].
165
+ Args:
166
+ data (Data): Pytorch Geometric data graph
167
+ v (int): physics variable to solve
168
+ Returns:
169
+ torch.Tensor: predictive solutions with shape [batch*n_nodes, time_window]
170
+ """
171
+ u = data.x[..., v] # [bs*nx, tw]
172
+ x_pos = data.x_pos # [bs*nx, spatial_dim]
173
+ t_pos = data.t_pos # [bs*nx]
174
+ edge_index = data.edge_index # [2, num_edges]
175
+ batch = data.batch # [bs*nx]
176
+ variables = data.variables # [bs*nx, num_variables]
177
+
178
+ # normalize temporal and spatial coordinate
179
+ t_pos = t_pos / self.pde.tmax
180
+ for d in range(self.pde.spatial_dim):
181
+ x_pos[:, d] = x_pos[:, d] / (self.pde.spatial_domain[d][1] - self.pde.spatial_domain[d][0])
182
+
183
+ # encode
184
+ node_input = torch.cat((u, x_pos, t_pos.unsqueeze(1), variables), dim=-1)
185
+ f = self.embedding_mlp(node_input) # [bs*nx, hidden_dim]
186
+
187
+ # process
188
+ for i in range(self.hidden_layers):
189
+ # f = self.gnn_layers[i](f, u, x_pos, variables, edge_index, batch)
190
+ f = self.gnn_layers[i](f, u, x_pos, torch.cat((t_pos.unsqueeze(1), variables), dim=-1), edge_index, batch)
191
+
192
+ # decode
193
+ dt = (self.pde.tmax - self.pde.tmin) / self.pde.resolution_t
194
+ dt = (torch.ones(1, self.time_window) * dt).to(f.device) # [1, tw]
195
+ dt = torch.cumsum(dt, dim=1) # [1, tw]
196
+ # [bs*nx, hidden_dim] -> [bs*nx, 1, hidden_dim]) -> conv1d -> [bs*nx, 1, tw] -> [bs*nx, tw] (diff)
197
+ diff = self.output_mlp(f[:, None]).squeeze(1)
198
+ # [bs*nx] -> [tw, bs*nx] -> [bs*nx, tw]
199
+ out = u[:, -1].repeat(self.time_window, 1).transpose(0, 1) + dt * diff
200
+ # print(out.shape)
201
+ return out
model/pino_fno.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ ############################
3
+ '''
4
+ The implement codes of FNO were taken and modified from: https://github.com/neuraloperator/physics_informed
5
+ '''
6
+
7
+ import numpy as np
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+
12
+ from onescience.utils.pdenneval.pino_utils import add_padding, remove_padding, add_padding2, remove_padding2, add_padding3, remove_padding3, _get_act
13
+
14
+
15
+ @torch.jit.script
16
+ def compl_mul1d(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
17
+ # (batch, in_channel, x ), (in_channel, out_channel, x) -> (batch, out_channel, x)
18
+ res = torch.einsum("bix,iox->box", a, b)
19
+ return res
20
+
21
+
22
+ @torch.jit.script
23
+ def compl_mul2d(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
24
+ # (batch, in_channel, x,y,t ), (in_channel, out_channel, x,y,t) -> (batch, out_channel, x,y,t)
25
+ res = torch.einsum("bixy,ioxy->boxy", a, b)
26
+ return res
27
+
28
+
29
+ @torch.jit.script
30
+ def compl_mul3d(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
31
+ res = torch.einsum("bixyz,ioxyz->boxyz", a, b)
32
+ return res
33
+
34
+ ################################################################
35
+ # 1d fourier layer
36
+ ################################################################
37
+
38
+
39
+ class SpectralConv1d(nn.Module):
40
+ def __init__(self, in_channels, out_channels, modes1):
41
+ super(SpectralConv1d, self).__init__()
42
+
43
+ """
44
+ 1D Fourier layer. It does FFT, linear transform, and Inverse FFT.
45
+ """
46
+
47
+ self.in_channels = in_channels
48
+ self.out_channels = out_channels
49
+ # Number of Fourier modes to multiply, at most floor(N/2) + 1
50
+ self.modes1 = modes1
51
+
52
+ self.scale = (1 / (in_channels*out_channels))
53
+ self.weights1 = nn.Parameter(
54
+ self.scale * torch.rand(in_channels, out_channels, self.modes1, dtype=torch.cfloat))
55
+
56
+ def forward(self, x):
57
+ batchsize = x.shape[0]
58
+ # Compute Fourier coeffcients up to factor of e^(- something constant)
59
+ x_ft = torch.fft.rfftn(x, dim=[2])
60
+
61
+ # Multiply relevant Fourier modes
62
+ out_ft = torch.zeros(batchsize, self.in_channels, x.size(-1)//2 + 1, device=x.device, dtype=torch.cfloat)
63
+ out_ft[:, :, :self.modes1] = compl_mul1d(x_ft[:, :, :self.modes1], self.weights1)
64
+
65
+ # Return to physical space
66
+ x = torch.fft.irfftn(out_ft, s=[x.size(-1)], dim=[2])
67
+ return x
68
+
69
+ ################################################################
70
+ # 2d fourier layer
71
+ ################################################################
72
+
73
+
74
+ class SpectralConv2d(nn.Module):
75
+ def __init__(self, in_channels, out_channels, modes1, modes2):
76
+ super(SpectralConv2d, self).__init__()
77
+ self.in_channels = in_channels
78
+ self.out_channels = out_channels
79
+ # Number of Fourier modes to multiply, at most floor(N/2) + 1
80
+ self.modes1 = modes1
81
+ self.modes2 = modes2
82
+
83
+ self.scale = (1 / (in_channels * out_channels))
84
+ self.weights1 = nn.Parameter(
85
+ self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, dtype=torch.cfloat))
86
+ self.weights2 = nn.Parameter(
87
+ self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, dtype=torch.cfloat))
88
+
89
+ def forward(self, x):
90
+ batchsize = x.shape[0]
91
+ size1 = x.shape[-2]
92
+ size2 = x.shape[-1]
93
+ # Compute Fourier coeffcients up to factor of e^(- something constant)
94
+ x_ft = torch.fft.rfftn(x, dim=[2, 3])
95
+
96
+ # Multiply relevant Fourier modes
97
+ out_ft = torch.zeros(batchsize, self.out_channels, x.size(-2), x.size(-1) // 2 + 1, device=x.device,
98
+ dtype=torch.cfloat)
99
+ out_ft[:, :, :self.modes1, :self.modes2] = \
100
+ compl_mul2d(x_ft[:, :, :self.modes1, :self.modes2], self.weights1)
101
+ out_ft[:, :, -self.modes1:, :self.modes2] = \
102
+ compl_mul2d(x_ft[:, :, -self.modes1:, :self.modes2], self.weights2)
103
+
104
+ # Return to physical space
105
+ x = torch.fft.irfftn(out_ft, s=(x.size(-2), x.size(-1)), dim=[2, 3])
106
+ return x
107
+
108
+
109
+ class SpectralConv3d(nn.Module):
110
+ def __init__(self, in_channels, out_channels, modes1, modes2, modes3):
111
+ super(SpectralConv3d, self).__init__()
112
+ self.in_channels = in_channels
113
+ self.out_channels = out_channels
114
+ self.modes1 = modes1 #Number of Fourier modes to multiply, at most floor(N/2) + 1
115
+ self.modes2 = modes2
116
+ self.modes3 = modes3
117
+
118
+ self.scale = (1 / (in_channels * out_channels))
119
+ self.weights1 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
120
+ self.weights2 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
121
+ self.weights3 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
122
+ self.weights4 = nn.Parameter(self.scale * torch.rand(in_channels, out_channels, self.modes1, self.modes2, self.modes3, dtype=torch.cfloat))
123
+
124
+ def forward(self, x):
125
+ batchsize = x.shape[0]
126
+ # Compute Fourier coeffcients up to factor of e^(- something constant)
127
+ x_ft = torch.fft.rfftn(x, dim=[2,3,4])
128
+
129
+ z_dim = min(x_ft.shape[4], self.modes3)
130
+
131
+ # Multiply relevant Fourier modes
132
+ out_ft = torch.zeros(batchsize, self.out_channels, x_ft.shape[2], x_ft.shape[3], self.modes3, device=x.device, dtype=torch.cfloat)
133
+
134
+ # if x_ft.shape[4] > self.modes3, truncate; if x_ft.shape[4] < self.modes3, add zero padding
135
+ coeff = torch.zeros(batchsize, self.in_channels, self.modes1, self.modes2, self.modes3, device=x.device, dtype=torch.cfloat)
136
+ coeff[..., :z_dim] = x_ft[:, :, :self.modes1, :self.modes2, :z_dim]
137
+ out_ft[:, :, :self.modes1, :self.modes2, :] = compl_mul3d(coeff, self.weights1)
138
+
139
+ coeff = torch.zeros(batchsize, self.in_channels, self.modes1, self.modes2, self.modes3, device=x.device, dtype=torch.cfloat)
140
+ coeff[..., :z_dim] = x_ft[:, :, -self.modes1:, :self.modes2, :z_dim]
141
+ out_ft[:, :, -self.modes1:, :self.modes2, :] = compl_mul3d(coeff, self.weights2)
142
+
143
+ coeff = torch.zeros(batchsize, self.in_channels, self.modes1, self.modes2, self.modes3, device=x.device, dtype=torch.cfloat)
144
+ coeff[..., :z_dim] = x_ft[:, :, :self.modes1, -self.modes2:, :z_dim]
145
+ out_ft[:, :, :self.modes1, -self.modes2:, :] = compl_mul3d(coeff, self.weights3)
146
+
147
+ coeff = torch.zeros(batchsize, self.in_channels, self.modes1, self.modes2, self.modes3, device=x.device, dtype=torch.cfloat)
148
+ coeff[..., :z_dim] = x_ft[:, :, -self.modes1:, -self.modes2:, :z_dim]
149
+ out_ft[:, :, -self.modes1:, -self.modes2:, :] = compl_mul3d(coeff, self.weights4)
150
+
151
+ #Return to physical space
152
+ x = torch.fft.irfftn(out_ft, s=(x.size(2), x.size(3), x.size(4)), dim=[2,3,4])
153
+ return x
154
+
155
+
156
+ class FourierBlock(nn.Module):
157
+ def __init__(self, in_channels, out_channels, modes1, modes2, modes3, act='tanh'):
158
+ super(FourierBlock, self).__init__()
159
+ self.in_channel = in_channels
160
+ self.out_channel = out_channels
161
+ self.speconv = SpectralConv3d(in_channels, out_channels, modes1, modes2, modes3)
162
+ self.linear = nn.Conv1d(in_channels, out_channels, 1)
163
+ if act in ['tanh','gelu','none']:
164
+ self.act=_get_act(act)
165
+ else:
166
+ raise ValueError(f'{act} is not supported')
167
+
168
+ def forward(self, x):
169
+ '''
170
+ input x: (batchsize, channel width, x_grid, y_grid, t_grid)
171
+ '''
172
+ x1 = self.speconv(x)
173
+ x2 = self.linear(x.view(x.shape[0], self.in_channel, -1))
174
+ out = x1 + x2.view(x.shape[0], self.out_channel, x.shape[2], x.shape[3], x.shape[4])
175
+ if self.act is not None:
176
+ out = self.act(out)
177
+ return out
178
+
179
+
180
+ class FNO1d(nn.Module):
181
+ def __init__(self,
182
+ modes, width=32,
183
+ layers=None,
184
+ fc_dim=128,
185
+ in_dim=2, out_dim=1,
186
+ act='relu',
187
+ pad_ratio=[0.,0.1]):
188
+ super(FNO1d, self).__init__()
189
+
190
+ """
191
+ The overall network. It contains several layers of the Fourier layer.
192
+ 1. Lift the input to the desire channel dimension by self.fc0 .
193
+ 2. 4 layers of the integral operators u' = (W + K)(u).
194
+ W defined by self.w; K defined by self.conv .
195
+ 3. Project from the channel space to the output space by self.fc1 and self.fc2 .
196
+
197
+ input: the solution of the initial condition and location (a(x), x)
198
+ input shape: (batchsize, x=s, c=2)
199
+ output: the solution of a later timestep
200
+ output shape: (batchsize, x=s, c=1)
201
+ """
202
+
203
+ self.modes1 = modes
204
+ self.width = width
205
+ self.pad_ratio = pad_ratio
206
+ if layers is None:
207
+ layers = [width] * 4
208
+
209
+ self.fc0 = nn.Linear(in_dim, layers[0]) # input channel is 2: (a(x), x)
210
+
211
+ self.sp_convs = nn.ModuleList([SpectralConv1d(
212
+ in_size, out_size, num_modes) for in_size, out_size, num_modes in zip(layers, layers[1:], self.modes1)])
213
+
214
+ self.ws = nn.ModuleList([nn.Conv1d(in_size, out_size, 1)
215
+ for in_size, out_size in zip(layers, layers[1:])])
216
+
217
+ self.fc1 = nn.Linear(layers[-1], fc_dim)
218
+ self.fc2 = nn.Linear(fc_dim, out_dim)
219
+ self.act = _get_act(act)
220
+
221
+ def forward(self, x):
222
+ length = len(self.ws)
223
+ size_1= x.shape[1]
224
+ if max(self.pad_ratio) > 0:
225
+ num_pad = [round(size_1 * i) for i in self.pad_ratio]
226
+ else:
227
+ num_pad = [0., 0.]
228
+ x = self.fc0(x)
229
+ x = x.permute(0, 2, 1)
230
+
231
+ x = add_padding(x,num_pad)
232
+ for i, (speconv, w) in enumerate(zip(self.sp_convs, self.ws)):
233
+ x1 = speconv(x)
234
+ x2 = w(x)
235
+ x = x1 + x2
236
+ if i != length - 1:
237
+ x = self.act(x)
238
+ x = remove_padding(x,num_pad)
239
+
240
+ x = x.permute(0, 2, 1)
241
+ x = self.fc1(x)
242
+ x = self.act(x)
243
+ x = self.fc2(x)
244
+ return x
245
+
246
+
247
+ class FNO2d(nn.Module):
248
+ def __init__(self, modes1, modes2,
249
+ width=64, fc_dim=128,
250
+ layers=None,
251
+ in_dim=3, out_dim=1,
252
+ act='gelu',
253
+ pad_ratio=[0., 0.1]):
254
+ super(FNO2d, self).__init__()
255
+ """
256
+ Args:
257
+ - modes1: list of int, number of modes in first dimension in each layer
258
+ - modes2: list of int, number of modes in second dimension in each layer
259
+ - width: int, optional, if layers is None, it will be initialized as [width] * [len(modes1) + 1]
260
+ - in_dim: number of input channels
261
+ - out_dim: number of output channels
262
+ - act: activation function, {tanh, gelu, relu, leaky_relu}, default: gelu
263
+ - pad_ratio: list of float, or float; portion of domain to be extended. If float, paddings are added to the right.
264
+ If list, paddings are added to both sides. pad_ratio[0] pads left, pad_ratio[1] pads right.
265
+ """
266
+ if isinstance(pad_ratio, float):
267
+ pad_ratio = [pad_ratio, pad_ratio]
268
+ else:
269
+ assert len(pad_ratio) == 2, 'Cannot add padding in more than 2 directions'
270
+ self.modes1 = modes1
271
+ self.modes2 = modes2
272
+
273
+ self.pad_ratio = pad_ratio
274
+ # input channel is 3: (a(x, y), x, y)
275
+ if layers is None:
276
+ self.layers = [width] * (len(modes1) + 1)
277
+ else:
278
+ self.layers = layers
279
+
280
+ self.fc0 = nn.Linear(in_dim, self.layers[0])
281
+
282
+ self.sp_convs = nn.ModuleList([SpectralConv2d(
283
+ in_size, out_size, mode1_num, mode2_num)
284
+ for in_size, out_size, mode1_num, mode2_num
285
+ in zip(self.layers, self.layers[1:], self.modes1, self.modes2)])
286
+
287
+ self.ws = nn.ModuleList([nn.Conv1d(in_size, out_size, 1)
288
+ for in_size, out_size in zip(self.layers, self.layers[1:])])
289
+
290
+ self.fc1 = nn.Linear(self.layers[-1], fc_dim)
291
+ self.fc2 = nn.Linear(fc_dim, self.layers[-1])
292
+ self.fc3 = nn.Linear(self.layers[-1], out_dim)
293
+ self.act = _get_act(act)
294
+
295
+ def forward(self, x):
296
+ '''
297
+ Args:
298
+ - x : (batch size, x_grid, y_grid, 2)
299
+ Returns:
300
+ - x: (batch size, x_grid, y_grid, 1)
301
+ '''
302
+ size_1, size_2 = x.shape[1], x.shape[2]
303
+ if max(self.pad_ratio) > 0:
304
+ num_pad1 = [round(i * size_1) for i in self.pad_ratio]
305
+ num_pad2 = [round(i * size_2) for i in self.pad_ratio]
306
+ else:
307
+ num_pad1 = num_pad2 = [0.,0.]
308
+
309
+ length = len(self.ws)
310
+ batchsize = x.shape[0]
311
+ x = self.fc0(x)
312
+ x = x.permute(0, 3, 1, 2) # B, C, X, Y
313
+ x = add_padding2(x, num_pad1, num_pad2)
314
+ size_x, size_y = x.shape[-2], x.shape[-1]
315
+
316
+ for i, (speconv, w) in enumerate(zip(self.sp_convs, self.ws)):
317
+ x1 = speconv(x)
318
+ x2 = w(x.view(batchsize, self.layers[i], -1)).view(batchsize, self.layers[i+1], size_x, size_y)
319
+ x = x1 + x2
320
+ if i != length - 1:
321
+ x = self.act(x)
322
+ x = remove_padding2(x, num_pad1, num_pad2)
323
+ x = x.permute(0, 2, 3, 1)
324
+ x = self.fc1(x)
325
+ x = self.act(x)
326
+ x = self.fc2(x)
327
+ x = self.act(x)
328
+ x = self.fc3(x)
329
+ return x
330
+
331
+ class FNO3d(nn.Module):
332
+ def __init__(self,
333
+ modes1, modes2, modes3,
334
+ width=16,
335
+ fc_dim=128,
336
+ layers=None,
337
+ in_dim=4, out_dim=1,
338
+ act='gelu',
339
+ pad_ratio=[0., 0.05]):
340
+ '''
341
+ Args:
342
+ modes1: list of int, first dimension maximal modes for each layer
343
+ modes2: list of int, second dimension maximal modes for each layer
344
+ modes3: list of int, third dimension maximal modes for each layer
345
+ layers: list of int, channels for each layer
346
+ fc_dim: dimension of fully connected layers
347
+ in_dim: int, input dimension
348
+ out_dim: int, output dimension
349
+ act: {tanh, gelu, relu, leaky_relu}, activation function
350
+ pad_ratio: the ratio of the extended domain
351
+ '''
352
+ super(FNO3d, self).__init__()
353
+
354
+ if isinstance(pad_ratio, float):
355
+ pad_ratio = [pad_ratio, pad_ratio]
356
+ else:
357
+ assert len(pad_ratio) == 2, 'Cannot add padding in more than 2 directions.'
358
+
359
+ self.pad_ratio = pad_ratio
360
+ self.modes1 = modes1
361
+ self.modes2 = modes2
362
+ self.modes3 = modes3
363
+ self.pad_ratio = pad_ratio
364
+
365
+ if layers is None:
366
+ self.layers = [width] * 4
367
+ else:
368
+ self.layers = layers
369
+ self.fc0 = nn.Linear(in_dim, self.layers[0])
370
+
371
+ self.sp_convs = nn.ModuleList([SpectralConv3d(
372
+ in_size, out_size, mode1_num, mode2_num, mode3_num)
373
+ for in_size, out_size, mode1_num, mode2_num, mode3_num
374
+ in zip(self.layers, self.layers[1:], self.modes1, self.modes2, self.modes3)])
375
+
376
+ self.ws = nn.ModuleList([nn.Conv1d(in_size, out_size, 1)
377
+ for in_size, out_size in zip(self.layers, self.layers[1:])])
378
+
379
+ self.fc1 = nn.Linear(self.layers[-1], fc_dim)
380
+ self.fc2 = nn.Linear(fc_dim, out_dim)
381
+ self.act = _get_act(act)
382
+
383
+ def forward(self, x):
384
+ '''
385
+ Args:
386
+ x: (batchsize, x_grid, y_grid, t_grid, 3)
387
+
388
+ Returns:
389
+ u: (batchsize, x_grid, y_grid, t_grid, 1)
390
+
391
+ '''
392
+ size_x,size_y,size_z = x.shape[1],x.shape[2],x.shape[3]
393
+ if max(self.pad_ratio) > 0:
394
+ num_pad1 = [round(size_x * i) for i in self.pad_ratio]
395
+ num_pad2 = [round(size_y * i) for i in self.pad_ratio]
396
+ num_pad3 = [round(size_z * i) for i in self.pad_ratio]
397
+ else:
398
+ num_pad1 = num_pad2 = num_pad3 = [0., 0.]
399
+ length = len(self.ws)
400
+ batchsize = x.shape[0]
401
+
402
+ x = self.fc0(x)
403
+ x = x.permute(0, 4, 1, 2, 3)
404
+ x = add_padding3(x, num_pad1,num_pad2,num_pad3)
405
+ size_x, size_y, size_z = x.shape[-3], x.shape[-2], x.shape[-1]
406
+
407
+ for i, (speconv, w) in enumerate(zip(self.sp_convs, self.ws)):
408
+ x1 = speconv(x)
409
+ x2 = w(x.view(batchsize, self.layers[i], -1)).view(batchsize, self.layers[i+1], size_x, size_y, size_z)
410
+ x = x1 + x2
411
+ if i != length - 1:
412
+ x = self.act(x)
413
+ x = remove_padding3(x, num_pad1,num_pad2,num_pad3)
414
+ x = x.permute(0, 2, 3, 4, 1)
415
+ x = self.fc1(x)
416
+ x = self.act(x)
417
+ x = self.fc2(x)
418
+ return x
419
+
420
+
model/unet.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from collections import OrderedDict
4
+
5
+ class UNet1d(nn.Module):
6
+
7
+ def __init__(self, in_channels=3, out_channels=1, init_features=32):
8
+ super(UNet1d, self).__init__()
9
+
10
+ features = init_features
11
+ self.encoder1 = UNet1d._block(in_channels, features, name="enc1")
12
+ self.pool1 = nn.MaxPool1d(kernel_size=2, stride=2)
13
+ self.encoder2 = UNet1d._block(features, features * 2, name="enc2")
14
+ self.pool2 = nn.MaxPool1d(kernel_size=2, stride=2)
15
+ self.encoder3 = UNet1d._block(features * 2, features * 4, name="enc3")
16
+ self.pool3 = nn.MaxPool1d(kernel_size=2, stride=2)
17
+ self.encoder4 = UNet1d._block(features * 4, features * 8, name="enc4")
18
+ self.pool4 = nn.MaxPool1d(kernel_size=2, stride=2)
19
+
20
+ self.bottleneck = UNet1d._block(features * 8, features * 16, name="bottleneck")
21
+
22
+ self.upconv4 = nn.ConvTranspose1d(features * 16, features * 8, kernel_size=2, stride=2)
23
+ self.decoder4 = UNet1d._block((features * 8) * 2, features * 8, name="dec4")
24
+ self.upconv3 = nn.ConvTranspose1d(features * 8, features * 4, kernel_size=2, stride=2)
25
+ self.decoder3 = UNet1d._block((features * 4) * 2, features * 4, name="dec3")
26
+ self.upconv2 = nn.ConvTranspose1d(features * 4, features * 2, kernel_size=2, stride=2)
27
+ self.decoder2 = UNet1d._block((features * 2) * 2, features * 2, name="dec2")
28
+ self.upconv1 = nn.ConvTranspose1d(features * 2, features, kernel_size=2, stride=2)
29
+ self.decoder1 = UNet1d._block(features * 2, features, name="dec1")
30
+
31
+ self.conv = nn.Conv1d(in_channels=features, out_channels=out_channels, kernel_size=1)
32
+
33
+ def forward(self, x):
34
+ enc1 = self.encoder1(x)
35
+ enc2 = self.encoder2(self.pool1(enc1))
36
+ enc3 = self.encoder3(self.pool2(enc2))
37
+ enc4 = self.encoder4(self.pool3(enc3))
38
+
39
+ bottleneck = self.bottleneck(self.pool4(enc4))
40
+
41
+ dec4 = self.upconv4(bottleneck)
42
+ dec4 = torch.cat((dec4, enc4), dim=1)
43
+ dec4 = self.decoder4(dec4)
44
+ dec3 = self.upconv3(dec4)
45
+ dec3 = torch.cat((dec3, enc3), dim=1)
46
+ dec3 = self.decoder3(dec3)
47
+ dec2 = self.upconv2(dec3)
48
+ dec2 = torch.cat((dec2, enc2), dim=1)
49
+ dec2 = self.decoder2(dec2)
50
+ dec1 = self.upconv1(dec2)
51
+ dec1 = torch.cat((dec1, enc1), dim=1)
52
+ dec1 = self.decoder1(dec1)
53
+ out = self.conv(dec1)
54
+ return out
55
+
56
+ @staticmethod
57
+ def _block(in_channels, features, name):
58
+ return nn.Sequential(
59
+ OrderedDict(
60
+ [
61
+ (
62
+ name + "conv1",
63
+ nn.Conv1d(
64
+ in_channels=in_channels,
65
+ out_channels=features,
66
+ kernel_size=3,
67
+ padding=1,
68
+ bias=False,
69
+ ),
70
+ ),
71
+ (name + "norm1", nn.BatchNorm1d(num_features=features)),
72
+ (name + "tanh1", nn.Tanh()),
73
+ (
74
+ name + "conv2",
75
+ nn.Conv1d(
76
+ in_channels=features,
77
+ out_channels=features,
78
+ kernel_size=3,
79
+ padding=1,
80
+ bias=False,
81
+ ),
82
+ ),
83
+ (name + "norm2", nn.BatchNorm1d(num_features=features)),
84
+ (name + "tanh2", nn.Tanh()),
85
+ ]
86
+ )
87
+ )
88
+
89
+
90
+ class UNet2d(nn.Module):
91
+
92
+ def __init__(self, in_channels=3, out_channels=1, init_features=32):
93
+ super(UNet2d, self).__init__()
94
+
95
+ features = init_features
96
+ self.encoder1 = UNet2d._block(in_channels, features, name="enc1")
97
+ self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
98
+ self.encoder2 = UNet2d._block(features, features * 2, name="enc2")
99
+ self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
100
+ self.encoder3 = UNet2d._block(features * 2, features * 4, name="enc3")
101
+ self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
102
+ self.encoder4 = UNet2d._block(features * 4, features * 8, name="enc4")
103
+ self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
104
+
105
+ self.bottleneck = UNet2d._block(features * 8, features * 16, name="bottleneck")
106
+
107
+ self.upconv4 = nn.ConvTranspose2d(features * 16, features * 8, kernel_size=2, stride=2)
108
+ self.decoder4 = UNet2d._block((features * 8) * 2, features * 8, name="dec4")
109
+ self.upconv3 = nn.ConvTranspose2d(features * 8, features * 4, kernel_size=2, stride=2)
110
+ self.decoder3 = UNet2d._block((features * 4) * 2, features * 4, name="dec3")
111
+ self.upconv2 = nn.ConvTranspose2d(features * 4, features * 2, kernel_size=2, stride=2)
112
+ self.decoder2 = UNet2d._block((features * 2) * 2, features * 2, name="dec2")
113
+ self.upconv1 = nn.ConvTranspose2d(features * 2, features, kernel_size=2, stride=2)
114
+ self.decoder1 = UNet2d._block(features * 2, features, name="dec1")
115
+
116
+ self.conv = nn.Conv2d(in_channels=features, out_channels=out_channels, kernel_size=1)
117
+
118
+ def forward(self, x):
119
+ enc1 = self.encoder1(x)
120
+ enc2 = self.encoder2(self.pool1(enc1))
121
+ enc3 = self.encoder3(self.pool2(enc2))
122
+ enc4 = self.encoder4(self.pool3(enc3))
123
+
124
+ bottleneck = self.bottleneck(self.pool4(enc4))
125
+
126
+ dec4 = self.upconv4(bottleneck)
127
+ dec4 = torch.cat((dec4, enc4), dim=1)
128
+ dec4 = self.decoder4(dec4)
129
+ dec3 = self.upconv3(dec4)
130
+ dec3 = torch.cat((dec3, enc3), dim=1)
131
+ dec3 = self.decoder3(dec3)
132
+ dec2 = self.upconv2(dec3)
133
+ dec2 = torch.cat((dec2, enc2), dim=1)
134
+ dec2 = self.decoder2(dec2)
135
+ dec1 = self.upconv1(dec2)
136
+ dec1 = torch.cat((dec1, enc1), dim=1)
137
+ dec1 = self.decoder1(dec1)
138
+ return self.conv(dec1)
139
+
140
+ @staticmethod
141
+ def _block(in_channels, features, name):
142
+ return nn.Sequential(
143
+ OrderedDict(
144
+ [
145
+ (
146
+ name + "conv1",
147
+ nn.Conv2d(
148
+ in_channels=in_channels,
149
+ out_channels=features,
150
+ kernel_size=3,
151
+ padding=1,
152
+ bias=False,
153
+ ),
154
+ ),
155
+ (name + "norm1", nn.BatchNorm2d(num_features=features)),
156
+ (name + "tanh1", nn.Tanh()),
157
+ (
158
+ name + "conv2",
159
+ nn.Conv2d(
160
+ in_channels=features,
161
+ out_channels=features,
162
+ kernel_size=3,
163
+ padding=1,
164
+ bias=False,
165
+ ),
166
+ ),
167
+ (name + "norm2", nn.BatchNorm2d(num_features=features)),
168
+ (name + "tanh2", nn.Tanh()),
169
+ ]
170
+ )
171
+ )
172
+
173
+
174
+ class UNet3d(nn.Module):
175
+
176
+ def __init__(self, in_channels=3, out_channels=1, init_features=32):
177
+ super(UNet3d, self).__init__()
178
+
179
+ features = init_features
180
+ self.encoder1 = UNet3d._block(in_channels, features, name="enc1")
181
+ self.pool1 = nn.MaxPool3d(kernel_size=2, stride=2)
182
+ self.encoder2 = UNet3d._block(features, features * 2, name="enc2")
183
+ self.pool2 = nn.MaxPool3d(kernel_size=2, stride=2)
184
+ self.encoder3 = UNet3d._block(features * 2, features * 4, name="enc3")
185
+ self.pool3 = nn.MaxPool3d(kernel_size=2, stride=2)
186
+ self.encoder4 = UNet3d._block(features * 4, features * 8, name="enc4")
187
+ self.pool4 = nn.MaxPool3d(kernel_size=2, stride=2)
188
+
189
+ self.bottleneck = UNet3d._block(features * 8, features * 16, name="bottleneck")
190
+
191
+ self.upconv4 = nn.ConvTranspose3d(features * 16, features * 8, kernel_size=2, stride=2)
192
+ self.decoder4 = UNet3d._block((features * 8) * 2, features * 8, name="dec4")
193
+ self.upconv3 = nn.ConvTranspose3d(features * 8, features * 4, kernel_size=2, stride=2)
194
+ self.decoder3 = UNet3d._block((features * 4) * 2, features * 4, name="dec3")
195
+ self.upconv2 = nn.ConvTranspose3d(features * 4, features * 2, kernel_size=2, stride=2)
196
+ self.decoder2 = UNet3d._block((features * 2) * 2, features * 2, name="dec2")
197
+ self.upconv1 = nn.ConvTranspose3d(features * 2, features, kernel_size=2, stride=2)
198
+ self.decoder1 = UNet3d._block(features * 2, features, name="dec1")
199
+
200
+ self.conv = nn.Conv3d(in_channels=features, out_channels=out_channels, kernel_size=1)
201
+
202
+ def forward(self, x):
203
+ enc1 = self.encoder1(x)
204
+ enc2 = self.encoder2(self.pool1(enc1))
205
+ enc3 = self.encoder3(self.pool2(enc2))
206
+ enc4 = self.encoder4(self.pool3(enc3))
207
+
208
+ bottleneck = self.bottleneck(self.pool4(enc4))
209
+
210
+ dec4 = self.upconv4(bottleneck)
211
+ dec4 = torch.cat((dec4, enc4), dim=1)
212
+ dec4 = self.decoder4(dec4)
213
+ dec3 = self.upconv3(dec4)
214
+ dec3 = torch.cat((dec3, enc3), dim=1)
215
+ dec3 = self.decoder3(dec3)
216
+ dec2 = self.upconv2(dec3)
217
+ dec2 = torch.cat((dec2, enc2), dim=1)
218
+ dec2 = self.decoder2(dec2)
219
+ dec1 = self.upconv1(dec2)
220
+ dec1 = torch.cat((dec1, enc1), dim=1)
221
+ dec1 = self.decoder1(dec1)
222
+ return self.conv(dec1)
223
+
224
+ @staticmethod
225
+ def _block(in_channels, features, name):
226
+ return nn.Sequential(
227
+ OrderedDict(
228
+ [
229
+ (
230
+ name + "conv1",
231
+ nn.Conv3d(
232
+ in_channels=in_channels,
233
+ out_channels=features,
234
+ kernel_size=3,
235
+ padding=1,
236
+ bias=False,
237
+ ),
238
+ ),
239
+ (name + "norm1", nn.BatchNorm3d(num_features=features)),
240
+ (name + "tanh1", nn.Tanh()),
241
+ (
242
+ name + "conv2",
243
+ nn.Conv3d(
244
+ in_channels=features,
245
+ out_channels=features,
246
+ kernel_size=3,
247
+ padding=1,
248
+ bias=False,
249
+ ),
250
+ ),
251
+ (name + "norm2", nn.BatchNorm3d(num_features=features)),
252
+ (name + "tanh2", nn.Tanh()),
253
+ ]
254
+ )
255
+ )
model/uno.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Codes for section: Results on Darcy Flow Equation
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from onescience.utils.pdenneval.integral_operators import *
8
+
9
+ ###############
10
+ # UNO^dagger achitechtures
11
+ ###############
12
+ class UNO1d(nn.Module):
13
+ def __init__(self, num_channels, width, pad = 9, factor = 1, initial_step = 10):
14
+ super(UNO1d, self).__init__()
15
+
16
+ self.in_width = num_channels * initial_step + 1 # input channel
17
+ self.width = width
18
+
19
+ self.padding = pad # pad the domain if input is non-periodic
20
+
21
+ self.fc_n1 = nn.Linear(self.in_width, self.width//2)
22
+
23
+ self.fc0 = nn.Linear(self.width//2, self.width)
24
+
25
+ self.conv0 = OperatorBlock_1D(self.width, 2*factor*self.width,24, 11)
26
+
27
+ self.conv1 = OperatorBlock_1D(2*factor*self.width, 4*factor*self.width, 12, 5, Normalize = True)
28
+
29
+ self.conv2 = OperatorBlock_1D(4*factor*self.width, 4*factor*self.width, 12, 5)
30
+
31
+ self.conv4 = OperatorBlock_1D(4*factor*self.width, 2*factor*self.width, 24, 5, Normalize = True)
32
+
33
+ self.conv5 = OperatorBlock_1D(4*factor*self.width, self.width, 57, 11) # will be reshaped
34
+
35
+ self.fc1 = nn.Linear(2*self.width, 1*self.width)
36
+ self.fc2 = nn.Linear(1*self.width, num_channels)
37
+
38
+ def forward(self, x, grid):
39
+ x = torch.cat((x, grid), dim=-1)
40
+
41
+ x_fc_1 = self.fc_n1(x)
42
+ x_fc_1 = F.gelu(x_fc_1)
43
+
44
+ x_fc0 = self.fc0(x_fc_1)
45
+ x_fc0 = F.gelu(x_fc0)
46
+
47
+ x_fc0 = x_fc0.permute(0, 2, 1)
48
+ # scale = math.ceil(x_fc0.shape[-1]/43)
49
+ x_fc0 = F.pad(x_fc0, [0,self.padding])
50
+
51
+ D1 = x_fc0.shape[-1]
52
+ x_c0 = self.conv0(x_fc0,D1//2)
53
+
54
+ x_c1 = self.conv1(x_c0,D1//4)
55
+
56
+ x_c2 = self.conv2(x_c1,D1//4)
57
+
58
+ x_c4 = self.conv4(x_c2 ,D1//2)
59
+ x_c4 = torch.cat([x_c4, x_c0], dim=1)
60
+
61
+ x_c5 = self.conv5(x_c4,D1)
62
+ x_c5 = torch.cat([x_c5, x_fc0], dim=1)
63
+
64
+
65
+ if self.padding!=0:
66
+ x_c5 = x_c5[..., :-self.padding]
67
+
68
+
69
+ x_c5 = x_c5.permute(0, 2, 1)
70
+
71
+ x_fc1 = self.fc1(x_c5)
72
+ x_fc1 = F.gelu(x_fc1)
73
+
74
+ x_out = self.fc2(x_fc1)
75
+ return x_out.unsqueeze(-2)
76
+
77
+ class UNO2d(nn.Module):
78
+ def __init__(self, num_channels, width, pad = 6, factor = 1, initial_step = 10):
79
+ super(UNO2d, self).__init__()
80
+
81
+ self.in_width = num_channels * initial_step + 2 # input channel
82
+ self.width = width
83
+
84
+ self.padding = pad # pad the domain if input is non-periodic
85
+
86
+ self.fc_n1 = nn.Linear(self.in_width, self.width//2)
87
+
88
+ self.fc0 = nn.Linear(self.width//2, self.width)
89
+
90
+ self.conv0 = OperatorBlock_2D(self.width, 2*factor*self.width,36, 36, 17, 17)
91
+
92
+ self.conv1 = OperatorBlock_2D(2*factor*self.width, 4*factor*self.width, 18, 18, 7, 7, Normalize = True)
93
+
94
+ self.conv2 = OperatorBlock_2D(4*factor*self.width, 4*factor*self.width, 18, 18, 7, 7)
95
+
96
+ self.conv4 = OperatorBlock_2D(4*factor*self.width, 2*factor*self.width, 36, 36, 7, 7, Normalize = True)
97
+
98
+ self.conv5 = OperatorBlock_2D(4*factor*self.width, self.width, 81, 81, 17, 17) # will be reshaped
99
+
100
+ self.fc1 = nn.Linear(2*self.width, 1*self.width)
101
+ self.fc2 = nn.Linear(1*self.width, num_channels)
102
+
103
+ def forward(self, x, grid):
104
+ x = torch.cat((x, grid), dim=-1)
105
+
106
+ x_fc_1 = self.fc_n1(x)
107
+ x_fc_1 = F.gelu(x_fc_1)
108
+
109
+ x_fc0 = self.fc0(x_fc_1)
110
+ x_fc0 = F.gelu(x_fc0)
111
+
112
+ x_fc0 = x_fc0.permute(0, 3, 1, 2)
113
+
114
+ # scale = math.ceil(x_fc0.shape[-1]/85)
115
+ x_fc0 = F.pad(x_fc0, [0,self.padding, 0,self.padding])
116
+
117
+ D1,D2 = x_fc0.shape[-2],x_fc0.shape[-1]
118
+
119
+ x_c0 = self.conv0(x_fc0,D1//2,D2//2)
120
+
121
+ x_c1 = self.conv1(x_c0,D1//4,D2//4)
122
+
123
+ x_c2 = self.conv2(x_c1,D1//4,D2//4)
124
+
125
+ x_c4 = self.conv4(x_c2 ,D1//2,D2//2)
126
+ x_c4 = torch.cat([x_c4, x_c0], dim=1)
127
+
128
+ x_c5 = self.conv5(x_c4,D1,D2)
129
+ x_c5 = torch.cat([x_c5, x_fc0], dim=1)
130
+
131
+
132
+ if self.padding!=0:
133
+ x_c5 = x_c5[..., :-self.padding, :-self.padding]
134
+
135
+
136
+ x_c5 = x_c5.permute(0, 2, 3, 1)
137
+
138
+ x_fc1 = self.fc1(x_c5)
139
+ x_fc1 = F.gelu(x_fc1)
140
+
141
+ x_out = self.fc2(x_fc1)
142
+
143
+ return x_out.unsqueeze(-2)
144
+
145
+ class UNO3d(nn.Module):
146
+ def __init__(self, num_channels, width, pad = 5, factor = 1, initial_step = 10):
147
+ super(UNO3d, self).__init__()
148
+
149
+ self.in_width = num_channels * initial_step + 3 # input channel
150
+ self.width = width
151
+
152
+ self.padding = pad # pad the domain if input is non-periodic
153
+
154
+ self.fc_n1 = nn.Linear(self.in_width, self.width//2)
155
+
156
+ self.fc0 = nn.Linear(self.width//2, self.width)
157
+
158
+ self.conv0 = OperatorBlock_3D(self.width, 2*factor*self.width,36, 36, 36, 17, 17, 17)
159
+
160
+ self.conv1 = OperatorBlock_3D(2*factor*self.width, 4*factor*self.width, 18, 18, 18, 7, 7, 7, Normalize = True)
161
+
162
+ self.conv2 = OperatorBlock_3D(4*factor*self.width, 4*factor*self.width, 18, 18, 18, 7, 7, 7)
163
+
164
+ self.conv4 = OperatorBlock_3D(4*factor*self.width, 2*factor*self.width, 36, 36, 36, 7, 7, 7, Normalize = True)
165
+
166
+ self.conv5 = OperatorBlock_3D(4*factor*self.width, self.width, 81, 81, 81, 17, 17, 17) # will be reshaped
167
+
168
+ self.fc1 = nn.Linear(2*self.width, 1*self.width)
169
+ self.fc2 = nn.Linear(1*self.width, num_channels)
170
+
171
+ def forward(self, x, grid):
172
+ x = torch.cat((x, grid), dim=-1)
173
+
174
+ x_fc_1 = self.fc_n1(x)
175
+ x_fc_1 = F.gelu(x_fc_1)
176
+
177
+ x_fc0 = self.fc0(x_fc_1)
178
+ x_fc0 = F.gelu(x_fc0)
179
+
180
+ x_fc0 = x_fc0.permute(0, 4, 1, 2, 3)
181
+
182
+ # scale = math.ceil(x_fc0.shape[-1]/85)
183
+ x_fc0 = F.pad(x_fc0, [0,self.padding, 0,self.padding, 0,self.padding])
184
+
185
+ D1,D2,D3 = x_fc0.shape[-3],x_fc0.shape[-2],x_fc0.shape[-1]
186
+
187
+ x_c0 = self.conv0(x_fc0,D1//2,D2//2,D3//2)
188
+
189
+ x_c1 = self.conv1(x_c0,D1//4,D2//4,D3//4)
190
+
191
+ x_c2 = self.conv2(x_c1,D1//4,D2//4,D3//4)
192
+
193
+ x_c4 = self.conv4(x_c2 ,D1//2,D2//2,D3//2)
194
+ x_c4 = torch.cat([x_c4, x_c0], dim=1)
195
+
196
+ x_c5 = self.conv5(x_c4,D1,D2,D3)
197
+ x_c5 = torch.cat([x_c5, x_fc0], dim=1)
198
+
199
+
200
+ if self.padding!=0:
201
+ x_c5 = x_c5[..., :-self.padding, :-self.padding, :-self.padding]
202
+
203
+
204
+ x_c5 = x_c5.permute(0, 2, 3, 4, 1)
205
+
206
+ x_fc1 = self.fc1(x_c5)
207
+ x_fc1 = F.gelu(x_fc1)
208
+
209
+ x_out = self.fc2(x_fc1)
210
+
211
+ return x_out.unsqueeze(-2)
212
+
213
+ class UNO_maxwell(nn.Module):
214
+ def __init__(self, num_channels, width, pad = 5, factor = 1, initial_step = 10):
215
+ super(UNO_maxwell, self).__init__()
216
+
217
+ self.in_width = num_channels * initial_step # input channel
218
+ self.width = width
219
+
220
+ self.padding = pad # pad the domain if input is non-periodic
221
+
222
+ self.fc_n1 = nn.Linear(self.in_width, self.width//2)
223
+
224
+ self.fc0 = nn.Linear(self.width//2, self.width)
225
+
226
+ self.conv0 = OperatorBlock_3D(self.width, 2*factor*self.width,24, 24, 24, 10, 10, 10)
227
+
228
+ self.conv1 = OperatorBlock_3D(2*factor*self.width, 4*factor*self.width, 12, 12, 12, 5, 5, 5, Normalize = True)
229
+
230
+ self.conv2 = OperatorBlock_3D(4*factor*self.width, 4*factor*self.width, 12, 12, 12, 5, 5, 5)
231
+
232
+ self.conv4 = OperatorBlock_3D(4*factor*self.width, 2*factor*self.width, 12, 12, 12, 5, 5, 5, Normalize = True)
233
+
234
+ self.conv5 = OperatorBlock_3D(4*factor*self.width, self.width, 53, 53, 53, 10, 10, 10) # will be reshaped
235
+
236
+ self.fc1 = nn.Linear(2*self.width, 1*self.width)
237
+ self.fc2 = nn.Linear(1*self.width, num_channels)
238
+
239
+ def forward(self, x, grid):
240
+ x_fc_1 = self.fc_n1(x)
241
+ x_fc_1 = F.gelu(x_fc_1)
242
+
243
+ x_fc0 = self.fc0(x_fc_1)
244
+ x_fc0 = F.gelu(x_fc0)
245
+
246
+ x_fc0 = x_fc0.permute(0, 4, 1, 2, 3)
247
+
248
+ # scale = math.ceil(x_fc0.shape[-1]/85)
249
+ x_fc0 = F.pad(x_fc0, [0,self.padding, 0,self.padding, 0,self.padding])
250
+
251
+ D1,D2,D3 = x_fc0.shape[-3],x_fc0.shape[-2],x_fc0.shape[-1]
252
+
253
+ x_c0 = self.conv0(x_fc0,D1//2,D2//2,D3//2)
254
+
255
+ x_c1 = self.conv1(x_c0,D1//4,D2//4,D3//4)
256
+
257
+ x_c2 = self.conv2(x_c1,D1//4,D2//4,D3//4)
258
+
259
+ x_c4 = self.conv4(x_c2 ,D1//2,D2//2,D3//2)
260
+ x_c4 = torch.cat([x_c4, x_c0], dim=1)
261
+
262
+ x_c5 = self.conv5(x_c4,D1,D2,D3)
263
+ x_c5 = torch.cat([x_c5, x_fc0], dim=1)
264
+
265
+
266
+ if self.padding!=0:
267
+ x_c5 = x_c5[..., :-self.padding, :-self.padding, :-self.padding]
268
+
269
+
270
+ x_c5 = x_c5.permute(0, 2, 3, 4, 1)
271
+
272
+ x_fc1 = self.fc1(x_c5)
273
+ x_fc1 = F.gelu(x_fc1)
274
+
275
+ x_out = self.fc2(x_fc1)
276
+
277
+ return x_out.unsqueeze(-2)
278
+
scripts/common.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import warnings
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+ from typing import Any, Optional
9
+
10
+ import h5py
11
+ import numpy as np
12
+ import torch
13
+ from torch.utils.data import DataLoader, Dataset
14
+
15
+
16
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
17
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
18
+
19
+ if str(PROJECT_ROOT) not in sys.path:
20
+ sys.path.insert(0, str(PROJECT_ROOT))
21
+
22
+ from model.fno import FNO1d, FNO2d, FNO3d, FNO_maxwell
23
+
24
+ try:
25
+ from onescience.utils.YParams import YParams
26
+ except ModuleNotFoundError as exc:
27
+ YParams = None
28
+ YPARAMS_IMPORT_ERROR = exc
29
+
30
+ try:
31
+ from onescience.distributed.manager import DistributedManager
32
+ except ModuleNotFoundError as exc:
33
+ DistributedManager = None
34
+ DISTRIBUTED_IMPORT_ERROR = exc
35
+
36
+ try:
37
+ from onescience.datapipes.cfd.PDENNEval import PDEBenchFNODatapipe
38
+ except (ImportError, ModuleNotFoundError, OSError) as exc:
39
+ PDEBenchFNODatapipe = None
40
+ DATAPIPE_IMPORT_ERROR = exc
41
+
42
+
43
+ def get_attr(obj: Any, name: str, default: Any = None) -> Any:
44
+ if isinstance(obj, dict):
45
+ return obj.get(name, default)
46
+ return getattr(obj, name, default)
47
+
48
+
49
+ def set_attr(obj: Any, name: str, value: Any) -> None:
50
+ if isinstance(obj, dict):
51
+ obj[name] = value
52
+ else:
53
+ setattr(obj, name, value)
54
+
55
+
56
+ def to_namespace(value: Any) -> Any:
57
+ if isinstance(value, dict):
58
+ return SimpleNamespace(**{key: to_namespace(val) for key, val in value.items()})
59
+ if isinstance(value, list):
60
+ return [to_namespace(item) for item in value]
61
+ return value
62
+
63
+
64
+ def load_config(config_path: Path | str = DEFAULT_CONFIG) -> Any:
65
+ config_path = Path(config_path).expanduser().resolve()
66
+ if YParams is not None:
67
+ return YParams(str(config_path), "fno_config")
68
+
69
+ import yaml
70
+
71
+ with config_path.open("r", encoding="utf-8") as handle:
72
+ raw = yaml.safe_load(handle)
73
+ if not isinstance(raw, dict) or "fno_config" not in raw:
74
+ raise ValueError(f"{config_path} must contain root key 'fno_config'")
75
+ return to_namespace(raw["fno_config"])
76
+
77
+
78
+ def resolve_path(value: Any, root: Path = PROJECT_ROOT) -> Path:
79
+ if value is None:
80
+ raise ValueError("path value can not be null")
81
+ expanded = os.path.expandvars(os.path.expanduser(str(value)))
82
+ path = Path(expanded)
83
+ if not path.is_absolute():
84
+ path = root / path
85
+ return path.resolve()
86
+
87
+
88
+ def prepare_config(
89
+ cfg: Any,
90
+ data_dir: Optional[str] = None,
91
+ output_dir: Optional[str] = None,
92
+ checkpoint: Optional[str] = None,
93
+ ) -> Any:
94
+ source = cfg.datapipe.source
95
+ training = cfg.training
96
+ inference = get_attr(cfg, "inference", None)
97
+
98
+ resolved_data = resolve_path(data_dir or source.data_dir)
99
+ set_attr(source, "data_dir", str(resolved_data))
100
+
101
+ resolved_output = resolve_path(output_dir or training.output_dir)
102
+ set_attr(training, "output_dir", str(resolved_output))
103
+
104
+ model_path = get_attr(training, "model_path", None)
105
+ if model_path:
106
+ set_attr(training, "model_path", str(resolve_path(model_path)))
107
+
108
+ if inference is not None:
109
+ infer_output = get_attr(inference, "output_dir", "./result/output")
110
+ infer_checkpoint = checkpoint or get_attr(inference, "checkpoint", None)
111
+ set_attr(inference, "output_dir", str(resolve_path(infer_output)))
112
+ if infer_checkpoint:
113
+ set_attr(inference, "checkpoint", str(resolve_path(infer_checkpoint)))
114
+
115
+ return cfg
116
+
117
+
118
+ class SingleProcessManager:
119
+ rank = 0
120
+ local_rank = 0
121
+ world_size = 1
122
+ distributed = False
123
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
124
+
125
+
126
+ def initialize_distributed() -> Any:
127
+ if DistributedManager is None:
128
+ warnings.warn(
129
+ f"Falling back to a single-process manager: {DISTRIBUTED_IMPORT_ERROR}",
130
+ RuntimeWarning,
131
+ )
132
+ return SingleProcessManager()
133
+ DistributedManager.initialize()
134
+ return DistributedManager()
135
+
136
+
137
+ def cleanup_distributed() -> None:
138
+ if DistributedManager is not None and DistributedManager.is_initialized():
139
+ DistributedManager.cleanup()
140
+
141
+
142
+ class LocalPDEBenchFNODataset(Dataset):
143
+ """Small FNO-compatible reader used when optional OneScience graph deps are absent."""
144
+
145
+ def __init__(self, datapipe_cfg: Any, mode: str):
146
+ self.mode = mode
147
+ self.data_cfg = datapipe_cfg.data
148
+ self.source_cfg = datapipe_cfg.source
149
+ self.initial_step = int(self.data_cfg.initial_step)
150
+ self.reduced_resolution = int(self.data_cfg.reduced_resolution)
151
+ self.reduced_resolution_t = int(self.data_cfg.reduced_resolution_t)
152
+ self.reduced_batch = int(self.data_cfg.reduced_batch)
153
+ self.test_ratio = float(get_attr(self.data_cfg, "test_ratio", 0.1))
154
+ self.file_path = Path(self.source_cfg.data_dir) / self.source_cfg.file_name
155
+ self._load_single_file()
156
+
157
+ def _load_single_file(self) -> None:
158
+ if not self.file_path.is_file():
159
+ raise FileNotFoundError(f"HDF5 file not found: {self.file_path}")
160
+
161
+ with h5py.File(self.file_path, "r") as handle:
162
+ if "tensor" not in handle:
163
+ raise ValueError("local fallback datapipe requires HDF5 dataset 'tensor'")
164
+ tensor = np.asarray(handle["tensor"], dtype=np.float32)
165
+ if tensor.ndim == 3:
166
+ data = tensor[
167
+ :: self.reduced_batch,
168
+ :: self.reduced_resolution_t,
169
+ :: self.reduced_resolution,
170
+ ]
171
+ data = np.transpose(data, (0, 2, 1))
172
+ self.data = data[:, :, :, None]
173
+ x = np.asarray(handle["x-coordinate"], dtype=np.float32)
174
+ self.grid = torch.tensor(
175
+ x[:: self.reduced_resolution], dtype=torch.float32
176
+ ).unsqueeze(-1)
177
+ elif tensor.ndim == 4:
178
+ data = tensor[
179
+ :: self.reduced_batch,
180
+ :,
181
+ :: self.reduced_resolution,
182
+ :: self.reduced_resolution,
183
+ ]
184
+ data = np.transpose(data, (0, 2, 3, 1))
185
+ if "nu" in handle:
186
+ nu = np.asarray(handle["nu"], dtype=np.float32)[
187
+ :: self.reduced_batch,
188
+ :: self.reduced_resolution,
189
+ :: self.reduced_resolution,
190
+ ]
191
+ data = np.concatenate([nu[:, :, :, None], data], axis=-1)
192
+ self.data = data[:, :, :, :, None]
193
+ x = torch.tensor(np.asarray(handle["x-coordinate"], dtype=np.float32))
194
+ y = torch.tensor(np.asarray(handle["y-coordinate"], dtype=np.float32))
195
+ xx, yy = torch.meshgrid(x, y, indexing="ij")
196
+ self.grid = torch.stack((xx, yy), dim=-1)[
197
+ :: self.reduced_resolution, :: self.reduced_resolution
198
+ ].float()
199
+ else:
200
+ raise ValueError(f"unsupported tensor ndim for FNO fallback: {tensor.ndim}")
201
+
202
+ sample_count = self.data.shape[0]
203
+ val_count = max(1, int(sample_count * self.test_ratio)) if sample_count > 1 else 0
204
+ if self.mode == "train":
205
+ self.data = self.data[val_count:]
206
+ else:
207
+ self.data = self.data[:val_count]
208
+ self.data = torch.tensor(self.data, dtype=torch.float32)
209
+ self.spatial_dim = len(self.data.shape) - 3
210
+
211
+ def __len__(self) -> int:
212
+ return len(self.data)
213
+
214
+ def __getitem__(self, idx: int):
215
+ sample = self.data[idx]
216
+ return sample[..., : self.initial_step, :], sample, self.grid
217
+
218
+
219
+ class LocalPDEBenchFNODatapipe:
220
+ def __init__(self, cfg: Any, distributed: bool = False):
221
+ self.config = cfg
222
+ self.distributed = distributed
223
+ self.train_dataset = LocalPDEBenchFNODataset(cfg.datapipe, "train")
224
+ self.val_dataset = LocalPDEBenchFNODataset(cfg.datapipe, "val")
225
+ self.spatial_dim = self.train_dataset.spatial_dim
226
+
227
+ def train_dataloader(self):
228
+ loader_args = self.config.datapipe.dataloader
229
+ return DataLoader(
230
+ self.train_dataset,
231
+ batch_size=int(loader_args.batch_size),
232
+ num_workers=int(loader_args.num_workers),
233
+ pin_memory=bool(loader_args.pin_memory),
234
+ shuffle=True,
235
+ drop_last=True,
236
+ ), None
237
+
238
+ def val_dataloader(self):
239
+ loader_args = self.config.datapipe.dataloader
240
+ return DataLoader(
241
+ self.val_dataset,
242
+ batch_size=int(loader_args.batch_size),
243
+ num_workers=int(loader_args.num_workers),
244
+ pin_memory=bool(loader_args.pin_memory),
245
+ shuffle=False,
246
+ drop_last=False,
247
+ ), None
248
+
249
+
250
+ def build_datapipe(cfg: Any, distributed: bool = False, force_local: bool = False) -> Any:
251
+ if PDEBenchFNODatapipe is not None and not force_local:
252
+ return PDEBenchFNODatapipe(cfg, distributed=distributed)
253
+ if PDEBenchFNODatapipe is None:
254
+ warnings.warn(
255
+ f"Using local FNO HDF5 datapipe because OneScience datapipe import failed: "
256
+ f"{DATAPIPE_IMPORT_ERROR}",
257
+ RuntimeWarning,
258
+ )
259
+ return LocalPDEBenchFNODatapipe(cfg, distributed=distributed)
260
+
261
+
262
+ def build_model(spatial_dim: int, cfg: Any) -> torch.nn.Module:
263
+ model_args = cfg.model
264
+ data_cfg = cfg.datapipe.data
265
+ initial_step = int(data_cfg.initial_step)
266
+ pde_name = get_attr(data_cfg, "pde_name", "")
267
+ modes = int(model_args.modes)
268
+
269
+ if pde_name == "3D_Maxwell":
270
+ return FNO_maxwell(
271
+ num_channels=int(model_args.num_channels),
272
+ width=int(model_args.width),
273
+ modes1=modes,
274
+ modes2=modes,
275
+ modes3=modes,
276
+ initial_step=initial_step,
277
+ )
278
+ if spatial_dim == 1:
279
+ return FNO1d(
280
+ num_channels=int(model_args.num_channels),
281
+ width=int(model_args.width),
282
+ modes=modes,
283
+ initial_step=initial_step,
284
+ )
285
+ if spatial_dim == 2:
286
+ return FNO2d(
287
+ num_channels=int(model_args.num_channels),
288
+ width=int(model_args.width),
289
+ modes1=modes,
290
+ modes2=modes,
291
+ initial_step=initial_step,
292
+ )
293
+ if spatial_dim == 3:
294
+ return FNO3d(
295
+ num_channels=int(model_args.num_channels),
296
+ width=int(model_args.width),
297
+ modes1=modes,
298
+ modes2=modes,
299
+ modes3=modes,
300
+ initial_step=initial_step,
301
+ )
302
+ raise ValueError(f"unsupported spatial dimension: {spatial_dim}")
303
+
304
+
305
+ def predict_batch(model: torch.nn.Module, x: torch.Tensor, y: torch.Tensor, grid: torch.Tensor, cfg: Any):
306
+ data_cfg = cfg.datapipe.data
307
+ train_cfg = cfg.training
308
+ initial_step = int(data_cfg.initial_step)
309
+ t_train = min(int(train_cfg.t_train), y.shape[-2])
310
+ input_shape = list(x.shape)[:-2] + [-1]
311
+
312
+ if get_attr(train_cfg, "training_type", "single") == "autoregressive":
313
+ pred = y[..., :initial_step, :]
314
+ for _ in range(initial_step, t_train):
315
+ model_input = x.reshape(input_shape)
316
+ model_output = model(model_input, grid)
317
+ if model_output.dim() == pred.dim() - 1:
318
+ model_output = model_output.unsqueeze(-2)
319
+ pred = torch.cat((pred, model_output), dim=-2)
320
+ x = torch.cat((x[..., 1:, :], model_output), dim=-2)
321
+ return pred, y[..., :t_train, :]
322
+
323
+ model_input = x.reshape(input_shape)
324
+ target = y[..., t_train - 1 : t_train, :]
325
+ pred = model(model_input, grid)
326
+ if pred.dim() == target.dim() - 1:
327
+ pred = pred.unsqueeze(-2)
328
+ return pred, target
329
+
330
+
331
+ def load_model_state(path: Path, device: torch.device) -> dict[str, torch.Tensor]:
332
+ checkpoint = torch.load(path, map_location=device, weights_only=False)
333
+ if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
334
+ return checkpoint["model_state_dict"]
335
+ if isinstance(checkpoint, dict):
336
+ return checkpoint
337
+ raise ValueError(f"unsupported checkpoint format: {path}")
scripts/fake_data.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import h5py
7
+ import numpy as np
8
+
9
+ from common import DEFAULT_CONFIG, load_config, prepare_config
10
+
11
+
12
+ def generate_darcy_file(path: Path, samples: int, grid_size: int, seed: int, overwrite: bool) -> None:
13
+ if path.exists() and not overwrite:
14
+ print(f"Fake data already exists: {path}")
15
+ return
16
+
17
+ path.parent.mkdir(parents=True, exist_ok=True)
18
+ rng = np.random.default_rng(seed)
19
+ x = np.linspace(0.0, 1.0, grid_size, dtype=np.float32)
20
+ y = np.linspace(0.0, 1.0, grid_size, dtype=np.float32)
21
+ xx, yy = np.meshgrid(x, y, indexing="ij")
22
+
23
+ nu = rng.uniform(0.05, 0.25, size=(samples, grid_size, grid_size)).astype(np.float32)
24
+ tensor = np.empty((samples, 2, grid_size, grid_size), dtype=np.float32)
25
+ base = np.sin(np.pi * xx) * np.sin(np.pi * yy)
26
+ for idx in range(samples):
27
+ tensor[idx, 0] = base + 0.05 * rng.standard_normal(base.shape)
28
+ tensor[idx, 1] = tensor[idx, 0] * (1.0 + nu[idx]) + 0.01 * rng.standard_normal(base.shape)
29
+
30
+ with h5py.File(path, "w") as handle:
31
+ handle.create_dataset("tensor", data=tensor)
32
+ handle.create_dataset("nu", data=nu)
33
+ handle.create_dataset("x-coordinate", data=x)
34
+ handle.create_dataset("y-coordinate", data=y)
35
+ handle.create_dataset("t-coordinate", data=np.array([0.0, 1.0], dtype=np.float32))
36
+
37
+ print(f"Fake Darcy HDF5 written: {path}")
38
+ print(f"tensor={tensor.shape}, nu={nu.shape}")
39
+
40
+
41
+ def main() -> int:
42
+ parser = argparse.ArgumentParser(description="Generate a small PDENNEval FNO fake dataset.")
43
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
44
+ parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
45
+ parser.add_argument("--samples", type=int, default=20)
46
+ parser.add_argument("--grid-size", type=int, default=16)
47
+ parser.add_argument("--seed", type=int, default=0)
48
+ parser.add_argument("--overwrite", action="store_true")
49
+ args = parser.parse_args()
50
+
51
+ cfg = prepare_config(load_config(args.config), data_dir=args.data_dir)
52
+ data_dir = Path(cfg.datapipe.source.data_dir)
53
+ file_path = data_dir / cfg.datapipe.source.file_name
54
+ generate_darcy_file(file_path, args.samples, args.grid_size, args.seed, args.overwrite)
55
+ return 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main())
scripts/inference.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+
9
+ from common import (
10
+ DEFAULT_CONFIG,
11
+ build_datapipe,
12
+ build_model,
13
+ cleanup_distributed,
14
+ get_attr,
15
+ initialize_distributed,
16
+ load_config,
17
+ load_model_state,
18
+ predict_batch,
19
+ prepare_config,
20
+ )
21
+
22
+
23
+ @torch.no_grad()
24
+ def main() -> int:
25
+ parser = argparse.ArgumentParser(description="Run PDENNEval FNO inference.")
26
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
27
+ parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
28
+ parser.add_argument("--checkpoint", default=None, help="Checkpoint path")
29
+ parser.add_argument("--output-dir", default=None, help="Directory for npz predictions")
30
+ parser.add_argument("--max-batches", type=int, default=None)
31
+ parser.add_argument("--force-local-datapipe", action="store_true")
32
+ args = parser.parse_args()
33
+
34
+ dist = initialize_distributed()
35
+ device = dist.device
36
+ cfg = prepare_config(load_config(args.config), data_dir=args.data_dir, checkpoint=args.checkpoint)
37
+ if args.output_dir:
38
+ cfg.inference.output_dir = str(Path(args.output_dir).expanduser().resolve())
39
+
40
+ checkpoint = Path(args.checkpoint or cfg.inference.checkpoint)
41
+ if not checkpoint.is_file():
42
+ raise FileNotFoundError(f"checkpoint not found: {checkpoint}")
43
+
44
+ datapipe = build_datapipe(
45
+ cfg,
46
+ distributed=False,
47
+ force_local=args.force_local_datapipe,
48
+ )
49
+ val_loader, _ = datapipe.val_dataloader()
50
+ model = build_model(datapipe.spatial_dim, cfg).to(device)
51
+ model.load_state_dict(load_model_state(checkpoint, device))
52
+ model.eval()
53
+
54
+ output_dir = Path(args.output_dir or cfg.inference.output_dir)
55
+ output_dir.mkdir(parents=True, exist_ok=True)
56
+ max_batches = args.max_batches or int(get_attr(cfg.inference, "max_batches", 1))
57
+
58
+ written = 0
59
+ for batch_idx, (x, y, grid) in enumerate(val_loader):
60
+ if batch_idx >= max_batches:
61
+ break
62
+ x = x.to(device)
63
+ y = y.to(device)
64
+ grid = grid.to(device)
65
+ pred, target = predict_batch(model, x, y, grid, cfg)
66
+ path = output_dir / f"batch_{batch_idx:04d}.npz"
67
+ np.savez_compressed(
68
+ path,
69
+ prediction=pred.detach().cpu().numpy(),
70
+ target=target.detach().cpu().numpy(),
71
+ )
72
+ print(f"wrote {path}")
73
+ written += 1
74
+
75
+ if written == 0:
76
+ raise RuntimeError("no validation batches were available for inference")
77
+
78
+ cleanup_distributed()
79
+ return 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ raise SystemExit(main())
scripts/preflight_check.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import h5py
7
+
8
+ from common import DEFAULT_CONFIG, load_config, prepare_config
9
+
10
+
11
+ def main() -> int:
12
+ parser = argparse.ArgumentParser(description="Preflight the PDENNEval standard package.")
13
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
14
+ parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
15
+ args = parser.parse_args()
16
+
17
+ cfg = prepare_config(load_config(args.config), data_dir=args.data_dir)
18
+ data_path = Path(cfg.datapipe.source.data_dir) / cfg.datapipe.source.file_name
19
+ if not data_path.is_file():
20
+ raise FileNotFoundError(f"missing HDF5 data file: {data_path}")
21
+
22
+ with h5py.File(data_path, "r") as handle:
23
+ for key in ("tensor", "nu", "x-coordinate", "y-coordinate"):
24
+ if key not in handle:
25
+ raise ValueError(f"{data_path.name} missing required key: {key}")
26
+ if handle["tensor"].ndim != 4:
27
+ raise ValueError("2D Darcy fake/default config expects tensor ndim=4")
28
+ if handle["nu"].ndim != 3:
29
+ raise ValueError("2D Darcy fake/default config expects nu ndim=3")
30
+
31
+ output_dir = Path(cfg.training.output_dir)
32
+ output_dir.mkdir(parents=True, exist_ok=True)
33
+ probe = output_dir / ".preflight_write_test"
34
+ probe.write_text("ok", encoding="utf-8")
35
+ probe.unlink()
36
+ print(f"[OK] data file: {data_path}")
37
+ print(f"[OK] output directory writable: {output_dir}")
38
+ return 0
39
+
40
+
41
+ if __name__ == "__main__":
42
+ raise SystemExit(main())
scripts/result.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+
9
+ from common import DEFAULT_CONFIG, load_config, prepare_config
10
+
11
+
12
+ def main() -> int:
13
+ parser = argparse.ArgumentParser(description="Summarize PDENNEval FNO inference outputs.")
14
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
15
+ parser.add_argument("--prediction-dir", default=None)
16
+ parser.add_argument("--output-dir", default=None)
17
+ args = parser.parse_args()
18
+
19
+ cfg = prepare_config(load_config(args.config))
20
+ prediction_dir = Path(args.prediction_dir or cfg.result.prediction_dir).expanduser().resolve()
21
+ output_dir = Path(args.output_dir or cfg.result.output_dir).expanduser().resolve()
22
+ files = sorted(prediction_dir.glob("*.npz"))
23
+ if not files:
24
+ raise FileNotFoundError(f"no prediction files found in {prediction_dir}")
25
+
26
+ mse_values = []
27
+ mae_values = []
28
+ for path in files:
29
+ data = np.load(path)
30
+ pred = data["prediction"]
31
+ target = data["target"]
32
+ diff = pred - target
33
+ mse_values.append(float(np.mean(diff ** 2)))
34
+ mae_values.append(float(np.mean(np.abs(diff))))
35
+
36
+ metrics = {
37
+ "num_files": len(files),
38
+ "mse": float(np.mean(mse_values)),
39
+ "mae": float(np.mean(mae_values)),
40
+ }
41
+ output_dir.mkdir(parents=True, exist_ok=True)
42
+ metrics_path = output_dir / "metrics.json"
43
+ metrics_path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
44
+ print(json.dumps(metrics, indent=2))
45
+ print(f"wrote {metrics_path}")
46
+ return 0
47
+
48
+
49
+ if __name__ == "__main__":
50
+ raise SystemExit(main())
scripts/smoke_models.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from types import SimpleNamespace
6
+
7
+ import torch
8
+
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ if str(PROJECT_ROOT) not in sys.path:
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+
15
+ def check(name: str, tensor: torch.Tensor, expected_last_dim: int | None = None) -> None:
16
+ if not torch.isfinite(tensor).all():
17
+ raise RuntimeError(f"{name} produced non-finite values")
18
+ if expected_last_dim is not None and tensor.shape[-1] != expected_last_dim:
19
+ raise RuntimeError(
20
+ f"{name} last dim expected {expected_last_dim}, got {tensor.shape[-1]}"
21
+ )
22
+ print(f"[OK] {name}: shape={tuple(tensor.shape)}")
23
+
24
+
25
+ @torch.no_grad()
26
+ def smoke_deeponet() -> None:
27
+ from model.deeponet import DeepONetCartesianProd1D
28
+
29
+ model = DeepONetCartesianProd1D(
30
+ size=8,
31
+ in_channel_branch=1,
32
+ query_dim=1,
33
+ out_channel=1,
34
+ activation="relu",
35
+ base_model="MLP",
36
+ ).eval()
37
+ x_func = torch.randn(2, 8, 1)
38
+ x_loc = torch.linspace(0.0, 1.0, 8).unsqueeze(-1)
39
+ out = model((x_func, x_loc))
40
+ check("deeponet.DeepONetCartesianProd1D", out, expected_last_dim=1)
41
+
42
+
43
+ @torch.no_grad()
44
+ def smoke_fno() -> None:
45
+ from model.fno import FNO2d
46
+
47
+ model = FNO2d(num_channels=1, modes1=4, modes2=4, width=8, initial_step=1).eval()
48
+ x = torch.randn(2, 16, 16, 1)
49
+ coords = torch.linspace(0.0, 1.0, 16)
50
+ xx, yy = torch.meshgrid(coords, coords, indexing="ij")
51
+ grid = torch.stack((xx, yy), dim=-1).unsqueeze(0).repeat(2, 1, 1, 1)
52
+ out = model(x, grid)
53
+ check("fno.FNO2d", out, expected_last_dim=1)
54
+
55
+
56
+ @torch.no_grad()
57
+ def smoke_mpnn() -> None:
58
+ from torch_geometric.data import Data
59
+
60
+ from model.mpnn import MPNN
61
+
62
+ pde = SimpleNamespace(
63
+ name="smoke",
64
+ tmin=0.0,
65
+ tmax=1.0,
66
+ resolution_t=10,
67
+ spatial_domain=[(0.0, 1.0)],
68
+ resolution=[4],
69
+ spatial_dim=1,
70
+ variables={},
71
+ )
72
+ model = MPNN(pde=pde, time_window=10, hidden_features=128, hidden_layers=1).eval()
73
+ edge_index = torch.tensor(
74
+ [[0, 1, 2, 3, 1, 2, 3, 0], [1, 2, 3, 0, 0, 1, 2, 3]],
75
+ dtype=torch.long,
76
+ )
77
+ data = Data(
78
+ x=torch.randn(4, 10, 1),
79
+ x_pos=torch.linspace(0.0, 1.0, 4).unsqueeze(-1),
80
+ t_pos=torch.zeros(4),
81
+ edge_index=edge_index,
82
+ batch=torch.zeros(4, dtype=torch.long),
83
+ variables=torch.empty(4, 0),
84
+ )
85
+ out = model(data, v=0)
86
+ check("mpnn.MPNN", out)
87
+
88
+
89
+ @torch.no_grad()
90
+ def smoke_pino_fno() -> None:
91
+ from model.pino_fno import FNO1d
92
+
93
+ model = FNO1d(
94
+ modes=[4, 4],
95
+ width=4,
96
+ layers=[4, 4, 4],
97
+ fc_dim=8,
98
+ in_dim=2,
99
+ out_dim=1,
100
+ pad_ratio=[0.0, 0.0],
101
+ ).eval()
102
+ out = model(torch.randn(2, 16, 2))
103
+ check("pino_fno.FNO1d", out, expected_last_dim=1)
104
+
105
+
106
+ @torch.no_grad()
107
+ def smoke_unet() -> None:
108
+ from model.unet import UNet2d
109
+
110
+ model = UNet2d(in_channels=1, out_channels=1, init_features=2).eval()
111
+ out = model(torch.randn(1, 1, 16, 16))
112
+ check("unet.UNet2d", out)
113
+
114
+
115
+ @torch.no_grad()
116
+ def smoke_uno() -> None:
117
+ from model.uno import UNO1d
118
+
119
+ model = UNO1d(num_channels=1, width=4, initial_step=1).eval()
120
+ x = torch.randn(1, 64, 1)
121
+ grid = torch.linspace(0.0, 1.0, 64).view(1, 64, 1)
122
+ out = model(x, grid)
123
+ check("uno.UNO1d", out, expected_last_dim=1)
124
+
125
+
126
+ def main() -> int:
127
+ torch.manual_seed(0)
128
+ smoke_deeponet()
129
+ smoke_fno()
130
+ smoke_mpnn()
131
+ smoke_pino_fno()
132
+ smoke_unet()
133
+ smoke_uno()
134
+ return 0
135
+
136
+
137
+ if __name__ == "__main__":
138
+ raise SystemExit(main())
scripts/train.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch.nn.parallel import DistributedDataParallel as DDP
10
+
11
+ from common import (
12
+ DEFAULT_CONFIG,
13
+ build_datapipe,
14
+ build_model,
15
+ cleanup_distributed,
16
+ get_attr,
17
+ initialize_distributed,
18
+ load_config,
19
+ load_model_state,
20
+ predict_batch,
21
+ prepare_config,
22
+ )
23
+
24
+
25
+ def train_one_epoch(model, loader, optimizer, loss_fn, device, cfg) -> float:
26
+ model.train()
27
+ losses = []
28
+ for x, y, grid in loader:
29
+ x = x.to(device)
30
+ y = y.to(device)
31
+ grid = grid.to(device)
32
+ pred, target = predict_batch(model, x, y, grid, cfg)
33
+ if pred.numel() == 0:
34
+ continue
35
+ loss = loss_fn(pred.reshape(pred.size(0), -1), target.reshape(target.size(0), -1))
36
+ optimizer.zero_grad()
37
+ loss.backward()
38
+ optimizer.step()
39
+ losses.append(float(loss.item()))
40
+ return float(np.mean(losses)) if losses else 0.0
41
+
42
+
43
+ @torch.no_grad()
44
+ def validate(model, loader, loss_fn, device, cfg) -> float:
45
+ model.eval()
46
+ losses = []
47
+ for x, y, grid in loader:
48
+ x = x.to(device)
49
+ y = y.to(device)
50
+ grid = grid.to(device)
51
+ pred, target = predict_batch(model, x, y, grid, cfg)
52
+ if pred.numel() == 0:
53
+ continue
54
+ loss = loss_fn(pred.reshape(pred.size(0), -1), target.reshape(target.size(0), -1))
55
+ losses.append(float(loss.item()))
56
+ return float(np.mean(losses)) if losses else 0.0
57
+
58
+
59
+ def main() -> int:
60
+ parser = argparse.ArgumentParser(description="Train the standard PDENNEval FNO package.")
61
+ parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="Path to conf/config.yaml")
62
+ parser.add_argument("--data-dir", default=None, help="Override datapipe.source.data_dir")
63
+ parser.add_argument("--output-dir", default=None, help="Override training.output_dir")
64
+ parser.add_argument("--force-local-datapipe", action="store_true")
65
+ args = parser.parse_args()
66
+
67
+ dist = initialize_distributed()
68
+ device = dist.device
69
+ cfg = prepare_config(load_config(args.config), data_dir=args.data_dir, output_dir=args.output_dir)
70
+
71
+ seed = int(get_attr(cfg.training, "seed", 0))
72
+ torch.manual_seed(seed + dist.rank)
73
+ np.random.seed(seed + dist.rank)
74
+
75
+ output_dir = Path(cfg.training.output_dir)
76
+ if dist.rank == 0:
77
+ output_dir.mkdir(parents=True, exist_ok=True)
78
+ print(f"Output directory: {output_dir}")
79
+ print(f"Device: {device}, world_size={dist.world_size}")
80
+
81
+ datapipe = build_datapipe(
82
+ cfg,
83
+ distributed=(dist.world_size > 1),
84
+ force_local=args.force_local_datapipe,
85
+ )
86
+ train_loader, train_sampler = datapipe.train_dataloader()
87
+ val_loader, _ = datapipe.val_dataloader()
88
+
89
+ model = build_model(datapipe.spatial_dim, cfg).to(device)
90
+ if dist.world_size > 1:
91
+ model = DDP(model, device_ids=[dist.local_rank], output_device=dist.local_rank)
92
+
93
+ optimizer_cfg = cfg.training.optimizer
94
+ scheduler_cfg = cfg.training.scheduler
95
+ optimizer = getattr(torch.optim, optimizer_cfg.name)(
96
+ model.parameters(),
97
+ lr=float(optimizer_cfg.lr),
98
+ weight_decay=float(optimizer_cfg.weight_decay),
99
+ )
100
+ scheduler = getattr(torch.optim.lr_scheduler, scheduler_cfg.name)(
101
+ optimizer,
102
+ step_size=int(scheduler_cfg.step_size),
103
+ gamma=float(scheduler_cfg.gamma),
104
+ )
105
+ loss_fn = nn.MSELoss()
106
+
107
+ if bool(get_attr(cfg.training, "continue_training", False)) and cfg.training.model_path:
108
+ model_to_load = model.module if hasattr(model, "module") else model
109
+ model_to_load.load_state_dict(load_model_state(Path(cfg.training.model_path), device))
110
+
111
+ best_val = float("inf")
112
+ epochs = int(cfg.training.epochs)
113
+ save_period = max(1, int(cfg.training.save_period))
114
+
115
+ for epoch in range(epochs):
116
+ if train_sampler is not None:
117
+ train_sampler.set_epoch(epoch)
118
+ train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, cfg)
119
+ scheduler.step()
120
+ val_loss = validate(model, val_loader, loss_fn, device, cfg)
121
+
122
+ if dist.rank == 0:
123
+ print(f"epoch={epoch} train_loss={train_loss:.6e} val_loss={val_loss:.6e}")
124
+ model_to_save = model.module if hasattr(model, "module") else model
125
+ state = {
126
+ "epoch": epoch,
127
+ "model_state_dict": model_to_save.state_dict(),
128
+ "optimizer_state_dict": optimizer.state_dict(),
129
+ "loss": val_loss,
130
+ }
131
+ torch.save(state, output_dir / "latest_model.pt")
132
+ if (epoch + 1) % save_period == 0:
133
+ torch.save(state, output_dir / f"model_epoch_{epoch}.pt")
134
+ if val_loss <= best_val:
135
+ best_val = val_loss
136
+ torch.save(state, output_dir / "best_model.pt")
137
+
138
+ cleanup_distributed()
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())
weight/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+