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