Upload folder using huggingface_hub
Browse files- README.md +126 -0
- conf/config.yaml +52 -0
- configuration.json +1 -0
- model/__init__.py +3 -0
- model/deeponet.py +257 -0
- model/fno.py +412 -0
- model/mpnn.py +201 -0
- model/pino_fno.py +420 -0
- model/unet.py +255 -0
- model/uno.py +278 -0
- scripts/common.py +337 -0
- scripts/fake_data.py +59 -0
- scripts/inference.py +83 -0
- scripts/preflight_check.py +42 -0
- scripts/result.py +50 -0
- scripts/smoke_models.py +138 -0
- scripts/train.py +143 -0
- weight/.gitkeep +1 -0
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 |
+
|