Upload folder using huggingface_hub
Browse files- README.md +158 -0
- conf/config.yaml +78 -0
- configuration.json +1 -0
- model/__init__.py +181 -0
- model/act_fn.py +47 -0
- model/auto_deeponet.py +200 -0
- model/auto_deeponet_cnn.py +238 -0
- model/auto_edeeponet.py +185 -0
- model/auto_ffn.py +178 -0
- model/base_model.py +81 -0
- model/deeponet.py +257 -0
- model/ffn.py +181 -0
- model/fno/Adam.py +198 -0
- model/fno/__init__.py +0 -0
- model/fno/fno2d.py +238 -0
- model/fno/utilities3.py +364 -0
- model/loss.py +50 -0
- model/resnet.py +238 -0
- model/unet.py +149 -0
- scripts/fake_data.py +78 -0
- scripts/inference.py +145 -0
- scripts/result.py +58 -0
- scripts/train.py +211 -0
- scripts/train_auto.py +13 -0
- weight/.gitkeep +1 -0
README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- en
|
| 4 |
+
tags:
|
| 5 |
+
- OneScience
|
| 6 |
+
- fluid dynamics
|
| 7 |
+
- large-scale fluid dynamics benchmark
|
| 8 |
+
frameworks: PyTorch
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
<p align="center">
|
| 12 |
+
<strong>
|
| 13 |
+
<span style="font-size: 30px;">CFDBench</span>
|
| 14 |
+
</strong>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
# Model Overview
|
| 18 |
+
|
| 19 |
+
CFDBench is a large-scale benchmark developed by researchers at Tsinghua University to evaluate machine-learning methods for computational fluid dynamics. It focuses on model generalization across different boundary conditions, fluid properties, and geometric configurations.
|
| 20 |
+
|
| 21 |
+
Paper: [CFDBench: A Large-Scale Benchmark for Machine Learning Methods in Fluid Dynamics](https://arxiv.org/abs/2310.05963)
|
| 22 |
+
|
| 23 |
+
# Model Description
|
| 24 |
+
CFDBench is built from several representative computational fluid dynamics datasets spanning diverse boundary conditions, fluid properties, and geometries. It evaluates both flow-field prediction performance and the generalization capabilities of machine-learning methods.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Use Cases
|
| 28 |
+
|
| 29 |
+
| Use Case | Description |
|
| 30 |
+
| :---: | :---: |
|
| 31 |
+
| CFD surrogate benchmarking | Compare the flow-field prediction capabilities of neural operators and other deep-learning models using consistent data splits and metrics |
|
| 32 |
+
| Autoregressive flow evolution | Predict subsequent two-dimensional velocity fields step by step from the current grid-based field and assess error accumulation over multiple steps |
|
| 33 |
+
| Non-autoregressive field queries | Predict velocity directly at target locations from operating parameters and spatiotemporal coordinates, enabling evaluation over long time horizons |
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
## Supported Models
|
| 37 |
+
|
| 38 |
+
| Type | `root.model.name` | Training Entry Point |
|
| 39 |
+
| :--- | :--- | :--- |
|
| 40 |
+
| Non-autoregressive | `ffn` | `python scripts/train.py` |
|
| 41 |
+
| Non-autoregressive | `deeponet` | `python scripts/train.py` |
|
| 42 |
+
| Autoregressive | `auto_ffn` | `python scripts/train_auto.py` |
|
| 43 |
+
| Autoregressive | `auto_deeponet` | `python scripts/train_auto.py` |
|
| 44 |
+
| Autoregressive | `auto_edeeponet` | `python scripts/train_auto.py` |
|
| 45 |
+
| Autoregressive | `auto_deeponet_cnn` | `python scripts/train_auto.py` |
|
| 46 |
+
| Autoregressive | `resnet` | `python scripts/train_auto.py` |
|
| 47 |
+
| Autoregressive | `unet` | `python scripts/train_auto.py` |
|
| 48 |
+
| Autoregressive | `fno` | `python scripts/train_auto.py` |
|
| 49 |
+
|
| 50 |
+
# Usage
|
| 51 |
+
|
| 52 |
+
## 1. OneCode
|
| 53 |
+
|
| 54 |
+
Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
|
| 55 |
+
|
| 56 |
+
[Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 57 |
+
|
| 58 |
+
## 2. Manual Setup
|
| 59 |
+
|
| 60 |
+
**Hardware Requirements**
|
| 61 |
+
|
| 62 |
+
- A GPU or DCU is recommended.
|
| 63 |
+
- A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
|
| 64 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
|
| 65 |
+
|
| 66 |
+
### Download the Model Package
|
| 67 |
+
```
|
| 68 |
+
modelscope download --model OneScience/CFDBench --local_dir ./CFDBench
|
| 69 |
+
cd CFDBench
|
| 70 |
+
```
|
| 71 |
+
### Set Up the Runtime Environment
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
**DCU Environment**
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Activate DTK and Conda first
|
| 78 |
+
conda create -n onescience311 python=3.11 -y
|
| 79 |
+
conda activate onescience311
|
| 80 |
+
# Installation with uv is also supported
|
| 81 |
+
pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
**GPU Environment**
|
| 85 |
+
```bash
|
| 86 |
+
# Activate Conda first
|
| 87 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 88 |
+
conda activate onescience311
|
| 89 |
+
# Installation with uv is also supported
|
| 90 |
+
pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Training Data
|
| 94 |
+
|
| 95 |
+
The OneScience community provides the `cfdbench` dataset for training. Download it with the command below and verify that the data path in `config/config.yaml` is configured correctly:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
modelscope download --dataset OneScience/cfdbench --local_dir ./data
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### Training
|
| 102 |
+
**Autoregressive Training**
|
| 103 |
+
|
| 104 |
+
The default configuration sets `root.model.name: fno`, so use the autoregressive training entry point:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
python scripts/train_auto.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
To select another autoregressive model, edit `conf/config.yaml`:
|
| 112 |
+
|
| 113 |
+
```yaml
|
| 114 |
+
root:
|
| 115 |
+
model:
|
| 116 |
+
name: "auto_ffn"
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
**Non-autoregressive Training**
|
| 120 |
+
```bash
|
| 121 |
+
python scripts/train.py --model deeponet
|
| 122 |
+
```
|
| 123 |
+
### Model Weights
|
| 124 |
+
This repository will provide pretrained CFDBench model weights in the `weights/` directory. The weights will be uploaded soon.
|
| 125 |
+
|
| 126 |
+
### Inference and Visualization
|
| 127 |
+
|
| 128 |
+
The inference script automatically selects the task type from the current model name and reads the following path by default:
|
| 129 |
+
|
| 130 |
+
```text
|
| 131 |
+
./weight/<model.name>.pt
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
Default FNO inference and visualization:
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
python scripts/inference.py
|
| 138 |
+
python scripts/result.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Non-autoregressive models can likewise be selected through a command-line argument:
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
python scripts/inference.py --model ffn
|
| 145 |
+
python scripts/result.py --model ffn
|
| 146 |
+
```
|
| 147 |
+
# Official OneScience Resources
|
| 148 |
+
|
| 149 |
+
| Platform | OneScience Repository | Skills Repository |
|
| 150 |
+
| --- | --- | --- |
|
| 151 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 152 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 153 |
+
|
| 154 |
+
# Citations and License
|
| 155 |
+
|
| 156 |
+
- Original CFDBench paper: [CFDBench: A Large-Scale Benchmark for Machine Learning Methods in Fluid Dynamics](https://arxiv.org/abs/2310.05963)
|
| 157 |
+
- Original CFDBench code repository: https://github.com/luo-yining/CFDBench
|
| 158 |
+
- This repository retains source attribution and has been adapted for automated execution through OneScience and ModelScope. Before public redistribution, verify the applicable licensing requirements of the upstream project.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
root:
|
| 2 |
+
datapipe:
|
| 3 |
+
source:
|
| 4 |
+
data_dir: "./data/cfdbench"
|
| 5 |
+
data_name: "tube_prop_bc_geo"
|
| 6 |
+
verbose: true
|
| 7 |
+
|
| 8 |
+
data:
|
| 9 |
+
task_type: "auto"
|
| 10 |
+
norm_props: true
|
| 11 |
+
norm_bc: true
|
| 12 |
+
delta_time: 0.1
|
| 13 |
+
stable_state_diff: 0.001
|
| 14 |
+
split_ratios: [0.7, 0.15, 0.15]
|
| 15 |
+
seed: 0
|
| 16 |
+
num_rows: 8
|
| 17 |
+
num_cols: 8
|
| 18 |
+
|
| 19 |
+
dataloader:
|
| 20 |
+
batch_size: 2
|
| 21 |
+
eval_batch_size: 2
|
| 22 |
+
num_workers: 0
|
| 23 |
+
|
| 24 |
+
model:
|
| 25 |
+
name: "fno"
|
| 26 |
+
num_label_samples: 64
|
| 27 |
+
in_chan: 2
|
| 28 |
+
out_chan: 2
|
| 29 |
+
ffn_depth: 2
|
| 30 |
+
ffn_width: 64
|
| 31 |
+
autoffn_depth: 2
|
| 32 |
+
autoffn_width: 64
|
| 33 |
+
deeponet_width: 64
|
| 34 |
+
branch_depth: 2
|
| 35 |
+
trunk_depth: 2
|
| 36 |
+
act_fn: "relu"
|
| 37 |
+
act_scale_invariant: true
|
| 38 |
+
act_on_output: false
|
| 39 |
+
autoedeeponet_width: 64
|
| 40 |
+
autoedeeponet_depth: 2
|
| 41 |
+
autoedeeponet_act_fn: "relu"
|
| 42 |
+
fno_depth: 2
|
| 43 |
+
fno_hidden_dim: 16
|
| 44 |
+
fno_modes_x: 4
|
| 45 |
+
fno_modes_y: 4
|
| 46 |
+
unet_dim: 8
|
| 47 |
+
unet_insert_case_params_at: "input"
|
| 48 |
+
resnet_depth: 2
|
| 49 |
+
resnet_hidden_chan: 16
|
| 50 |
+
resnet_kernel_size: 7
|
| 51 |
+
resnet_padding: 3
|
| 52 |
+
|
| 53 |
+
training:
|
| 54 |
+
output_dir: "./result"
|
| 55 |
+
mode: "train_test"
|
| 56 |
+
group_by_model: true
|
| 57 |
+
device: "cpu"
|
| 58 |
+
num_epochs: 1
|
| 59 |
+
lr: 1.0e-3
|
| 60 |
+
lr_step_size: 20
|
| 61 |
+
lr_gamma: 0.9
|
| 62 |
+
log_interval: 1
|
| 63 |
+
eval_interval: 1
|
| 64 |
+
loss_name: "nmse"
|
| 65 |
+
checkpoint_name: "auto"
|
| 66 |
+
|
| 67 |
+
inference:
|
| 68 |
+
checkpoint_path: "auto"
|
| 69 |
+
output_dir: "./result/inference"
|
| 70 |
+
group_by_model: true
|
| 71 |
+
device: "cpu"
|
| 72 |
+
max_batches: 2
|
| 73 |
+
plot_interval: 1
|
| 74 |
+
|
| 75 |
+
fake_data:
|
| 76 |
+
num_cases_per_subset: 4
|
| 77 |
+
num_frames: 4
|
| 78 |
+
seed: 123
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other","model":"CFDBench","domain":"cfd"}
|
model/__init__.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from model.auto_deeponet import AutoDeepONet
|
| 6 |
+
from model.auto_deeponet_cnn import AutoDeepONetCnn
|
| 7 |
+
from model.auto_edeeponet import AutoEDeepONet
|
| 8 |
+
from model.auto_ffn import AutoFfn
|
| 9 |
+
from model.deeponet import DeepONet
|
| 10 |
+
from model.ffn import FfnModel
|
| 11 |
+
from model.fno.fno2d import Fno2d
|
| 12 |
+
from model.loss import loss_name_to_fn
|
| 13 |
+
from model.resnet import ResNet
|
| 14 |
+
from model.unet import UNet
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
AUTO_MODEL_NAMES = {
|
| 18 |
+
"auto_ffn",
|
| 19 |
+
"auto_deeponet",
|
| 20 |
+
"auto_edeeponet",
|
| 21 |
+
"auto_deeponet_cnn",
|
| 22 |
+
"resnet",
|
| 23 |
+
"unet",
|
| 24 |
+
"fno",
|
| 25 |
+
}
|
| 26 |
+
STATIC_MODEL_NAMES = {"ffn", "deeponet"}
|
| 27 |
+
ALL_MODEL_NAMES = AUTO_MODEL_NAMES | STATIC_MODEL_NAMES
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def infer_task_type(model_name: str) -> str:
|
| 31 |
+
if model_name in AUTO_MODEL_NAMES:
|
| 32 |
+
return "auto"
|
| 33 |
+
if model_name in STATIC_MODEL_NAMES:
|
| 34 |
+
return "static"
|
| 35 |
+
raise ValueError(f"Unknown CFDBench model.name={model_name!r}. Available: {sorted(ALL_MODEL_NAMES)}")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_input_shapes(data_name: str, num_rows: int, num_cols: int) -> Tuple[int, int, int]:
|
| 39 |
+
if any(name in data_name for name in ("tube", "dam", "cylinder")):
|
| 40 |
+
n_rows = num_rows + 2
|
| 41 |
+
n_cols = num_cols + 1
|
| 42 |
+
elif "cavity" in data_name:
|
| 43 |
+
n_rows = num_rows
|
| 44 |
+
n_cols = num_cols
|
| 45 |
+
else:
|
| 46 |
+
raise ValueError(f"Unknown CFDBench problem in data_name: {data_name}")
|
| 47 |
+
|
| 48 |
+
if "cylinder" in data_name:
|
| 49 |
+
n_case_params = 8
|
| 50 |
+
elif any(name in data_name for name in ("cavity", "tube", "dam")):
|
| 51 |
+
n_case_params = 5
|
| 52 |
+
else:
|
| 53 |
+
raise ValueError(f"Unknown CFDBench parameter set in data_name: {data_name}")
|
| 54 |
+
|
| 55 |
+
return n_rows, n_cols, n_case_params
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_model(cfg) -> torch.nn.Module:
|
| 59 |
+
model_cfg = cfg.model
|
| 60 |
+
data_cfg = cfg.datapipe.data
|
| 61 |
+
source_cfg = cfg.datapipe.source
|
| 62 |
+
train_cfg = cfg.training
|
| 63 |
+
loss_fn = loss_name_to_fn(train_cfg.loss_name)
|
| 64 |
+
n_rows, n_cols, n_case_params = get_input_shapes(
|
| 65 |
+
data_name=source_cfg.data_name,
|
| 66 |
+
num_rows=data_cfg.num_rows,
|
| 67 |
+
num_cols=data_cfg.num_cols,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
name = model_cfg.name
|
| 71 |
+
if name == "ffn":
|
| 72 |
+
widths = [n_case_params + 3] + [model_cfg.ffn_width] * model_cfg.ffn_depth + [1]
|
| 73 |
+
return FfnModel(
|
| 74 |
+
widths=widths,
|
| 75 |
+
loss_fn=loss_fn,
|
| 76 |
+
act_name=model_cfg.act_fn,
|
| 77 |
+
act_norm=model_cfg.act_scale_invariant,
|
| 78 |
+
act_on_output=model_cfg.act_on_output,
|
| 79 |
+
num_label_samples=model_cfg.num_label_samples,
|
| 80 |
+
)
|
| 81 |
+
if name == "deeponet":
|
| 82 |
+
return DeepONet(
|
| 83 |
+
branch_dim=n_case_params,
|
| 84 |
+
trunk_dim=3,
|
| 85 |
+
loss_fn=loss_fn,
|
| 86 |
+
width=model_cfg.deeponet_width,
|
| 87 |
+
trunk_depth=model_cfg.trunk_depth,
|
| 88 |
+
branch_depth=model_cfg.branch_depth,
|
| 89 |
+
act_name=model_cfg.act_fn,
|
| 90 |
+
act_norm=model_cfg.act_scale_invariant,
|
| 91 |
+
act_on_output=model_cfg.act_on_output,
|
| 92 |
+
num_label_samples=model_cfg.num_label_samples,
|
| 93 |
+
)
|
| 94 |
+
if name == "auto_ffn":
|
| 95 |
+
return AutoFfn(
|
| 96 |
+
input_field_dim=n_rows * n_cols,
|
| 97 |
+
num_case_params=n_case_params,
|
| 98 |
+
query_dim=2,
|
| 99 |
+
loss_fn=loss_fn,
|
| 100 |
+
width=model_cfg.autoffn_width,
|
| 101 |
+
depth=model_cfg.autoffn_depth,
|
| 102 |
+
act_name=model_cfg.act_fn,
|
| 103 |
+
act_norm=model_cfg.act_scale_invariant,
|
| 104 |
+
num_label_samples=model_cfg.num_label_samples,
|
| 105 |
+
)
|
| 106 |
+
if name == "auto_deeponet":
|
| 107 |
+
return AutoDeepONet(
|
| 108 |
+
branch_dim=n_rows * n_cols + n_case_params,
|
| 109 |
+
trunk_dim=2,
|
| 110 |
+
loss_fn=loss_fn,
|
| 111 |
+
width=model_cfg.deeponet_width,
|
| 112 |
+
trunk_depth=model_cfg.trunk_depth,
|
| 113 |
+
branch_depth=model_cfg.branch_depth,
|
| 114 |
+
act_name=model_cfg.act_fn,
|
| 115 |
+
act_norm=model_cfg.act_scale_invariant,
|
| 116 |
+
act_on_output=model_cfg.act_on_output,
|
| 117 |
+
num_label_samples=model_cfg.num_label_samples,
|
| 118 |
+
)
|
| 119 |
+
if name == "auto_edeeponet":
|
| 120 |
+
return AutoEDeepONet(
|
| 121 |
+
dim_branch1=n_rows * n_cols,
|
| 122 |
+
dim_branch2=n_case_params,
|
| 123 |
+
trunk_dim=2,
|
| 124 |
+
loss_fn=loss_fn,
|
| 125 |
+
width=model_cfg.autoedeeponet_width,
|
| 126 |
+
trunk_depth=model_cfg.autoedeeponet_depth,
|
| 127 |
+
branch_depth=model_cfg.autoedeeponet_depth,
|
| 128 |
+
act_name=model_cfg.autoedeeponet_act_fn,
|
| 129 |
+
num_label_samples=model_cfg.num_label_samples,
|
| 130 |
+
)
|
| 131 |
+
if name == "auto_deeponet_cnn":
|
| 132 |
+
if n_rows // 16 != 4 or n_cols // 16 != 4:
|
| 133 |
+
raise ValueError(
|
| 134 |
+
"auto_deeponet_cnn expects the post-padding grid to reduce to 4x4 after four 2x pools. "
|
| 135 |
+
"For tube/dam/cylinder fake data, set datapipe.data.num_rows=64 and num_cols=64."
|
| 136 |
+
)
|
| 137 |
+
return AutoDeepONetCnn(
|
| 138 |
+
in_chan=model_cfg.in_chan,
|
| 139 |
+
height=n_rows,
|
| 140 |
+
width=n_cols,
|
| 141 |
+
num_case_params=n_case_params,
|
| 142 |
+
query_dim=2,
|
| 143 |
+
loss_fn=loss_fn,
|
| 144 |
+
trunk_depth=model_cfg.trunk_depth,
|
| 145 |
+
act_name=model_cfg.act_fn,
|
| 146 |
+
act_norm=model_cfg.act_scale_invariant,
|
| 147 |
+
act_on_output=model_cfg.act_on_output,
|
| 148 |
+
)
|
| 149 |
+
if name == "resnet":
|
| 150 |
+
return ResNet(
|
| 151 |
+
in_chan=model_cfg.in_chan,
|
| 152 |
+
out_chan=model_cfg.out_chan,
|
| 153 |
+
n_case_params=n_case_params,
|
| 154 |
+
loss_fn=loss_fn,
|
| 155 |
+
hidden_chan=model_cfg.resnet_hidden_chan,
|
| 156 |
+
num_blocks=model_cfg.resnet_depth,
|
| 157 |
+
kernel_size=model_cfg.resnet_kernel_size,
|
| 158 |
+
padding=model_cfg.resnet_padding,
|
| 159 |
+
)
|
| 160 |
+
if name == "unet":
|
| 161 |
+
return UNet(
|
| 162 |
+
in_chan=model_cfg.in_chan,
|
| 163 |
+
out_chan=model_cfg.out_chan,
|
| 164 |
+
loss_fn=loss_fn,
|
| 165 |
+
n_case_params=n_case_params,
|
| 166 |
+
insert_case_params_at=model_cfg.unet_insert_case_params_at,
|
| 167 |
+
dim=model_cfg.unet_dim,
|
| 168 |
+
)
|
| 169 |
+
if name == "fno":
|
| 170 |
+
return Fno2d(
|
| 171 |
+
in_chan=model_cfg.in_chan,
|
| 172 |
+
out_chan=model_cfg.out_chan,
|
| 173 |
+
n_case_params=n_case_params,
|
| 174 |
+
loss_fn=loss_fn,
|
| 175 |
+
num_layers=model_cfg.fno_depth,
|
| 176 |
+
hidden_dim=model_cfg.fno_hidden_dim,
|
| 177 |
+
modes1=model_cfg.fno_modes_x,
|
| 178 |
+
modes2=model_cfg.fno_modes_y,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
raise ValueError(f"Invalid CFDBench model.name={name!r}. Available: {sorted(ALL_MODEL_NAMES)}")
|
model/act_fn.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
from torch import Tensor
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_act_fn(name: str, norm: bool = False) -> nn.Module:
|
| 6 |
+
if name == "relu":
|
| 7 |
+
fn = nn.ReLU()
|
| 8 |
+
elif name == "tanh":
|
| 9 |
+
fn = nn.Tanh()
|
| 10 |
+
elif name == "gelu":
|
| 11 |
+
fn = nn.GELU()
|
| 12 |
+
elif name == "swish":
|
| 13 |
+
fn = nn.SiLU()
|
| 14 |
+
else:
|
| 15 |
+
raise ValueError(f"Unknown activation function: {name}")
|
| 16 |
+
if norm:
|
| 17 |
+
fn = NormAct(fn)
|
| 18 |
+
return fn
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class NormAct(nn.Module):
|
| 22 |
+
"""
|
| 23 |
+
Normalized Activation Function.
|
| 24 |
+
|
| 25 |
+
A wrapper around any activation function that normalizes the input
|
| 26 |
+
before applying the activation function, and then transforms the
|
| 27 |
+
output back to the original scale.
|
| 28 |
+
"""
|
| 29 |
+
def __init__(self, act_fn: nn.Module):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.act_fn = act_fn
|
| 32 |
+
|
| 33 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 34 |
+
'''
|
| 35 |
+
x: (b, h, w)
|
| 36 |
+
'''
|
| 37 |
+
num_dims = len(x.shape)
|
| 38 |
+
dims = tuple(range(1, num_dims))
|
| 39 |
+
# find the mean and std of each example in the batch
|
| 40 |
+
mean = x.mean(dim=dims, keepdim=True)
|
| 41 |
+
std = x.std(dim=dims, keepdim=True)
|
| 42 |
+
# normalize
|
| 43 |
+
x = (x - mean) / std
|
| 44 |
+
x = self.act_fn(x)
|
| 45 |
+
# Transform back to the original scale
|
| 46 |
+
x = x * std + mean
|
| 47 |
+
return x
|
model/auto_deeponet.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding: utf8
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
Contains the implementation of autoregressive DeepONet
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from itertools import product
|
| 8 |
+
from typing import List, Optional
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn, Tensor
|
| 12 |
+
|
| 13 |
+
from .ffn import Ffn
|
| 14 |
+
from .base_model import AutoCfdModel
|
| 15 |
+
from .act_fn import get_act_fn
|
| 16 |
+
from .loss import MseLoss
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AutoDeepONet(AutoCfdModel):
|
| 20 |
+
"""
|
| 21 |
+
Auto-regressive DeepONet for CFD.
|
| 22 |
+
|
| 23 |
+
Our task is different from the one that the original DeepONet. In the
|
| 24 |
+
original DeepONet, the input function (input to the branch net)
|
| 25 |
+
is the initial condition (IC), but here, we have a fixed (zero) IC.
|
| 26 |
+
Instead, we have different boundary conditions (BCs), but we also
|
| 27 |
+
want the model to predict the next time step given the current time step.
|
| 28 |
+
Ideally, we should have two different branch nets, one accepting the
|
| 29 |
+
BCs, one accepting the current time step.
|
| 30 |
+
|
| 31 |
+
Here, we assume that the current time step includes the information about
|
| 32 |
+
BCs (which are the values on the bounds), so we just feed
|
| 33 |
+
the current time step to one branch net.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
branch_dim: int,
|
| 39 |
+
trunk_dim: int,
|
| 40 |
+
loss_fn: MseLoss,
|
| 41 |
+
num_label_samples: int = 1000,
|
| 42 |
+
branch_depth: int = 4,
|
| 43 |
+
trunk_depth: int = 4,
|
| 44 |
+
width: int = 100,
|
| 45 |
+
act_name="relu",
|
| 46 |
+
act_norm: bool = False,
|
| 47 |
+
act_on_output: bool = False,
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Args:
|
| 51 |
+
- branch_dim: int, the dimension of the branch net input.
|
| 52 |
+
- trunk_dim: int, the dimension of the trunk net input.
|
| 53 |
+
"""
|
| 54 |
+
super().__init__(loss_fn)
|
| 55 |
+
self.branch_dim = branch_dim
|
| 56 |
+
self.trunk_dim = trunk_dim
|
| 57 |
+
self.branch_depth = branch_depth
|
| 58 |
+
self.trunk_depth = trunk_depth
|
| 59 |
+
self.width = width
|
| 60 |
+
self.act_name = act_name
|
| 61 |
+
self.act_norm = act_norm
|
| 62 |
+
self.act_on_output = act_on_output
|
| 63 |
+
self.num_label_samples = num_label_samples
|
| 64 |
+
|
| 65 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 66 |
+
self.branch_dims = [branch_dim] + [width] * branch_depth
|
| 67 |
+
self.trunk_dims = [trunk_dim] + [width] * trunk_depth
|
| 68 |
+
self.branch_net = Ffn(
|
| 69 |
+
self.branch_dims,
|
| 70 |
+
act_fn=act_fn,
|
| 71 |
+
act_on_output=act_on_output,
|
| 72 |
+
)
|
| 73 |
+
self.trunk_net = Ffn(self.trunk_dims, act_fn=act_fn)
|
| 74 |
+
self.bias = nn.Parameter(torch.zeros(1)) # type: ignore
|
| 75 |
+
|
| 76 |
+
def forward(
|
| 77 |
+
self,
|
| 78 |
+
inputs: Tensor,
|
| 79 |
+
case_params: Tensor,
|
| 80 |
+
label: Optional[Tensor] = None,
|
| 81 |
+
mask: Optional[Tensor] = None,
|
| 82 |
+
query_idxs: Optional[Tensor] = None,
|
| 83 |
+
):
|
| 84 |
+
"""
|
| 85 |
+
Here, we just randomly sample some points, and use the label values on
|
| 86 |
+
those points as the label.
|
| 87 |
+
|
| 88 |
+
### Args
|
| 89 |
+
- inputs: (b, c, h, w)
|
| 90 |
+
- case_params: (b, p)
|
| 91 |
+
- labels: (b, c, h, w)
|
| 92 |
+
- query_point: (k, 2), k is the number of query points, each is
|
| 93 |
+
an (x, y) coordinate.
|
| 94 |
+
- masks: For future use.
|
| 95 |
+
|
| 96 |
+
### Returns
|
| 97 |
+
Output: Tensor, if query_points is not None, the shape is (b, k).
|
| 98 |
+
Else, the shape is (b, c, h, w).
|
| 99 |
+
|
| 100 |
+
Notations:
|
| 101 |
+
- b: batch size
|
| 102 |
+
- c: number of channels
|
| 103 |
+
- h: height
|
| 104 |
+
- w: width
|
| 105 |
+
- p: number of case parameters
|
| 106 |
+
- k: number of query points
|
| 107 |
+
"""
|
| 108 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 109 |
+
# Only use the u channel
|
| 110 |
+
inputs = inputs[:, 0] # (B, h, w)
|
| 111 |
+
# Flatten
|
| 112 |
+
flat_inputs = inputs.view(batch_size, -1) # (B, h * w)
|
| 113 |
+
|
| 114 |
+
# Simple prepend physical properties to the input field.
|
| 115 |
+
# (B, h * w + 2)
|
| 116 |
+
flat_inputs = torch.cat([flat_inputs, case_params], dim=1)
|
| 117 |
+
x_branch = self.branch_net(flat_inputs)
|
| 118 |
+
|
| 119 |
+
if query_idxs is None:
|
| 120 |
+
query_idxs = torch.tensor(
|
| 121 |
+
list(product(range(height), range(width))),
|
| 122 |
+
dtype=torch.long,
|
| 123 |
+
device=flat_inputs.device,
|
| 124 |
+
) # (h * w, 2)
|
| 125 |
+
|
| 126 |
+
# Input to the trunk net
|
| 127 |
+
x_trunk = (query_idxs.float() - 50) / 100 # (k, 2)
|
| 128 |
+
x_trunk = self.trunk_net(x_trunk) # (k, p)
|
| 129 |
+
x_trunk = x_trunk.unsqueeze(0) # (1, k, p)
|
| 130 |
+
x_branch = x_branch.unsqueeze(1) # (b, 1, p)
|
| 131 |
+
preds = torch.sum(x_branch * x_trunk, dim=-1) + self.bias # (b, k)
|
| 132 |
+
|
| 133 |
+
# Use values of the input field at query points as residuals
|
| 134 |
+
residuals = inputs[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 135 |
+
preds += residuals
|
| 136 |
+
|
| 137 |
+
if label is not None:
|
| 138 |
+
label = label[:, 0] # (B, 1, h, w) # Predict only u
|
| 139 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 140 |
+
loss = self.loss_fn(labels=labels, preds=preds) # (b, k)
|
| 141 |
+
return dict(
|
| 142 |
+
preds=preds,
|
| 143 |
+
loss=loss,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 147 |
+
return dict(preds=preds)
|
| 148 |
+
|
| 149 |
+
def generate(
|
| 150 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor
|
| 151 |
+
) -> Tensor:
|
| 152 |
+
"""
|
| 153 |
+
x: (c, h, w) or (B, c, h, w)
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
(b, c, h, w)
|
| 157 |
+
"""
|
| 158 |
+
if inputs.dim() == 3:
|
| 159 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 160 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 161 |
+
query_idxs = torch.tensor(
|
| 162 |
+
list(product(range(height), range(width))),
|
| 163 |
+
dtype=torch.long,
|
| 164 |
+
device=inputs.device,
|
| 165 |
+
) # (h * w, 2)
|
| 166 |
+
# query_points = query_points / 100
|
| 167 |
+
# (b, 1, h * w)
|
| 168 |
+
preds = self.forward(
|
| 169 |
+
inputs, case_params=case_params, query_idxs=query_idxs, mask=mask
|
| 170 |
+
)["preds"]
|
| 171 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 172 |
+
return preds
|
| 173 |
+
|
| 174 |
+
def generate_many(
|
| 175 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int
|
| 176 |
+
) -> List[Tensor]:
|
| 177 |
+
"""
|
| 178 |
+
Args:
|
| 179 |
+
x: (c, h, w) or (B, c, h, w)
|
| 180 |
+
mask: (h, w). 1 for interior, 0 for boundaries.
|
| 181 |
+
steps: int, number of steps to generate.
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
list of tensors, each of shape (b, c, h, w)
|
| 185 |
+
"""
|
| 186 |
+
assert len(inputs.shape) == len(case_params.shape) + 2
|
| 187 |
+
if inputs.dim() == 3:
|
| 188 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 189 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 190 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 191 |
+
assert inputs.shape[0] == case_params.shape[0]
|
| 192 |
+
cur_frame = inputs
|
| 193 |
+
preds = []
|
| 194 |
+
for _ in range(steps):
|
| 195 |
+
# (b, c, h, w)
|
| 196 |
+
cur_frame = self.generate(
|
| 197 |
+
inputs=cur_frame, case_params=case_params, mask=mask
|
| 198 |
+
)
|
| 199 |
+
preds.append(cur_frame)
|
| 200 |
+
return preds
|
model/auto_deeponet_cnn.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import product
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn, Tensor
|
| 6 |
+
|
| 7 |
+
from .ffn import Ffn
|
| 8 |
+
from .base_model import AutoCfdModel
|
| 9 |
+
from .act_fn import get_act_fn
|
| 10 |
+
from .loss import MseLoss
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class CnnBranch(nn.Module):
|
| 14 |
+
def __init__(
|
| 15 |
+
self, in_chan: int, kernel_size: int, padding: int, depth: int = 4
|
| 16 |
+
):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.in_chan = in_chan
|
| 19 |
+
self.in_conv = nn.Conv2d(
|
| 20 |
+
in_chan, 32, kernel_size=kernel_size, padding=padding
|
| 21 |
+
)
|
| 22 |
+
self.out_conv = nn.Conv2d(
|
| 23 |
+
32, 32, kernel_size=kernel_size, padding=padding
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
blocks = []
|
| 27 |
+
for i in range(depth):
|
| 28 |
+
blocks += [
|
| 29 |
+
nn.Conv2d(32, 32, kernel_size, padding=padding),
|
| 30 |
+
nn.MaxPool2d(2),
|
| 31 |
+
nn.ReLU(),
|
| 32 |
+
]
|
| 33 |
+
self.blocks = nn.Sequential(*blocks)
|
| 34 |
+
|
| 35 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 36 |
+
x = self.in_conv(x) # (b, 16, h, w)
|
| 37 |
+
x = self.blocks(x) # (b, 32, h/16=4, w/16=4)
|
| 38 |
+
x = self.out_conv(x) # (b, 32, 4, 4)
|
| 39 |
+
return x
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class AutoDeepONetCnn(AutoCfdModel):
|
| 43 |
+
"""
|
| 44 |
+
Auto-regressive DeepONet for CFD.
|
| 45 |
+
|
| 46 |
+
Our task is different from the one that the original DeepONet. In the
|
| 47 |
+
original DeepONet, the input function (input to the branch net)
|
| 48 |
+
is the initial condition (IC), but here, we have a fixed (zero) IC.
|
| 49 |
+
Instead, we have different boundary conditions (BCs), but we also
|
| 50 |
+
want the model to predict the next time step given the current time step.
|
| 51 |
+
Ideally, we should have two different branch nets, one accepting the
|
| 52 |
+
BCs, one accepting the current time step.
|
| 53 |
+
|
| 54 |
+
Here, we assume that the current time step includes the information about
|
| 55 |
+
BCs (which are the values on the bounds), so we just feed
|
| 56 |
+
the current time step to one branch net.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
in_chan: int,
|
| 62 |
+
query_dim: int,
|
| 63 |
+
loss_fn: MseLoss,
|
| 64 |
+
height: int = 100,
|
| 65 |
+
width: int = 100,
|
| 66 |
+
num_case_params: int = 5,
|
| 67 |
+
trunk_depth: int = 4,
|
| 68 |
+
act_name="relu",
|
| 69 |
+
act_norm: bool = False,
|
| 70 |
+
act_on_output: bool = False,
|
| 71 |
+
):
|
| 72 |
+
"""
|
| 73 |
+
Args:
|
| 74 |
+
- branch_dim: int, the dimension of the branch net input.
|
| 75 |
+
- trunk_dim: int, the dimension of the trunk net input.
|
| 76 |
+
"""
|
| 77 |
+
super().__init__(loss_fn)
|
| 78 |
+
self.in_chan = in_chan
|
| 79 |
+
self.query_dim = query_dim
|
| 80 |
+
self.num_case_params = num_case_params
|
| 81 |
+
self.trunk_depth = trunk_depth
|
| 82 |
+
self.height = height
|
| 83 |
+
self.width = width
|
| 84 |
+
self.act_name = act_name
|
| 85 |
+
self.act_norm = act_norm
|
| 86 |
+
self.act_on_output = act_on_output
|
| 87 |
+
|
| 88 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 89 |
+
self.trunk_dims = [query_dim] + [100] * trunk_depth + [4 * 4 * 32]
|
| 90 |
+
# + 1 for mask
|
| 91 |
+
self.branch_net = CnnBranch(
|
| 92 |
+
in_chan + 1 + num_case_params, kernel_size=5, padding=2
|
| 93 |
+
)
|
| 94 |
+
self.trunk_net = Ffn(
|
| 95 |
+
self.trunk_dims, act_fn=act_fn, act_on_output=False
|
| 96 |
+
)
|
| 97 |
+
self.out_ffn = Ffn(
|
| 98 |
+
[32 * 4 * 4] * 3 + [1], act_fn=act_fn, act_on_output=False
|
| 99 |
+
)
|
| 100 |
+
self.bias = nn.Parameter(torch.zeros(1)) # type: ignore
|
| 101 |
+
|
| 102 |
+
def forward(
|
| 103 |
+
self,
|
| 104 |
+
inputs: Tensor,
|
| 105 |
+
case_params: Tensor,
|
| 106 |
+
label: Optional[Tensor] = None,
|
| 107 |
+
mask: Optional[Tensor] = None,
|
| 108 |
+
query_idxs: Optional[Tensor] = None,
|
| 109 |
+
):
|
| 110 |
+
"""
|
| 111 |
+
Here, we just randomly sample some points, and use the label values on
|
| 112 |
+
those points as the label.
|
| 113 |
+
|
| 114 |
+
x: (b, c, h, w)
|
| 115 |
+
labels: (b, c, h, w)
|
| 116 |
+
query_point: (k, 2), k is the number of query points, each is an (x, y)
|
| 117 |
+
coordinate.
|
| 118 |
+
|
| 119 |
+
Goal:
|
| 120 |
+
Input: [b, branch_dim + trunk_dim]
|
| 121 |
+
Output: [b, 1]
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
# Add mask to input as additional channels
|
| 125 |
+
if mask is not None:
|
| 126 |
+
if mask.dim() == 3:
|
| 127 |
+
mask = mask.unsqueeze(1) # (B, 1, h, w)
|
| 128 |
+
inputs = torch.cat([inputs, mask], dim=1) # (B, c + 1, h, w)
|
| 129 |
+
|
| 130 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 131 |
+
|
| 132 |
+
# Only use the u channel
|
| 133 |
+
residuals = inputs[:, :] # (B, c, h, w)
|
| 134 |
+
|
| 135 |
+
# Add case params as additional channels
|
| 136 |
+
case_params = case_params.unsqueeze(-1).unsqueeze(-1) # (B, c, 1, 1)
|
| 137 |
+
# (B, n_params, h, w)
|
| 138 |
+
case_params = case_params.expand(
|
| 139 |
+
-1, -1, inputs.shape[-2], inputs.shape[-1]
|
| 140 |
+
)
|
| 141 |
+
inputs = torch.cat(
|
| 142 |
+
[inputs, case_params], dim=1
|
| 143 |
+
) # (B, c + n_params, h, w)
|
| 144 |
+
|
| 145 |
+
x_branch = self.branch_net(inputs) # (b, 32, h/16=4, w/16=4)
|
| 146 |
+
x_branch = x_branch.view(batch_size, -1) # (b, 32 * 4 * 4 = 512)
|
| 147 |
+
|
| 148 |
+
if query_idxs is None:
|
| 149 |
+
query_idxs = torch.tensor(
|
| 150 |
+
list(product(range(height), range(width))),
|
| 151 |
+
dtype=torch.long,
|
| 152 |
+
device=x_branch.device,
|
| 153 |
+
) # (h * w, 2)
|
| 154 |
+
|
| 155 |
+
# Input to the trunk net
|
| 156 |
+
x_trunk = (query_idxs.float() - 50) / 100 # (k, 2)
|
| 157 |
+
x_trunk = self.trunk_net(x_trunk) # (k, p)
|
| 158 |
+
x_trunk = x_trunk.unsqueeze(0) # (1, k, p)
|
| 159 |
+
x_branch = x_branch.unsqueeze(1) # (b, 1, 32, p)
|
| 160 |
+
# preds = torch.sum(x_branch * x_trunk, dim=-1) + self.bias # (b, k)
|
| 161 |
+
preds = x_branch * x_trunk # (b, k, h)
|
| 162 |
+
preds = self.out_ffn(preds) # (b, k, 1)
|
| 163 |
+
preds = preds.squeeze(-1) # (b, k)
|
| 164 |
+
|
| 165 |
+
# Use values of the input field at query points as residuals
|
| 166 |
+
residuals = residuals[
|
| 167 |
+
:, 0, query_idxs[:, 0], query_idxs[:, 1]
|
| 168 |
+
] # (b, c, k)
|
| 169 |
+
# print(residuals.shape, preds.shape)
|
| 170 |
+
preds += residuals
|
| 171 |
+
|
| 172 |
+
if label is not None:
|
| 173 |
+
label = label[:, 0] # (B, 1, h, w) # Predict only u
|
| 174 |
+
# we have labels[i, j] = label[
|
| 175 |
+
# i, query_points[i, j, 0], query_points[i, j, 1]]
|
| 176 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 177 |
+
loss = self.loss_fn(labels=labels, preds=preds) # (b, k)
|
| 178 |
+
return dict(
|
| 179 |
+
preds=preds,
|
| 180 |
+
loss=loss,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 184 |
+
return dict(preds=preds)
|
| 185 |
+
|
| 186 |
+
def generate(
|
| 187 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor
|
| 188 |
+
) -> Tensor:
|
| 189 |
+
"""
|
| 190 |
+
x: (c, h, w) or (B, c, h, w)
|
| 191 |
+
|
| 192 |
+
Returns:
|
| 193 |
+
(b, c, h, w)
|
| 194 |
+
"""
|
| 195 |
+
if inputs.dim() == 3:
|
| 196 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 197 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 198 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 199 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 200 |
+
query_idxs = torch.tensor(
|
| 201 |
+
list(product(range(height), range(width))),
|
| 202 |
+
dtype=torch.long,
|
| 203 |
+
device=inputs.device,
|
| 204 |
+
) # (h * w, 2)
|
| 205 |
+
# query_points = query_points / 100
|
| 206 |
+
# (b, 1, h * w)
|
| 207 |
+
preds = self.forward(
|
| 208 |
+
inputs, query_idxs=query_idxs, case_params=case_params, mask=mask
|
| 209 |
+
)["preds"]
|
| 210 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 211 |
+
return preds
|
| 212 |
+
|
| 213 |
+
def generate_many(
|
| 214 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int
|
| 215 |
+
) -> List[Tensor]:
|
| 216 |
+
"""
|
| 217 |
+
x: (c, h, w) or (B, c, h, w)
|
| 218 |
+
mask: (h, w). 1 for interior, 0 for boundaries.
|
| 219 |
+
steps: int, number of steps to generate.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
list of tensors, each of shape (b, c, h, w)
|
| 223 |
+
"""
|
| 224 |
+
if inputs.dim() == 3:
|
| 225 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 226 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 227 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 228 |
+
cur_frame = inputs
|
| 229 |
+
p = inputs[:, -1:]
|
| 230 |
+
preds = []
|
| 231 |
+
for _ in range(steps):
|
| 232 |
+
# (b, c, h, w)
|
| 233 |
+
cur_frame = self.generate(
|
| 234 |
+
cur_frame, case_params=case_params, mask=mask
|
| 235 |
+
)
|
| 236 |
+
preds.append(cur_frame)
|
| 237 |
+
cur_frame = torch.cat([cur_frame, p], dim=1)
|
| 238 |
+
return preds
|
model/auto_edeeponet.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import product
|
| 2 |
+
from typing import Dict, Optional, List
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn, Tensor
|
| 6 |
+
|
| 7 |
+
from .base_model import AutoCfdModel
|
| 8 |
+
from .ffn import Ffn
|
| 9 |
+
from .act_fn import get_act_fn
|
| 10 |
+
from .loss import MseLoss
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AutoEDeepONet(AutoCfdModel):
|
| 14 |
+
"""
|
| 15 |
+
EDeepONet for autoregressive generation. The two input functions are
|
| 16 |
+
the previous field (flattened) and the case parameters.
|
| 17 |
+
|
| 18 |
+
Branch net accepts the boundary and physics properties as inputs.
|
| 19 |
+
Trunk net accepts the query location (t, x, y) as input.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
dim_branch1: int,
|
| 25 |
+
dim_branch2: int,
|
| 26 |
+
trunk_dim: int, # (x, y)
|
| 27 |
+
loss_fn: MseLoss,
|
| 28 |
+
num_label_samples: int = 1000,
|
| 29 |
+
branch_depth: int = 4,
|
| 30 |
+
trunk_depth: int = 4,
|
| 31 |
+
width: int = 100,
|
| 32 |
+
act_name: str = "relu",
|
| 33 |
+
act_norm: bool = False,
|
| 34 |
+
act_on_output: bool = False,
|
| 35 |
+
):
|
| 36 |
+
super().__init__(loss_fn)
|
| 37 |
+
self.dim_branch1 = dim_branch1
|
| 38 |
+
self.dim_branch2 = dim_branch2
|
| 39 |
+
self.trunk_dim = trunk_dim
|
| 40 |
+
self.loss_fn = loss_fn
|
| 41 |
+
self.branch_depth = branch_depth
|
| 42 |
+
self.trunk_depth = trunk_depth
|
| 43 |
+
self.width = width
|
| 44 |
+
self.act_name = act_name
|
| 45 |
+
self.act_norm = act_norm
|
| 46 |
+
self.act_on_output = act_on_output
|
| 47 |
+
self.num_label_samples = num_label_samples
|
| 48 |
+
|
| 49 |
+
self.branch1_dims = [dim_branch1] + [width] * branch_depth
|
| 50 |
+
self.branch2_dims = [dim_branch2] + [width] * branch_depth
|
| 51 |
+
self.trunk_dims = [trunk_dim] + [width] * trunk_depth
|
| 52 |
+
print(self.trunk_dims)
|
| 53 |
+
|
| 54 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 55 |
+
self.branch1 = Ffn(
|
| 56 |
+
self.branch1_dims, act_fn=act_fn, act_on_output=self.act_on_output
|
| 57 |
+
)
|
| 58 |
+
self.branch2 = Ffn(
|
| 59 |
+
self.branch2_dims, act_fn=act_fn, act_on_output=self.act_on_output
|
| 60 |
+
)
|
| 61 |
+
# we will be using an entire frame as label.
|
| 62 |
+
self.trunk_net = Ffn(self.trunk_dims, act_fn=act_fn)
|
| 63 |
+
|
| 64 |
+
self.bias = nn.Parameter(torch.zeros(1)) # type: ignore
|
| 65 |
+
|
| 66 |
+
def forward(
|
| 67 |
+
self,
|
| 68 |
+
inputs: Tensor, # (b, d1), previous field u(t-1)
|
| 69 |
+
case_params: Tensor, # (b, d2), physical properties
|
| 70 |
+
label: Optional[Tensor] = None,
|
| 71 |
+
mask: Optional[Tensor] = None, # NOTE: not used
|
| 72 |
+
query_idxs: Optional[Tensor] = None,
|
| 73 |
+
) -> Dict[str, Tensor]:
|
| 74 |
+
"""
|
| 75 |
+
A faster forward by using all the points in the frame (`label`) at
|
| 76 |
+
time step `t` as training examples.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
- x_branch: (b, branch_dim), input to the branch net.
|
| 80 |
+
- t: (b), input to the trunk net, a batch of t
|
| 81 |
+
- label: (b, w, h), the frame to be predicted.
|
| 82 |
+
- query_idxs: (b, k, 2), the query locations.
|
| 83 |
+
"""
|
| 84 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 85 |
+
# Only use the u channel, because using more channels is to expensive
|
| 86 |
+
inputs = inputs[:, 0] # (B, h, w)
|
| 87 |
+
# Flatten
|
| 88 |
+
flat_inputs = inputs.view(batch_size, -1) # (B, h * w)
|
| 89 |
+
|
| 90 |
+
b1 = self.branch1(flat_inputs) # (b, p)
|
| 91 |
+
b2 = self.branch2(case_params) # (b, p)
|
| 92 |
+
x_branch = b1 * b2
|
| 93 |
+
|
| 94 |
+
if query_idxs is None:
|
| 95 |
+
query_idxs = torch.tensor(
|
| 96 |
+
list(product(range(height), range(width))),
|
| 97 |
+
dtype=torch.long,
|
| 98 |
+
device=flat_inputs.device,
|
| 99 |
+
) # (h * w, 2)
|
| 100 |
+
|
| 101 |
+
# Normalize query location
|
| 102 |
+
x_trunk = (query_idxs.float() - 50) / 100 # (k, 2)
|
| 103 |
+
x_trunk = self.trunk_net(x_trunk) # (k, p)
|
| 104 |
+
x_trunk = x_trunk.unsqueeze(0) # (1, k, p)
|
| 105 |
+
x_branch = x_branch.unsqueeze(1) # (b, 1, p)
|
| 106 |
+
preds = torch.sum(x_branch * x_trunk, dim=-1) + self.bias # (b, k)
|
| 107 |
+
|
| 108 |
+
# Use values of input field at query points as residuals
|
| 109 |
+
residuals = inputs[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 110 |
+
preds = preds + residuals
|
| 111 |
+
|
| 112 |
+
if label is not None:
|
| 113 |
+
# Use only the u channel
|
| 114 |
+
label = label[:, 0] # (B, w, h)
|
| 115 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 116 |
+
assert (
|
| 117 |
+
preds.shape == labels.shape
|
| 118 |
+
), f"{preds.shape}, {labels.shape}"
|
| 119 |
+
loss = self.loss_fn(preds=preds, labels=labels) # (b, k)
|
| 120 |
+
return dict(
|
| 121 |
+
preds=preds,
|
| 122 |
+
loss=loss,
|
| 123 |
+
)
|
| 124 |
+
preds.view(-1, 1, height, width)
|
| 125 |
+
return dict(preds=preds)
|
| 126 |
+
|
| 127 |
+
def generate(
|
| 128 |
+
self,
|
| 129 |
+
inputs: Tensor,
|
| 130 |
+
case_params: Tensor,
|
| 131 |
+
mask: Optional[Tensor] = None,
|
| 132 |
+
) -> Tensor:
|
| 133 |
+
"""
|
| 134 |
+
Generate one frame at time t.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
- x: Tensor, (b, field dim)
|
| 138 |
+
- case_params: Tensor, (b, case params dim)
|
| 139 |
+
- t: Tensor, (b)
|
| 140 |
+
- height: int
|
| 141 |
+
- width: int
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
(b, c, h, w)
|
| 145 |
+
"""
|
| 146 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 147 |
+
# Create 2D lattice of query points to infer the frame.
|
| 148 |
+
query_idxs = torch.tensor(
|
| 149 |
+
list(product(range(height), range(width))),
|
| 150 |
+
dtype=torch.long,
|
| 151 |
+
device=inputs.device,
|
| 152 |
+
) # (h * w, 2)
|
| 153 |
+
# (b, 1, h * w)
|
| 154 |
+
preds = self.forward(
|
| 155 |
+
inputs=inputs, case_params=case_params, query_idxs=query_idxs
|
| 156 |
+
)["preds"]
|
| 157 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 158 |
+
return preds
|
| 159 |
+
|
| 160 |
+
def generate_many(
|
| 161 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int
|
| 162 |
+
) -> List[Tensor]:
|
| 163 |
+
"""
|
| 164 |
+
x: (c, h, w) or (B, c, h, w)
|
| 165 |
+
mask: (h, w). 1 for interior, 0 for boundaries.
|
| 166 |
+
steps: int, number of steps to generate.
|
| 167 |
+
F
|
| 168 |
+
Returns:
|
| 169 |
+
list of tensors, each of shape (b, c, h, w)
|
| 170 |
+
"""
|
| 171 |
+
assert len(inputs.shape) == len(case_params.shape) + 2
|
| 172 |
+
if inputs.dim() == 3:
|
| 173 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 174 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 175 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 176 |
+
assert inputs.shape[0] == case_params.shape[0]
|
| 177 |
+
cur_frame = inputs
|
| 178 |
+
preds = []
|
| 179 |
+
for _ in range(steps):
|
| 180 |
+
# (b, c, h, w)
|
| 181 |
+
cur_frame = self.generate(
|
| 182 |
+
inputs=cur_frame, case_params=case_params, mask=None
|
| 183 |
+
)
|
| 184 |
+
preds.append(cur_frame)
|
| 185 |
+
return preds
|
model/auto_ffn.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import product
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch import Tensor
|
| 6 |
+
|
| 7 |
+
from .ffn import Ffn
|
| 8 |
+
from .base_model import AutoCfdModel
|
| 9 |
+
from .act_fn import get_act_fn
|
| 10 |
+
from .loss import MseLoss
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AutoFfn(AutoCfdModel):
|
| 14 |
+
"""
|
| 15 |
+
Equivalent to autoregressive data-driven PINN.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
input_field_dim: int,
|
| 21 |
+
num_case_params: int,
|
| 22 |
+
query_dim: int,
|
| 23 |
+
loss_fn: MseLoss,
|
| 24 |
+
num_label_samples: int = 1000,
|
| 25 |
+
depth: int = 8,
|
| 26 |
+
width: int = 100,
|
| 27 |
+
act_norm: bool = False,
|
| 28 |
+
act_name="relu",
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Args:
|
| 32 |
+
- branch_dim: int, the dimension of the branch net input.
|
| 33 |
+
- trunk_dim: int, the dimension of the trunk net input.
|
| 34 |
+
"""
|
| 35 |
+
super().__init__(loss_fn)
|
| 36 |
+
self.input_field_dim = input_field_dim
|
| 37 |
+
self.num_case_params = num_case_params
|
| 38 |
+
self.query_dim = query_dim
|
| 39 |
+
self.depth = depth
|
| 40 |
+
self.width = width
|
| 41 |
+
self.act_name = act_name
|
| 42 |
+
self.act_norm = act_norm
|
| 43 |
+
self.num_label_samples = num_label_samples
|
| 44 |
+
|
| 45 |
+
self.in_dim = input_field_dim + num_case_params + query_dim
|
| 46 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 47 |
+
self.widths = [self.in_dim] + [width] * depth + [1]
|
| 48 |
+
self.ffn = Ffn(
|
| 49 |
+
self.widths,
|
| 50 |
+
act_fn=act_fn,
|
| 51 |
+
act_on_output=False,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def forward(
|
| 55 |
+
self,
|
| 56 |
+
inputs: Tensor,
|
| 57 |
+
case_params: Tensor,
|
| 58 |
+
label: Optional[Tensor] = None,
|
| 59 |
+
mask: Optional[Tensor] = None, # NOTE: Not used
|
| 60 |
+
query_idxs: Optional[Tensor] = None,
|
| 61 |
+
):
|
| 62 |
+
"""
|
| 63 |
+
Here, we just randomly sample some points, and use the label values on
|
| 64 |
+
those points as the label.
|
| 65 |
+
|
| 66 |
+
### Parameters
|
| 67 |
+
- `inputs: Tensor` -- (b, c, h, w)
|
| 68 |
+
- `labels: Tensor` -- (b, c, h, w)
|
| 69 |
+
- `query_idxs: Tensor` -- (k, 2), k is the number of query points,
|
| 70 |
+
each is an (x, y) coordinate.
|
| 71 |
+
- `mask: Tensor` -- Not used.
|
| 72 |
+
|
| 73 |
+
### Function
|
| 74 |
+
Input: [b, branch_dim + trunk_dim]
|
| 75 |
+
Output: [b, 1]
|
| 76 |
+
"""
|
| 77 |
+
batch_size, _num_chan, height, width = inputs.shape
|
| 78 |
+
|
| 79 |
+
# Only use the u channel
|
| 80 |
+
inputs = inputs[:, 0] # (B, h, w)
|
| 81 |
+
# Flatten
|
| 82 |
+
flat_inputs = inputs.view(batch_size, -1) # (B, h * w)
|
| 83 |
+
flat_inputs = torch.cat(
|
| 84 |
+
[flat_inputs, case_params], dim=1
|
| 85 |
+
) # (B, h * w + 2)
|
| 86 |
+
|
| 87 |
+
if query_idxs is None:
|
| 88 |
+
query_idxs = torch.tensor(
|
| 89 |
+
list(product(range(height), range(width))),
|
| 90 |
+
dtype=torch.long,
|
| 91 |
+
device=flat_inputs.device,
|
| 92 |
+
) # (k=h * w, 2)
|
| 93 |
+
|
| 94 |
+
n_queries = query_idxs.shape[0]
|
| 95 |
+
|
| 96 |
+
# For each combination of (input, query_point), we have a sample.
|
| 97 |
+
# Repeat tensors such that we get (b * k) samples
|
| 98 |
+
# (b, k, h * w)
|
| 99 |
+
flat_inputs = flat_inputs.repeat(n_queries, 1) # (b * k, h * w)
|
| 100 |
+
batch_query_idxs = query_idxs.repeat(batch_size, 1) # (b * k, 2)
|
| 101 |
+
|
| 102 |
+
# (b * k, h * w + 2)
|
| 103 |
+
flat_inputs = torch.cat([flat_inputs, batch_query_idxs.float()], dim=1)
|
| 104 |
+
|
| 105 |
+
preds = self.ffn(flat_inputs) # (b * k, 1)
|
| 106 |
+
preds = preds.view(batch_size, -1) # (b, k)
|
| 107 |
+
|
| 108 |
+
# Use values of the input field at query points as residuals
|
| 109 |
+
residuals = inputs[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 110 |
+
preds += residuals
|
| 111 |
+
|
| 112 |
+
if label is not None:
|
| 113 |
+
label = label[:, 0] # (B, 1, h, w) # Predict only u
|
| 114 |
+
# we have labels[i, j] = label[
|
| 115 |
+
# i, query_points[i, j, 0], query_points[i, j, 1]]
|
| 116 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 117 |
+
loss = self.loss_fn(labels=labels, preds=preds) # (b, k)
|
| 118 |
+
return dict(
|
| 119 |
+
preds=preds,
|
| 120 |
+
loss=loss,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 124 |
+
return dict(preds=preds)
|
| 125 |
+
|
| 126 |
+
def generate(
|
| 127 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor
|
| 128 |
+
) -> Tensor:
|
| 129 |
+
"""
|
| 130 |
+
x: (c, h, w) or (B, c, h, w)
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
(b, c, h, w)
|
| 134 |
+
"""
|
| 135 |
+
if inputs.dim() == 3:
|
| 136 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 137 |
+
batch_size, num_chan, height, width = inputs.shape
|
| 138 |
+
query_idxs = torch.tensor(
|
| 139 |
+
list(product(range(height), range(width))),
|
| 140 |
+
dtype=torch.long,
|
| 141 |
+
device=inputs.device,
|
| 142 |
+
) # (h * w, 2)
|
| 143 |
+
# query_points = query_points / 100
|
| 144 |
+
# (b, 1, h * w)
|
| 145 |
+
preds = self.forward(
|
| 146 |
+
inputs, query_idxs=query_idxs, case_params=case_params, mask=mask
|
| 147 |
+
)["preds"]
|
| 148 |
+
preds = preds.view(-1, 1, height, width) # (b, 1, h, w)
|
| 149 |
+
return preds
|
| 150 |
+
|
| 151 |
+
def generate_many(
|
| 152 |
+
self,
|
| 153 |
+
inputs: Tensor,
|
| 154 |
+
case_params: Tensor,
|
| 155 |
+
mask: Tensor,
|
| 156 |
+
steps: int,
|
| 157 |
+
) -> List[Tensor]:
|
| 158 |
+
"""
|
| 159 |
+
x: (c, h, w) or (B, c, h, w)
|
| 160 |
+
mask: (h, w). 1 for interior, 0 for boundaries.
|
| 161 |
+
steps: int, number of steps to generate.
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
list of tensors, each of shape (b, c, h, w)
|
| 165 |
+
"""
|
| 166 |
+
if inputs.dim() == 3:
|
| 167 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 168 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 169 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 170 |
+
cur_frame = inputs
|
| 171 |
+
preds = []
|
| 172 |
+
for _ in range(steps):
|
| 173 |
+
# (b, c, h, w)
|
| 174 |
+
cur_frame = self.generate(
|
| 175 |
+
cur_frame, case_params=case_params, mask=mask
|
| 176 |
+
)
|
| 177 |
+
preds.append(cur_frame)
|
| 178 |
+
return preds
|
model/base_model.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn, Tensor
|
| 2 |
+
from typing import Optional, List
|
| 3 |
+
|
| 4 |
+
from .loss import MseLoss
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class CfdModel(nn.Module):
|
| 8 |
+
"""
|
| 9 |
+
Base model for all data-driven NN for CFD, learns the mapping from
|
| 10 |
+
conditions (physics properties, boundary conditions and geometry)
|
| 11 |
+
to the solution at a later time.
|
| 12 |
+
"""
|
| 13 |
+
def __init__(self, loss_fn: MseLoss):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self.loss_fn = loss_fn
|
| 16 |
+
|
| 17 |
+
def forward(
|
| 18 |
+
self,
|
| 19 |
+
x: Tensor,
|
| 20 |
+
mask: Optional[Tensor] = None,
|
| 21 |
+
label: Optional[Tensor] = None,
|
| 22 |
+
case_params: Optional[dict] = None,
|
| 23 |
+
) -> dict:
|
| 24 |
+
raise NotImplementedError
|
| 25 |
+
|
| 26 |
+
def generate_one(
|
| 27 |
+
self,
|
| 28 |
+
case_params,
|
| 29 |
+
t: Tensor,
|
| 30 |
+
height: int,
|
| 31 |
+
width: int,
|
| 32 |
+
**kwargs,
|
| 33 |
+
) -> Tensor:
|
| 34 |
+
"""
|
| 35 |
+
Generate the frame at time step `time`, given the case parameters.
|
| 36 |
+
`case_params`.
|
| 37 |
+
"""
|
| 38 |
+
raise NotImplementedError
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class AutoCfdModel(nn.Module):
|
| 42 |
+
"""
|
| 43 |
+
A CFD model that generates the solution auto-regressively, one frame
|
| 44 |
+
at a time.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, loss_fn: nn.Module):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.loss_fn = loss_fn
|
| 50 |
+
|
| 51 |
+
def forward(
|
| 52 |
+
self,
|
| 53 |
+
inputs: Tensor,
|
| 54 |
+
label: Optional[Tensor] = None,
|
| 55 |
+
case_params: Optional[dict] = None,
|
| 56 |
+
mask: Optional[Tensor] = None,
|
| 57 |
+
**kwargs,
|
| 58 |
+
) -> dict:
|
| 59 |
+
raise NotImplementedError
|
| 60 |
+
|
| 61 |
+
def generate(
|
| 62 |
+
self,
|
| 63 |
+
inputs: Tensor,
|
| 64 |
+
case_params: Tensor,
|
| 65 |
+
mask: Tensor,
|
| 66 |
+
**kwargs,
|
| 67 |
+
) -> Tensor:
|
| 68 |
+
raise NotImplementedError
|
| 69 |
+
|
| 70 |
+
def generate_many(
|
| 71 |
+
self,
|
| 72 |
+
inputs: Tensor,
|
| 73 |
+
case_params: Tensor,
|
| 74 |
+
mask: Tensor,
|
| 75 |
+
steps: int,
|
| 76 |
+
**kwargs,
|
| 77 |
+
) -> List[Tensor]:
|
| 78 |
+
"""
|
| 79 |
+
Given a frame `inputs`, generate the next `steps` frames.
|
| 80 |
+
"""
|
| 81 |
+
raise NotImplementedError
|
model/deeponet.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from itertools import product
|
| 2 |
+
from typing import Dict, Optional
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn, Tensor
|
| 6 |
+
|
| 7 |
+
from .base_model import CfdModel
|
| 8 |
+
from .ffn import Ffn
|
| 9 |
+
from .act_fn import get_act_fn
|
| 10 |
+
from .loss import MseLoss
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class DeepONet(CfdModel):
|
| 14 |
+
"""
|
| 15 |
+
DeepONet for CFD.
|
| 16 |
+
|
| 17 |
+
Branch net accepts the boundary and physics properties as inputs.
|
| 18 |
+
Trunk net accepts the query location (t, x, y) as input.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
branch_dim: int,
|
| 24 |
+
trunk_dim: int, # (t, x, y)
|
| 25 |
+
loss_fn: MseLoss,
|
| 26 |
+
num_label_samples: int = 1000,
|
| 27 |
+
branch_depth: int = 4,
|
| 28 |
+
trunk_depth: int = 3,
|
| 29 |
+
width: int = 100,
|
| 30 |
+
act_name: str = "relu",
|
| 31 |
+
act_norm: bool = False,
|
| 32 |
+
act_on_output: bool = False,
|
| 33 |
+
):
|
| 34 |
+
"""
|
| 35 |
+
Args:
|
| 36 |
+
- branch_dim: int, the dimension of the branch net input.
|
| 37 |
+
- trunk_dim: int, the dimension of the trunk net input.
|
| 38 |
+
"""
|
| 39 |
+
super().__init__(loss_fn)
|
| 40 |
+
self.branch_dim = branch_dim
|
| 41 |
+
self.trunk_dim = trunk_dim
|
| 42 |
+
self.loss_fn = loss_fn
|
| 43 |
+
self.branch_depth = branch_depth
|
| 44 |
+
self.trunk_depth = trunk_depth
|
| 45 |
+
self.width = width
|
| 46 |
+
self.act_name = act_name
|
| 47 |
+
self.act_norm = act_norm
|
| 48 |
+
self.act_on_output = act_on_output
|
| 49 |
+
self.num_label_samples = num_label_samples
|
| 50 |
+
|
| 51 |
+
self.branch_dims = [branch_dim] + [width] * branch_depth
|
| 52 |
+
|
| 53 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 54 |
+
self.branch_net = Ffn(
|
| 55 |
+
self.branch_dims, act_fn=act_fn, act_on_output=self.act_on_output
|
| 56 |
+
)
|
| 57 |
+
# t and x, y uses separate fc layers because during training
|
| 58 |
+
# we will be using an entire frame as label.
|
| 59 |
+
self.fc_trunk_t = nn.Linear(1, width)
|
| 60 |
+
self.fc_trunk_xy = nn.Linear(2, width)
|
| 61 |
+
self.trunk_dims = [width] * trunk_depth
|
| 62 |
+
self.trunk_net = Ffn(self.trunk_dims, act_fn=act_fn)
|
| 63 |
+
|
| 64 |
+
# self.params = self.__init_params()
|
| 65 |
+
self.bias = nn.Parameter(torch.zeros(1)) # type: ignore
|
| 66 |
+
# self.__initialize()
|
| 67 |
+
|
| 68 |
+
# NOTE: This is no long used!!
|
| 69 |
+
# This is the implementation of the ordinary DeepONet, but it is very slow.
|
| 70 |
+
# Our `forward` is much faster because it re-uses the output
|
| 71 |
+
# of the branch net for different queries in one batch.
|
| 72 |
+
def forward_vanilla(
|
| 73 |
+
self,
|
| 74 |
+
x_branch: Tensor,
|
| 75 |
+
x_trunk: Tensor,
|
| 76 |
+
label: Optional[Tensor] = None,
|
| 77 |
+
query_idxs: Optional[Tensor] = None,
|
| 78 |
+
):
|
| 79 |
+
"""
|
| 80 |
+
NOTE: This is no long used!!
|
| 81 |
+
This is the implementation of the ordinary DeepONet, but it is very
|
| 82 |
+
slow! Our `forward` is much faster because it re-uses the output
|
| 83 |
+
of the branch net for different queries in one batch.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
- x_branch: (b, branch_dim), input to the branch net.
|
| 87 |
+
- x_trunk: (b), input to the trunk net, a batch of (t, x, y)
|
| 88 |
+
- label: (b), the label for the query location.
|
| 89 |
+
|
| 90 |
+
Return:
|
| 91 |
+
- if label is None:
|
| 92 |
+
- preds: (b, k), the prediction for the query location.
|
| 93 |
+
- else:
|
| 94 |
+
- preds: (b, k), the prediction for the query location.
|
| 95 |
+
- loss: (b, k), the loss for the query location.
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
# Create k query locations.
|
| 99 |
+
# (b, k, 3), where each element is (t, x, y)
|
| 100 |
+
if query_idxs is None:
|
| 101 |
+
assert label is not None
|
| 102 |
+
height, width = label.shape[-2:]
|
| 103 |
+
query_idxs = torch.stack(
|
| 104 |
+
[
|
| 105 |
+
torch.randint(
|
| 106 |
+
0,
|
| 107 |
+
height,
|
| 108 |
+
(self.num_label_samples,),
|
| 109 |
+
device=x_trunk.device,
|
| 110 |
+
),
|
| 111 |
+
torch.randint(
|
| 112 |
+
0,
|
| 113 |
+
width,
|
| 114 |
+
(self.num_label_samples,),
|
| 115 |
+
device=x_trunk.device,
|
| 116 |
+
),
|
| 117 |
+
],
|
| 118 |
+
dim=-1,
|
| 119 |
+
) # (k, 2)
|
| 120 |
+
t = x_trunk.unsqueeze(1).float() # (b, 1)
|
| 121 |
+
x_trunk_t = self.fc_trunk_t(t) # (b, p)
|
| 122 |
+
# Normalize query location
|
| 123 |
+
x_trunk_xy = (
|
| 124 |
+
query_idxs.float() - 32.0
|
| 125 |
+
) / 64.0 # (k, 2) # TODO: update this
|
| 126 |
+
x_trunk_xy = self.fc_trunk_xy(x_trunk_xy) # (k, p)
|
| 127 |
+
x_trunk_t = x_trunk_t.unsqueeze(1) # (b, 1, p)
|
| 128 |
+
x_trunk_xy = x_trunk_xy.unsqueeze(0) # (1, k, p)
|
| 129 |
+
x_trunk = x_trunk_t + x_trunk_xy # (b, k, p)
|
| 130 |
+
|
| 131 |
+
x_branch = self.branch_net(x_branch) # (b, p)
|
| 132 |
+
x_trunk = self.trunk_net(x_trunk) # (b, k, p)
|
| 133 |
+
|
| 134 |
+
# x_trunk = x_trunk.unsqueeze(0) # (1, k, p)
|
| 135 |
+
x_branch = x_branch.unsqueeze(1) # (b, 1, p)
|
| 136 |
+
preds = torch.sum(x_branch * x_trunk, dim=-1) + self.bias # (b)
|
| 137 |
+
print(preds.dtype)
|
| 138 |
+
|
| 139 |
+
if label is not None:
|
| 140 |
+
print(label.dtype)
|
| 141 |
+
# Use only the u channel
|
| 142 |
+
label = label[:, 0] # (B, w, h)
|
| 143 |
+
# we have labels[i, j] = label[
|
| 144 |
+
# i, query_points[i, j, 0], query_points[i, j, 1]]
|
| 145 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 146 |
+
# assert preds.shape == label.shape, f"{preds.shape} {label.shape}"
|
| 147 |
+
loss = self.loss_fn(preds=preds, labels=labels) # (b, k)
|
| 148 |
+
print(labels.dtype)
|
| 149 |
+
print(loss.dtype)
|
| 150 |
+
return preds, loss
|
| 151 |
+
return preds
|
| 152 |
+
|
| 153 |
+
def forward(
|
| 154 |
+
self,
|
| 155 |
+
case_params: Tensor, # Case parameters
|
| 156 |
+
t: Tensor,
|
| 157 |
+
label: Optional[Tensor] = None,
|
| 158 |
+
query_idxs: Optional[Tensor] = None,
|
| 159 |
+
) -> Dict[str, Tensor]:
|
| 160 |
+
"""
|
| 161 |
+
A faster forward by using all the points in the frame (`label`) at
|
| 162 |
+
time step `t` as training examples.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
- x_branch: (b, branch_dim), input to the branch net.
|
| 166 |
+
- t: (b), input to the trunk net, a batch of t
|
| 167 |
+
- label: (b, w, h), the frame to be predicted.
|
| 168 |
+
- query_idxs: (b, k, 2), the query locations.
|
| 169 |
+
"""
|
| 170 |
+
if query_idxs is None:
|
| 171 |
+
# Create k query locations on a lattice.
|
| 172 |
+
# (b, k, 2), where each element is (x, y)
|
| 173 |
+
assert label is not None
|
| 174 |
+
height, width = label.shape[-2:]
|
| 175 |
+
query_idxs = torch.stack(
|
| 176 |
+
[
|
| 177 |
+
torch.randint(
|
| 178 |
+
0,
|
| 179 |
+
height,
|
| 180 |
+
(self.num_label_samples,),
|
| 181 |
+
device=label.device,
|
| 182 |
+
),
|
| 183 |
+
torch.randint(
|
| 184 |
+
0,
|
| 185 |
+
width,
|
| 186 |
+
(self.num_label_samples,),
|
| 187 |
+
device=label.device,
|
| 188 |
+
),
|
| 189 |
+
],
|
| 190 |
+
dim=-1,
|
| 191 |
+
) # (k, 2)
|
| 192 |
+
|
| 193 |
+
x_trunk_t = self.fc_trunk_t(t) # (b, p)
|
| 194 |
+
# Normalize query location
|
| 195 |
+
x_trunk_xy = query_idxs.float() # (k, 2)
|
| 196 |
+
x_trunk_xy = self.fc_trunk_xy(x_trunk_xy) # (k, p)
|
| 197 |
+
x_trunk_t = x_trunk_t.unsqueeze(1) # (b, 1, p)
|
| 198 |
+
x_trunk_xy = x_trunk_xy.unsqueeze(0) # (1, k, p)
|
| 199 |
+
x_trunk = x_trunk_t + x_trunk_xy # (b, k, p)
|
| 200 |
+
|
| 201 |
+
case_params = self.branch_net(case_params) # (b, p)
|
| 202 |
+
x_trunk = self.trunk_net(x_trunk) # (b, k, p)
|
| 203 |
+
|
| 204 |
+
case_params = case_params.unsqueeze(1) # (b, 1, p)
|
| 205 |
+
preds = torch.sum(case_params * x_trunk, dim=-1) + self.bias # (b, k)
|
| 206 |
+
|
| 207 |
+
if label is not None:
|
| 208 |
+
# Only predict u
|
| 209 |
+
label = label[:, 0] # (B, w, h)
|
| 210 |
+
# we have labels[i, j] = label[
|
| 211 |
+
# i, query_points[i, j, 0], query_points[i, j, 1]]
|
| 212 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 213 |
+
assert (
|
| 214 |
+
preds.shape == labels.shape
|
| 215 |
+
), f"{preds.shape}, {labels.shape}"
|
| 216 |
+
loss = self.loss_fn(preds=preds, labels=labels) # (b, k)
|
| 217 |
+
return dict(
|
| 218 |
+
preds=preds,
|
| 219 |
+
loss=loss,
|
| 220 |
+
)
|
| 221 |
+
return dict(
|
| 222 |
+
preds=preds,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def generate_one(
|
| 226 |
+
self, case_params: Tensor, t: Tensor, height: int, width: int
|
| 227 |
+
) -> Tensor:
|
| 228 |
+
"""
|
| 229 |
+
Generate one frame at time t.
|
| 230 |
+
|
| 231 |
+
Args:
|
| 232 |
+
- x_branch: Tensor, (b, branch_dim)
|
| 233 |
+
- t: Tensor, (b)
|
| 234 |
+
- height: int
|
| 235 |
+
- width: int
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
(b, c, h, w)
|
| 239 |
+
"""
|
| 240 |
+
if len(case_params.shape) == 1:
|
| 241 |
+
case_params = case_params.unsqueeze(0)
|
| 242 |
+
if len(t.shape) == 0:
|
| 243 |
+
t = t.unsqueeze(0).unsqueeze(0)
|
| 244 |
+
elif len(t.shape) == 1:
|
| 245 |
+
t = t.unsqueeze(0)
|
| 246 |
+
|
| 247 |
+
# Create 2D lattice of query points to infer the frame.
|
| 248 |
+
query_idxs = torch.tensor(
|
| 249 |
+
list(product(range(height), range(width))),
|
| 250 |
+
# dtype=torch.long,
|
| 251 |
+
device=case_params.device,
|
| 252 |
+
) # (h * w, 2)
|
| 253 |
+
# query_points = query_points / 100
|
| 254 |
+
# (b, 1, h * w)
|
| 255 |
+
output = self.forward(case_params, t=t, query_idxs=query_idxs)["preds"]
|
| 256 |
+
output = output.view(-1, 1, height, width) # (b, 1, h, w)
|
| 257 |
+
return output
|
model/ffn.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Dict, List
|
| 2 |
+
from itertools import product
|
| 3 |
+
|
| 4 |
+
from torch import nn, Tensor
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from .base_model import CfdModel
|
| 8 |
+
from .loss import MseLoss
|
| 9 |
+
from .act_fn import get_act_fn
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Ffn(nn.Module):
|
| 13 |
+
"""
|
| 14 |
+
A general fully connected multi-layer neural network.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self, dims: list, act_fn: nn.Module, act_on_output: bool = False
|
| 19 |
+
):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.dims = dims
|
| 22 |
+
|
| 23 |
+
layers = []
|
| 24 |
+
for i in range(len(dims) - 2):
|
| 25 |
+
layers.append(nn.Linear(dims[i], dims[i + 1]))
|
| 26 |
+
layers.append(act_fn)
|
| 27 |
+
# layers.append(NormAct(nn.Tanh()))
|
| 28 |
+
layers.append(nn.Linear(dims[-2], dims[-1]))
|
| 29 |
+
if act_on_output:
|
| 30 |
+
layers.append(act_fn)
|
| 31 |
+
self.layers = nn.Sequential(*layers)
|
| 32 |
+
|
| 33 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 34 |
+
x = self.layers(x)
|
| 35 |
+
return x
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class FfnModel(CfdModel):
|
| 39 |
+
"""
|
| 40 |
+
Non-autoregressive FNN for data-driven CFD.
|
| 41 |
+
|
| 42 |
+
Branch net accepts the boundary and physics properties as inputs.
|
| 43 |
+
Trunk net accepts the query location (t, x, y) as input.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
loss_fn: MseLoss,
|
| 49 |
+
widths: List[int],
|
| 50 |
+
act_name: str = "relu",
|
| 51 |
+
act_norm: bool = True,
|
| 52 |
+
act_on_output: bool = False,
|
| 53 |
+
num_label_samples: int = 1000,
|
| 54 |
+
):
|
| 55 |
+
"""
|
| 56 |
+
Args:
|
| 57 |
+
- branch_dim: int, the dimension of the branch net input.
|
| 58 |
+
- trunk_dim: int, the dimension of the trunk net input.
|
| 59 |
+
"""
|
| 60 |
+
super().__init__(loss_fn)
|
| 61 |
+
self.loss_fn = loss_fn
|
| 62 |
+
self.widths = widths
|
| 63 |
+
self.act_name = act_name
|
| 64 |
+
self.act_norm = act_norm
|
| 65 |
+
self.act_on_output = act_on_output
|
| 66 |
+
self.num_label_samples = num_label_samples
|
| 67 |
+
|
| 68 |
+
act_fn = get_act_fn(act_name, act_norm)
|
| 69 |
+
self.ffn = Ffn(
|
| 70 |
+
self.widths, act_fn=act_fn, act_on_output=self.act_on_output
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def forward(
|
| 74 |
+
self,
|
| 75 |
+
case_params: Tensor, # Case parameters
|
| 76 |
+
t: Tensor,
|
| 77 |
+
label: Optional[Tensor] = None,
|
| 78 |
+
query_idxs: Optional[Tensor] = None,
|
| 79 |
+
) -> Dict[str, Tensor]:
|
| 80 |
+
"""
|
| 81 |
+
A faster forward by using all the points in the frame (`label`) at
|
| 82 |
+
time step `t` as training examples.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
- x_branch: (b, branch_dim), input to the branch net.
|
| 86 |
+
- t: (b), input to the trunk net, a batch of t
|
| 87 |
+
- label: (b, w, h), the frame to be predicted.
|
| 88 |
+
- query_idxs: (b, k, 2), the query locations.
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
batch_size, dim_in = case_params.shape
|
| 92 |
+
|
| 93 |
+
if query_idxs is None:
|
| 94 |
+
# Create k query locations on a lattice.
|
| 95 |
+
# (b, k, 2), where each element is (x, y)
|
| 96 |
+
assert label is not None
|
| 97 |
+
height, width = label.shape[-2:]
|
| 98 |
+
query_idxs = torch.stack(
|
| 99 |
+
[
|
| 100 |
+
torch.randint(
|
| 101 |
+
0,
|
| 102 |
+
height,
|
| 103 |
+
(self.num_label_samples,),
|
| 104 |
+
device=label.device,
|
| 105 |
+
),
|
| 106 |
+
torch.randint(
|
| 107 |
+
0,
|
| 108 |
+
width,
|
| 109 |
+
(self.num_label_samples,),
|
| 110 |
+
device=label.device,
|
| 111 |
+
),
|
| 112 |
+
],
|
| 113 |
+
dim=-1,
|
| 114 |
+
) # (k, 2)
|
| 115 |
+
|
| 116 |
+
# Concatenate query spatial coordinates with time coordinate
|
| 117 |
+
coords = query_idxs.unsqueeze(0) # (1, k, 2)
|
| 118 |
+
coords = coords.repeat(batch_size, 1, 1) # (b, k, 2)
|
| 119 |
+
num_queries = coords.shape[1]
|
| 120 |
+
t = t.unsqueeze(-1) # (b, 1)
|
| 121 |
+
t = t.repeat(1, num_queries, 1) # (b, k)
|
| 122 |
+
coords = torch.cat([coords, t], dim=-1) # (b, k, 3)
|
| 123 |
+
|
| 124 |
+
# Concatenate case params with queries
|
| 125 |
+
case_params = case_params.unsqueeze(1) # (b, 1, p)
|
| 126 |
+
case_params = case_params.repeat(1, num_queries, 1) # (b, k, p)
|
| 127 |
+
inp = torch.cat([case_params, coords], dim=-1) # (b, k, p + 3)
|
| 128 |
+
inp = inp.view(batch_size * num_queries, -1) # (bk, p + 3)
|
| 129 |
+
preds = self.ffn(inp) # (bk, out_dim)
|
| 130 |
+
preds = preds.view(batch_size, num_queries) # (b, k)
|
| 131 |
+
|
| 132 |
+
if label is not None:
|
| 133 |
+
# Use only the u channel
|
| 134 |
+
label = label[:, 0] # (B, w, h)
|
| 135 |
+
labels = label[:, query_idxs[:, 0], query_idxs[:, 1]] # (b, k)
|
| 136 |
+
assert (
|
| 137 |
+
preds.shape == labels.shape
|
| 138 |
+
), f"{preds.shape}, {labels.shape}"
|
| 139 |
+
loss = self.loss_fn(preds=preds, labels=labels) # (b, k)
|
| 140 |
+
return dict(
|
| 141 |
+
preds=preds,
|
| 142 |
+
loss=loss,
|
| 143 |
+
)
|
| 144 |
+
return dict(
|
| 145 |
+
preds=preds,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
def generate_one(
|
| 149 |
+
self, case_params: Tensor, t: Tensor, height: int, width: int
|
| 150 |
+
) -> Tensor:
|
| 151 |
+
"""
|
| 152 |
+
Generate one frame at time t.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
- x_branch: Tensor, (branch_dim)
|
| 156 |
+
- t: Tensor, (1,)
|
| 157 |
+
- height: int
|
| 158 |
+
- width: int
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
(b, c, h, w)
|
| 162 |
+
"""
|
| 163 |
+
if len(case_params.shape) == 1:
|
| 164 |
+
case_params = case_params.unsqueeze(0)
|
| 165 |
+
if len(t.shape) == 0:
|
| 166 |
+
t = t.unsqueeze(0).unsqueeze(0)
|
| 167 |
+
elif len(t.shape) == 1:
|
| 168 |
+
t = t.unsqueeze(0)
|
| 169 |
+
|
| 170 |
+
# Create 2D lattice of query points to infer the frame.
|
| 171 |
+
query_idxs = torch.tensor(
|
| 172 |
+
list(product(range(height), range(width))),
|
| 173 |
+
# dtype=torch.long,
|
| 174 |
+
device=case_params.device,
|
| 175 |
+
) # (h * w, 2)
|
| 176 |
+
|
| 177 |
+
# query_points = query_points / 100
|
| 178 |
+
# (b, 1, h * w)
|
| 179 |
+
output = self.forward(case_params, t=t, query_idxs=query_idxs)["preds"]
|
| 180 |
+
output = output.view(-1, 1, height, width) # (b, 1, h, w)
|
| 181 |
+
return output
|
model/fno/Adam.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
from typing import List
|
| 5 |
+
from torch.optim.optimizer import Optimizer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def adam(
|
| 9 |
+
params: List[Tensor],
|
| 10 |
+
grads: List[Tensor],
|
| 11 |
+
exp_avgs: List[Tensor],
|
| 12 |
+
exp_avg_sqs: List[Tensor],
|
| 13 |
+
max_exp_avg_sqs: List[Tensor],
|
| 14 |
+
state_steps: List[int],
|
| 15 |
+
*,
|
| 16 |
+
amsgrad: bool,
|
| 17 |
+
beta1: float,
|
| 18 |
+
beta2: float,
|
| 19 |
+
lr: float,
|
| 20 |
+
weight_decay: float,
|
| 21 |
+
eps: float
|
| 22 |
+
):
|
| 23 |
+
r"""Functional API that performs Adam algorithm computation.
|
| 24 |
+
See :class:`~torch.optim.Adam` for details.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
for i, param in enumerate(params):
|
| 28 |
+
grad = grads[i]
|
| 29 |
+
exp_avg = exp_avgs[i]
|
| 30 |
+
exp_avg_sq = exp_avg_sqs[i]
|
| 31 |
+
step = state_steps[i]
|
| 32 |
+
|
| 33 |
+
bias_correction1 = 1 - beta1**step
|
| 34 |
+
bias_correction2 = 1 - beta2**step
|
| 35 |
+
|
| 36 |
+
if weight_decay != 0:
|
| 37 |
+
grad = grad.add(param, alpha=weight_decay)
|
| 38 |
+
|
| 39 |
+
# Decay the first and second moment running average coefficient
|
| 40 |
+
exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
|
| 41 |
+
exp_avg_sq.mul_(beta2).addcmul_(grad, grad.conj(), value=1 - beta2)
|
| 42 |
+
if amsgrad:
|
| 43 |
+
# Maintains the maximum of all 2nd moment running avg. till now
|
| 44 |
+
torch.maximum(
|
| 45 |
+
max_exp_avg_sqs[i], exp_avg_sq, out=max_exp_avg_sqs[i]
|
| 46 |
+
)
|
| 47 |
+
# Use the max. for normalizing running avg. of gradient
|
| 48 |
+
denom = (
|
| 49 |
+
max_exp_avg_sqs[i].sqrt() / math.sqrt(bias_correction2)
|
| 50 |
+
).add_(eps)
|
| 51 |
+
else:
|
| 52 |
+
denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps)
|
| 53 |
+
|
| 54 |
+
step_size = lr / bias_correction1
|
| 55 |
+
|
| 56 |
+
param.addcdiv_(exp_avg, denom, value=-step_size)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class Adam(Optimizer):
|
| 60 |
+
r"""Implements Adam algorithm.
|
| 61 |
+
It has been proposed in `Adam: A Method for Stochastic Optimization`_.
|
| 62 |
+
The implementation of the L2 penalty follows changes proposed in
|
| 63 |
+
`Decoupled Weight Decay Regularization`_.
|
| 64 |
+
Args:
|
| 65 |
+
params (iterable): iterable of parameters to optimize or dicts defining
|
| 66 |
+
parameter groups
|
| 67 |
+
lr (float, optional): learning rate (default: 1e-3)
|
| 68 |
+
betas (Tuple[float, float], optional): coefficients used for computing
|
| 69 |
+
running averages of gradient and its square (default: (0.9, 0.999))
|
| 70 |
+
eps (float, optional): term added to the denominator to improve
|
| 71 |
+
numerical stability (default: 1e-8)
|
| 72 |
+
weight_decay (float, optional): weight decay (L2 penalty) (default: 0)
|
| 73 |
+
amsgrad (boolean, optional): whether to use the AMSGrad variant of this
|
| 74 |
+
algorithm from the paper `On the Convergence of Adam and Beyond`_
|
| 75 |
+
(default: False)
|
| 76 |
+
.. _Adam\: A Method for Stochastic Optimization:
|
| 77 |
+
https://arxiv.org/abs/1412.6980
|
| 78 |
+
.. _Decoupled Weight Decay Regularization:
|
| 79 |
+
https://arxiv.org/abs/1711.05101
|
| 80 |
+
.. _On the Convergence of Adam and Beyond:
|
| 81 |
+
https://openreview.net/forum?id=ryQu7f-RZ
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
def __init__(
|
| 85 |
+
self,
|
| 86 |
+
params,
|
| 87 |
+
lr=1e-3,
|
| 88 |
+
betas=(0.9, 0.999),
|
| 89 |
+
eps=1e-8,
|
| 90 |
+
weight_decay=0,
|
| 91 |
+
amsgrad=False,
|
| 92 |
+
):
|
| 93 |
+
if not 0.0 <= lr:
|
| 94 |
+
raise ValueError("Invalid learning rate: {}".format(lr))
|
| 95 |
+
if not 0.0 <= eps:
|
| 96 |
+
raise ValueError("Invalid epsilon value: {}".format(eps))
|
| 97 |
+
if not 0.0 <= betas[0] < 1.0:
|
| 98 |
+
raise ValueError(
|
| 99 |
+
"Invalid beta parameter at index 0: {}".format(betas[0])
|
| 100 |
+
)
|
| 101 |
+
if not 0.0 <= betas[1] < 1.0:
|
| 102 |
+
raise ValueError(
|
| 103 |
+
"Invalid beta parameter at index 1: {}".format(betas[1])
|
| 104 |
+
)
|
| 105 |
+
if not 0.0 <= weight_decay:
|
| 106 |
+
raise ValueError(
|
| 107 |
+
"Invalid weight_decay value: {}".format(weight_decay)
|
| 108 |
+
)
|
| 109 |
+
defaults = dict(
|
| 110 |
+
lr=lr,
|
| 111 |
+
betas=betas,
|
| 112 |
+
eps=eps,
|
| 113 |
+
weight_decay=weight_decay,
|
| 114 |
+
amsgrad=amsgrad,
|
| 115 |
+
)
|
| 116 |
+
super(Adam, self).__init__(params, defaults)
|
| 117 |
+
|
| 118 |
+
def __setstate__(self, state):
|
| 119 |
+
super(Adam, self).__setstate__(state)
|
| 120 |
+
for group in self.param_groups:
|
| 121 |
+
group.setdefault("amsgrad", False)
|
| 122 |
+
|
| 123 |
+
@torch.no_grad()
|
| 124 |
+
def step(self, closure=None):
|
| 125 |
+
"""Performs a single optimization step.
|
| 126 |
+
Args:
|
| 127 |
+
closure (callable, optional): A closure that reevaluates the model
|
| 128 |
+
and returns the loss.
|
| 129 |
+
"""
|
| 130 |
+
loss = None
|
| 131 |
+
if closure is not None:
|
| 132 |
+
with torch.enable_grad():
|
| 133 |
+
loss = closure()
|
| 134 |
+
|
| 135 |
+
for group in self.param_groups:
|
| 136 |
+
params_with_grad = []
|
| 137 |
+
grads = []
|
| 138 |
+
exp_avgs = []
|
| 139 |
+
exp_avg_sqs = []
|
| 140 |
+
max_exp_avg_sqs = []
|
| 141 |
+
state_steps = []
|
| 142 |
+
beta1, beta2 = group["betas"]
|
| 143 |
+
|
| 144 |
+
for p in group["params"]:
|
| 145 |
+
if p.grad is not None:
|
| 146 |
+
params_with_grad.append(p)
|
| 147 |
+
if p.grad.is_sparse:
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
"Adam does not support sparse gradients,"
|
| 150 |
+
" please consider SparseAdam instead"
|
| 151 |
+
)
|
| 152 |
+
grads.append(p.grad)
|
| 153 |
+
|
| 154 |
+
state = self.state[p]
|
| 155 |
+
# Lazy state initialization
|
| 156 |
+
if len(state) == 0:
|
| 157 |
+
state["step"] = 0
|
| 158 |
+
# Exponential moving average of gradient values
|
| 159 |
+
state["exp_avg"] = torch.zeros_like(
|
| 160 |
+
p, memory_format=torch.preserve_format
|
| 161 |
+
)
|
| 162 |
+
# Exponential moving average of squared gradient values
|
| 163 |
+
state["exp_avg_sq"] = torch.zeros_like(
|
| 164 |
+
p, memory_format=torch.preserve_format
|
| 165 |
+
)
|
| 166 |
+
if group["amsgrad"]:
|
| 167 |
+
# Maintains max of all exp. moving avg. of
|
| 168 |
+
# sq. grad. values
|
| 169 |
+
state["max_exp_avg_sq"] = torch.zeros_like(
|
| 170 |
+
p, memory_format=torch.preserve_format
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
exp_avgs.append(state["exp_avg"])
|
| 174 |
+
exp_avg_sqs.append(state["exp_avg_sq"])
|
| 175 |
+
|
| 176 |
+
if group["amsgrad"]:
|
| 177 |
+
max_exp_avg_sqs.append(state["max_exp_avg_sq"])
|
| 178 |
+
|
| 179 |
+
# update the steps for each param group update
|
| 180 |
+
state["step"] += 1
|
| 181 |
+
# record the step after step update
|
| 182 |
+
state_steps.append(state["step"])
|
| 183 |
+
|
| 184 |
+
adam(
|
| 185 |
+
params_with_grad,
|
| 186 |
+
grads,
|
| 187 |
+
exp_avgs,
|
| 188 |
+
exp_avg_sqs,
|
| 189 |
+
max_exp_avg_sqs,
|
| 190 |
+
state_steps,
|
| 191 |
+
amsgrad=group["amsgrad"],
|
| 192 |
+
beta1=beta1,
|
| 193 |
+
beta2=beta2,
|
| 194 |
+
lr=group["lr"],
|
| 195 |
+
weight_decay=group["weight_decay"],
|
| 196 |
+
eps=group["eps"],
|
| 197 |
+
)
|
| 198 |
+
return loss
|
model/fno/__init__.py
ADDED
|
File without changes
|
model/fno/fno2d.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
from torch import Tensor
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
from ..base_model import AutoCfdModel
|
| 9 |
+
|
| 10 |
+
from onescience.modules.fourier.fno_layers import SpectralConv2d
|
| 11 |
+
torch.manual_seed(0)
|
| 12 |
+
np.random.seed(0)
|
| 13 |
+
|
| 14 |
+
class FnoBlock(nn.Module):
|
| 15 |
+
"""
|
| 16 |
+
FNO 块 (Fourier Neural Operator Block)。
|
| 17 |
+
|
| 18 |
+
包含一个 2D 频域卷积路径和一个 1x1 的空间卷积残差路径,
|
| 19 |
+
最后加上可选的激活函数。
|
| 20 |
+
"""
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
in_chan: int,
|
| 24 |
+
out_chan: int,
|
| 25 |
+
modes1: int,
|
| 26 |
+
modes2: int,
|
| 27 |
+
act_fn: Optional[nn.Module] = None,
|
| 28 |
+
):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.in_chan = in_chan
|
| 31 |
+
self.out_chan = out_chan
|
| 32 |
+
self.modes1 = modes1
|
| 33 |
+
self.modes2 = modes2
|
| 34 |
+
self.act_fn = act_fn
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
self.conv0 = SpectralConv2d(
|
| 38 |
+
in_channels=self.in_chan,
|
| 39 |
+
out_channels=self.out_chan,
|
| 40 |
+
modes1=self.modes1,
|
| 41 |
+
modes2=self.modes2
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
self.w0 = nn.Conv2d(self.in_chan, self.out_chan, 1)
|
| 45 |
+
|
| 46 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 47 |
+
|
| 48 |
+
x1 = self.conv0(x)
|
| 49 |
+
x2 = self.w0(x)
|
| 50 |
+
x = x1 + x2
|
| 51 |
+
|
| 52 |
+
if self.act_fn is not None:
|
| 53 |
+
x = self.act_fn(x)
|
| 54 |
+
|
| 55 |
+
return x
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class Fno2d(AutoCfdModel):
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
in_chan: int,
|
| 62 |
+
out_chan: int,
|
| 63 |
+
n_case_params: int,
|
| 64 |
+
loss_fn: nn.Module,
|
| 65 |
+
num_layers: int,
|
| 66 |
+
modes1: int = 12,
|
| 67 |
+
modes2: int = 12,
|
| 68 |
+
hidden_dim: int = 20,
|
| 69 |
+
padding: Optional[int] = None,
|
| 70 |
+
):
|
| 71 |
+
super().__init__(loss_fn)
|
| 72 |
+
|
| 73 |
+
"""
|
| 74 |
+
The overall network. It contains 4 layers of the Fourier layer.
|
| 75 |
+
1. Lift the input to the desire channel dimension by self.fc0 .
|
| 76 |
+
2. 4 layers of the integral operators u' = (W + K)(u).
|
| 77 |
+
W defined by self.w; K defined by self.conv .
|
| 78 |
+
3. Project from the channel space to the output space by self.fc1
|
| 79 |
+
and self.fc2 .
|
| 80 |
+
"""
|
| 81 |
+
self.in_chan = in_chan
|
| 82 |
+
self.out_chan = out_chan
|
| 83 |
+
self.n_case_params = n_case_params
|
| 84 |
+
self.num_layers = num_layers
|
| 85 |
+
self.modes1 = modes1
|
| 86 |
+
self.modes2 = modes2
|
| 87 |
+
self.hidden_dim = hidden_dim
|
| 88 |
+
self.padding = padding # pad the domain if input is non-periodic
|
| 89 |
+
|
| 90 |
+
self.act_fn = nn.GELU()
|
| 91 |
+
# Channel projection into `hidden_dim` channels
|
| 92 |
+
# +1 for mask, +2 for coordinates
|
| 93 |
+
self.fc0 = nn.Conv2d(
|
| 94 |
+
in_chan + 1 + 2 + n_case_params,
|
| 95 |
+
self.hidden_dim,
|
| 96 |
+
1,
|
| 97 |
+
1,
|
| 98 |
+
0,
|
| 99 |
+
)
|
| 100 |
+
# input channel is 12: the solution of the previous 10
|
| 101 |
+
# timesteps + 2 locations
|
| 102 |
+
# (u(t-10, x, y), ..., u(t-1, x, y), x, y)
|
| 103 |
+
|
| 104 |
+
# FNO blocks
|
| 105 |
+
blocks = []
|
| 106 |
+
for _ in range(self.num_layers):
|
| 107 |
+
blocks.append(
|
| 108 |
+
FnoBlock(
|
| 109 |
+
self.hidden_dim,
|
| 110 |
+
self.hidden_dim,
|
| 111 |
+
self.modes1,
|
| 112 |
+
self.modes2,
|
| 113 |
+
self.act_fn,
|
| 114 |
+
)
|
| 115 |
+
)
|
| 116 |
+
self.blocks = nn.Sequential(*blocks)
|
| 117 |
+
|
| 118 |
+
self.fc1 = nn.Conv2d(self.hidden_dim, 128, 1, 1, 0)
|
| 119 |
+
self.fc2 = nn.Conv2d(128, self.out_chan, 1, 1, 0)
|
| 120 |
+
|
| 121 |
+
def forward(
|
| 122 |
+
self,
|
| 123 |
+
inputs: Tensor,
|
| 124 |
+
case_params: Tensor,
|
| 125 |
+
mask: Optional[Tensor] = None,
|
| 126 |
+
label: Optional[Tensor] = None,
|
| 127 |
+
) -> Dict:
|
| 128 |
+
"""
|
| 129 |
+
Args:
|
| 130 |
+
- input: (b, c, h, w)
|
| 131 |
+
- labels: (b, c, h, w)
|
| 132 |
+
- mask: (b, h, w), a binary mask for indicating the geometry
|
| 133 |
+
1 for interior, 0 for obstacles.
|
| 134 |
+
|
| 135 |
+
Returns: a tensor of shape (b, c, h, w), the solution at
|
| 136 |
+
the next timestep
|
| 137 |
+
"""
|
| 138 |
+
batch_size, n_chan, height, width = inputs.shape
|
| 139 |
+
|
| 140 |
+
if mask is None:
|
| 141 |
+
# When there is no mask, we assume that there is no obstacles.
|
| 142 |
+
mask = torch.ones((batch_size, 1, height, width)).to(inputs.device)
|
| 143 |
+
else:
|
| 144 |
+
if mask.dim() == 3: # (B, h, w)
|
| 145 |
+
mask = mask.unsqueeze(1) # (B, 1, h, w)
|
| 146 |
+
inputs = torch.cat([inputs, mask], dim=1) # (B, c + 1, h, w)
|
| 147 |
+
|
| 148 |
+
# Physical properties
|
| 149 |
+
props = case_params # (B, p)
|
| 150 |
+
props = props.unsqueeze(-1).unsqueeze(-1) # (B, p, 1, 1)
|
| 151 |
+
props = props.repeat(1, 1, height, width) # (B, p, H, W)
|
| 152 |
+
|
| 153 |
+
# Append (x, y) coordinates to every location
|
| 154 |
+
grid = self.get_coords(inputs.shape, inputs.device) # (b, 2, h, w)
|
| 155 |
+
inputs = torch.cat(
|
| 156 |
+
(inputs, grid, props), dim=1
|
| 157 |
+
) # (b, c + 2 + 2, h, w)
|
| 158 |
+
|
| 159 |
+
# Project channels
|
| 160 |
+
inputs = self.fc0(inputs) # (b, hidden_dim, h, w)
|
| 161 |
+
# x = x.permute(0, 3, 1, 2) # (b, c, h, w)?
|
| 162 |
+
if self.padding is not None:
|
| 163 |
+
# pad the domain if input is non-periodic
|
| 164 |
+
inputs = F.pad(inputs, [0, self.padding, 0, self.padding])
|
| 165 |
+
|
| 166 |
+
inputs = self.blocks(inputs) # (b, hidden_dim, h, w)
|
| 167 |
+
if self.padding is not None:
|
| 168 |
+
# pad the domain if inputis non-periodic
|
| 169 |
+
inputs = inputs[..., : -self.padding, : -self.padding]
|
| 170 |
+
|
| 171 |
+
inputs = self.fc1(inputs) # (b, 128, h, w)
|
| 172 |
+
inputs = self.act_fn(inputs)
|
| 173 |
+
preds = self.fc2(inputs) # (b, c_out, h, w)
|
| 174 |
+
|
| 175 |
+
# Masked locations are not prediction
|
| 176 |
+
preds = preds * mask
|
| 177 |
+
|
| 178 |
+
if label is not None:
|
| 179 |
+
label = label * mask
|
| 180 |
+
loss = self.loss_fn(preds=preds, labels=label)
|
| 181 |
+
return dict(
|
| 182 |
+
preds=preds,
|
| 183 |
+
loss=loss,
|
| 184 |
+
)
|
| 185 |
+
return dict(preds=preds)
|
| 186 |
+
|
| 187 |
+
def get_coords(self, shape, device):
|
| 188 |
+
"""
|
| 189 |
+
Return a tensor of shape (b, 2, h, w) such that the element at
|
| 190 |
+
[:, :, i, j] is the (x, y) coordinates at the grid location (i, j).
|
| 191 |
+
"""
|
| 192 |
+
bsz, c, size_x, size_y = shape
|
| 193 |
+
grid_x = torch.tensor(np.linspace(0, 1, size_x), dtype=torch.float)
|
| 194 |
+
grid_x = grid_x.reshape(1, 1, size_x, 1).repeat([bsz, 1, 1, size_y])
|
| 195 |
+
grid_y = torch.tensor(np.linspace(0, 1, size_y), dtype=torch.float)
|
| 196 |
+
grid_y = grid_y.reshape(1, 1, 1, size_y).repeat([bsz, 1, size_x, 1])
|
| 197 |
+
coords = torch.cat([grid_x, grid_y], dim=1).to(device) # (b, 2, h, w)
|
| 198 |
+
return coords
|
| 199 |
+
|
| 200 |
+
def generate(
|
| 201 |
+
self,
|
| 202 |
+
inputs: Tensor,
|
| 203 |
+
case_params: Tensor,
|
| 204 |
+
mask: Optional[Tensor] = None,
|
| 205 |
+
) -> Tensor:
|
| 206 |
+
outputs = self.forward(
|
| 207 |
+
inputs=inputs, case_params=case_params, mask=mask
|
| 208 |
+
) # (b, c, h, w)
|
| 209 |
+
preds = outputs["preds"]
|
| 210 |
+
return preds
|
| 211 |
+
|
| 212 |
+
def generate_many(
|
| 213 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int
|
| 214 |
+
) -> List[Tensor]:
|
| 215 |
+
"""
|
| 216 |
+
Args:
|
| 217 |
+
x (Tensor): (c, h, w)
|
| 218 |
+
case_params (Tensor): (p)
|
| 219 |
+
mask (Tensor): (h, w), 1 for interior, 0 for obstacles.
|
| 220 |
+
Returns:
|
| 221 |
+
output: (steps, c, h, w)
|
| 222 |
+
"""
|
| 223 |
+
assert len(inputs.shape) == len(case_params.shape) + 2
|
| 224 |
+
if inputs.dim() == 3:
|
| 225 |
+
# Add a dimension for batch size of 1
|
| 226 |
+
inputs = inputs.unsqueeze(0)
|
| 227 |
+
case_params = case_params.unsqueeze(0)
|
| 228 |
+
mask = mask.unsqueeze(0)
|
| 229 |
+
assert inputs.shape[0] == case_params.shape[0] == mask.shape[0]
|
| 230 |
+
|
| 231 |
+
cur_frame = inputs # (b, c, h, w)
|
| 232 |
+
preds = []
|
| 233 |
+
for _ in range(steps):
|
| 234 |
+
cur_frame = self.generate(
|
| 235 |
+
inputs=cur_frame, case_params=case_params, mask=mask
|
| 236 |
+
)
|
| 237 |
+
preds.append(cur_frame)
|
| 238 |
+
return preds
|
model/fno/utilities3.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import numpy as np
|
| 3 |
+
import scipy.io
|
| 4 |
+
import h5py
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
|
| 7 |
+
import operator
|
| 8 |
+
from functools import reduce
|
| 9 |
+
|
| 10 |
+
#################################################
|
| 11 |
+
#
|
| 12 |
+
# Utilities
|
| 13 |
+
#
|
| 14 |
+
#################################################
|
| 15 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MatReader(object):
|
| 19 |
+
"""reading data"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, file_path, to_torch=True, to_cuda=False, to_float=True):
|
| 22 |
+
super(MatReader, self).__init__()
|
| 23 |
+
|
| 24 |
+
self.to_torch = to_torch
|
| 25 |
+
self.to_cuda = to_cuda
|
| 26 |
+
self.to_float = to_float
|
| 27 |
+
|
| 28 |
+
self.file_path = file_path
|
| 29 |
+
|
| 30 |
+
self.data = None
|
| 31 |
+
self.old_mat = None
|
| 32 |
+
self._load_file()
|
| 33 |
+
|
| 34 |
+
def _load_file(self):
|
| 35 |
+
try:
|
| 36 |
+
self.data = scipy.io.loadmat(self.file_path)
|
| 37 |
+
self.old_mat = True
|
| 38 |
+
except Exception:
|
| 39 |
+
self.data = h5py.File(self.file_path)
|
| 40 |
+
self.old_mat = False
|
| 41 |
+
|
| 42 |
+
def load_file(self, file_path):
|
| 43 |
+
self.file_path = file_path
|
| 44 |
+
self._load_file()
|
| 45 |
+
|
| 46 |
+
def read_field(self, field):
|
| 47 |
+
x = self.data[field] # type: ignore
|
| 48 |
+
|
| 49 |
+
if not self.old_mat:
|
| 50 |
+
x = x[()] # type: ignore
|
| 51 |
+
x = np.transpose(x, axes=range(len(x.shape) - 1, -1, -1)) # type: ignore # noqa
|
| 52 |
+
|
| 53 |
+
if self.to_float:
|
| 54 |
+
x = x.astype(np.float32) # type: ignore
|
| 55 |
+
|
| 56 |
+
if self.to_torch:
|
| 57 |
+
x = torch.from_numpy(x)
|
| 58 |
+
|
| 59 |
+
if self.to_cuda:
|
| 60 |
+
x = x.cuda()
|
| 61 |
+
|
| 62 |
+
return x
|
| 63 |
+
|
| 64 |
+
def set_cuda(self, to_cuda):
|
| 65 |
+
self.to_cuda = to_cuda
|
| 66 |
+
|
| 67 |
+
def set_torch(self, to_torch):
|
| 68 |
+
self.to_torch = to_torch
|
| 69 |
+
|
| 70 |
+
def set_float(self, to_float):
|
| 71 |
+
self.to_float = to_float
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# normalization, pointwise gaussian
|
| 75 |
+
class UnitGaussianNormalizer(object):
|
| 76 |
+
def __init__(self, x, eps=0.00001):
|
| 77 |
+
super(UnitGaussianNormalizer, self).__init__()
|
| 78 |
+
|
| 79 |
+
# x could be in shape of ntrain*n or ntrain*T*n or ntrain*n*T
|
| 80 |
+
self.mean = torch.mean(x, 0)
|
| 81 |
+
self.std = torch.std(x, 0)
|
| 82 |
+
self.eps = eps
|
| 83 |
+
|
| 84 |
+
def encode(self, x):
|
| 85 |
+
x = (x - self.mean) / (self.std + self.eps)
|
| 86 |
+
return x
|
| 87 |
+
|
| 88 |
+
def decode(self, x, sample_idx=None):
|
| 89 |
+
if sample_idx is None:
|
| 90 |
+
std = self.std + self.eps # n
|
| 91 |
+
mean = self.mean
|
| 92 |
+
else:
|
| 93 |
+
if len(self.mean.shape) == len(sample_idx[0].shape):
|
| 94 |
+
std = self.std[sample_idx] + self.eps # batch*n
|
| 95 |
+
mean = self.mean[sample_idx]
|
| 96 |
+
if len(self.mean.shape) > len(sample_idx[0].shape):
|
| 97 |
+
std = self.std[:, sample_idx] + self.eps # T*batch*n
|
| 98 |
+
mean = self.mean[:, sample_idx]
|
| 99 |
+
|
| 100 |
+
# x is in shape of batch*n or T*batch*n
|
| 101 |
+
x = (x * std) + mean # type: ignore
|
| 102 |
+
return x
|
| 103 |
+
|
| 104 |
+
def cuda(self):
|
| 105 |
+
self.mean = self.mean.cuda()
|
| 106 |
+
self.std = self.std.cuda()
|
| 107 |
+
|
| 108 |
+
def cpu(self):
|
| 109 |
+
self.mean = self.mean.cpu()
|
| 110 |
+
self.std = self.std.cpu()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# normalization, Gaussian
|
| 114 |
+
class GaussianNormalizer(object):
|
| 115 |
+
def __init__(self, x, eps=0.00001):
|
| 116 |
+
super(GaussianNormalizer, self).__init__()
|
| 117 |
+
|
| 118 |
+
self.mean = torch.mean(x)
|
| 119 |
+
self.std = torch.std(x)
|
| 120 |
+
self.eps = eps
|
| 121 |
+
|
| 122 |
+
def encode(self, x):
|
| 123 |
+
x = (x - self.mean) / (self.std + self.eps)
|
| 124 |
+
return x
|
| 125 |
+
|
| 126 |
+
def decode(self, x, sample_idx=None):
|
| 127 |
+
x = (x * (self.std + self.eps)) + self.mean
|
| 128 |
+
return x
|
| 129 |
+
|
| 130 |
+
def cuda(self):
|
| 131 |
+
self.mean = self.mean.cuda()
|
| 132 |
+
self.std = self.std.cuda()
|
| 133 |
+
|
| 134 |
+
def cpu(self):
|
| 135 |
+
self.mean = self.mean.cpu()
|
| 136 |
+
self.std = self.std.cpu()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# normalization, scaling by range
|
| 140 |
+
class RangeNormalizer(object):
|
| 141 |
+
def __init__(self, x, low=0.0, high=1.0):
|
| 142 |
+
super(RangeNormalizer, self).__init__()
|
| 143 |
+
mymin = torch.min(x, 0)[0].view(-1)
|
| 144 |
+
mymax = torch.max(x, 0)[0].view(-1)
|
| 145 |
+
|
| 146 |
+
self.a = (high - low) / (mymax - mymin)
|
| 147 |
+
self.b = -self.a * mymax + high
|
| 148 |
+
|
| 149 |
+
def encode(self, x):
|
| 150 |
+
s = x.size()
|
| 151 |
+
x = x.view(s[0], -1)
|
| 152 |
+
x = self.a * x + self.b
|
| 153 |
+
x = x.view(s)
|
| 154 |
+
return x
|
| 155 |
+
|
| 156 |
+
def decode(self, x):
|
| 157 |
+
s = x.size()
|
| 158 |
+
x = x.view(s[0], -1)
|
| 159 |
+
x = (x - self.b) / self.a
|
| 160 |
+
x = x.view(s)
|
| 161 |
+
return x
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# loss function with rel/abs Lp loss
|
| 165 |
+
class LpLoss(object):
|
| 166 |
+
def __init__(self, d=2, p=2, size_average=True, reduction=True):
|
| 167 |
+
super(LpLoss, self).__init__()
|
| 168 |
+
|
| 169 |
+
# Dimension and Lp-norm type are postive
|
| 170 |
+
assert d > 0 and p > 0
|
| 171 |
+
|
| 172 |
+
self.d = d
|
| 173 |
+
self.p = p
|
| 174 |
+
self.reduction = reduction
|
| 175 |
+
self.size_average = size_average
|
| 176 |
+
|
| 177 |
+
def abs(self, x, y):
|
| 178 |
+
num_examples = x.size()[0]
|
| 179 |
+
|
| 180 |
+
# Assume uniform mesh
|
| 181 |
+
h = 1.0 / (x.size()[1] - 1.0)
|
| 182 |
+
|
| 183 |
+
all_norms = (h ** (self.d / self.p)) * torch.norm(
|
| 184 |
+
x.view(num_examples, -1) - y.view(num_examples, -1), self.p, 1
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
if self.reduction:
|
| 188 |
+
if self.size_average:
|
| 189 |
+
return torch.mean(all_norms)
|
| 190 |
+
else:
|
| 191 |
+
return torch.sum(all_norms)
|
| 192 |
+
|
| 193 |
+
return all_norms
|
| 194 |
+
|
| 195 |
+
def rel(self, x, y):
|
| 196 |
+
num_examples = x.size()[0]
|
| 197 |
+
|
| 198 |
+
diff_norms = torch.norm(
|
| 199 |
+
x.reshape(num_examples, -1) - y.reshape(num_examples, -1),
|
| 200 |
+
self.p,
|
| 201 |
+
1,
|
| 202 |
+
)
|
| 203 |
+
y_norms = torch.norm(y.reshape(num_examples, -1), self.p, 1)
|
| 204 |
+
|
| 205 |
+
if self.reduction:
|
| 206 |
+
if self.size_average:
|
| 207 |
+
return torch.mean(diff_norms / y_norms)
|
| 208 |
+
else:
|
| 209 |
+
return torch.sum(diff_norms / y_norms)
|
| 210 |
+
|
| 211 |
+
return diff_norms / y_norms
|
| 212 |
+
|
| 213 |
+
def __call__(self, x, y):
|
| 214 |
+
return self.rel(x, y)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# Sobolev norm (HS norm)
|
| 218 |
+
# where we also compare the numerical derivatives between the output and target
|
| 219 |
+
class HsLoss(object):
|
| 220 |
+
def __init__(
|
| 221 |
+
self,
|
| 222 |
+
d=2,
|
| 223 |
+
p=2,
|
| 224 |
+
k=1,
|
| 225 |
+
a=None,
|
| 226 |
+
group=False,
|
| 227 |
+
size_average=True,
|
| 228 |
+
reduction=True,
|
| 229 |
+
):
|
| 230 |
+
super(HsLoss, self).__init__()
|
| 231 |
+
|
| 232 |
+
# Dimension and Lp-norm type are postive
|
| 233 |
+
assert d > 0 and p > 0
|
| 234 |
+
|
| 235 |
+
self.d = d
|
| 236 |
+
self.p = p
|
| 237 |
+
self.k = k
|
| 238 |
+
self.balanced = group
|
| 239 |
+
self.reduction = reduction
|
| 240 |
+
self.size_average = size_average
|
| 241 |
+
|
| 242 |
+
if a is None:
|
| 243 |
+
a = [
|
| 244 |
+
1,
|
| 245 |
+
] * k
|
| 246 |
+
self.a = a
|
| 247 |
+
|
| 248 |
+
def rel(self, x, y):
|
| 249 |
+
num_examples = x.size()[0]
|
| 250 |
+
diff_norms = torch.norm(
|
| 251 |
+
x.reshape(num_examples, -1) - y.reshape(num_examples, -1),
|
| 252 |
+
self.p,
|
| 253 |
+
1,
|
| 254 |
+
)
|
| 255 |
+
y_norms = torch.norm(y.reshape(num_examples, -1), self.p, 1)
|
| 256 |
+
if self.reduction:
|
| 257 |
+
if self.size_average:
|
| 258 |
+
return torch.mean(diff_norms / y_norms)
|
| 259 |
+
else:
|
| 260 |
+
return torch.sum(diff_norms / y_norms)
|
| 261 |
+
return diff_norms / y_norms
|
| 262 |
+
|
| 263 |
+
def __call__(self, x, y, a=None):
|
| 264 |
+
nx = x.size()[1]
|
| 265 |
+
ny = x.size()[2]
|
| 266 |
+
k = self.k
|
| 267 |
+
balanced = self.balanced
|
| 268 |
+
a = self.a
|
| 269 |
+
x = x.view(x.shape[0], nx, ny, -1)
|
| 270 |
+
y = y.view(y.shape[0], nx, ny, -1)
|
| 271 |
+
|
| 272 |
+
k_x = (
|
| 273 |
+
torch.cat(
|
| 274 |
+
(
|
| 275 |
+
torch.arange(start=0, end=nx // 2, step=1),
|
| 276 |
+
torch.arange(start=-nx // 2, end=0, step=1),
|
| 277 |
+
),
|
| 278 |
+
0,
|
| 279 |
+
)
|
| 280 |
+
.reshape(nx, 1)
|
| 281 |
+
.repeat(1, ny)
|
| 282 |
+
)
|
| 283 |
+
k_y = (
|
| 284 |
+
torch.cat(
|
| 285 |
+
(
|
| 286 |
+
torch.arange(start=0, end=ny // 2, step=1),
|
| 287 |
+
torch.arange(start=-ny // 2, end=0, step=1),
|
| 288 |
+
),
|
| 289 |
+
0,
|
| 290 |
+
)
|
| 291 |
+
.reshape(1, ny)
|
| 292 |
+
.repeat(nx, 1)
|
| 293 |
+
)
|
| 294 |
+
k_x = torch.abs(k_x).reshape(1, nx, ny, 1).to(x.device)
|
| 295 |
+
k_y = torch.abs(k_y).reshape(1, nx, ny, 1).to(x.device)
|
| 296 |
+
|
| 297 |
+
x = torch.fft.fftn(x, dim=[1, 2])
|
| 298 |
+
y = torch.fft.fftn(y, dim=[1, 2])
|
| 299 |
+
|
| 300 |
+
if not balanced:
|
| 301 |
+
weight = 1
|
| 302 |
+
if k >= 1:
|
| 303 |
+
weight += a[0] ** 2 * (k_x**2 + k_y**2)
|
| 304 |
+
if k >= 2:
|
| 305 |
+
weight += a[1] ** 2 * (
|
| 306 |
+
k_x**4 + 2 * k_x**2 * k_y**2 + k_y**4
|
| 307 |
+
)
|
| 308 |
+
weight = torch.sqrt(weight) # type: ignore
|
| 309 |
+
loss = self.rel(x * weight, y * weight)
|
| 310 |
+
else:
|
| 311 |
+
loss = self.rel(x, y)
|
| 312 |
+
if k >= 1:
|
| 313 |
+
weight = a[0] * torch.sqrt(k_x**2 + k_y**2)
|
| 314 |
+
loss += self.rel(x * weight, y * weight)
|
| 315 |
+
if k >= 2:
|
| 316 |
+
weight = a[1] * torch.sqrt(
|
| 317 |
+
k_x**4 + 2 * k_x**2 * k_y**2 + k_y**4
|
| 318 |
+
)
|
| 319 |
+
loss += self.rel(x * weight, y * weight)
|
| 320 |
+
loss = loss / (k + 1)
|
| 321 |
+
|
| 322 |
+
return loss
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
# A simple feedforward neural network
|
| 326 |
+
class DenseNet(torch.nn.Module):
|
| 327 |
+
def __init__(
|
| 328 |
+
self, layers, nonlinearity, out_nonlinearity=None, normalize=False
|
| 329 |
+
):
|
| 330 |
+
super(DenseNet, self).__init__()
|
| 331 |
+
|
| 332 |
+
self.n_layers = len(layers) - 1
|
| 333 |
+
|
| 334 |
+
assert self.n_layers >= 1
|
| 335 |
+
|
| 336 |
+
self.layers = nn.ModuleList()
|
| 337 |
+
|
| 338 |
+
for j in range(self.n_layers):
|
| 339 |
+
self.layers.append(nn.Linear(layers[j], layers[j + 1]))
|
| 340 |
+
|
| 341 |
+
if j != self.n_layers - 1:
|
| 342 |
+
if normalize:
|
| 343 |
+
self.layers.append(nn.BatchNorm1d(layers[j + 1]))
|
| 344 |
+
|
| 345 |
+
self.layers.append(nonlinearity())
|
| 346 |
+
|
| 347 |
+
if out_nonlinearity is not None:
|
| 348 |
+
self.layers.append(out_nonlinearity())
|
| 349 |
+
|
| 350 |
+
def forward(self, x):
|
| 351 |
+
for _, l in enumerate(self.layers):
|
| 352 |
+
x = l(x)
|
| 353 |
+
|
| 354 |
+
return x
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
# print the number of parameters
|
| 358 |
+
def count_params(model):
|
| 359 |
+
c = 0
|
| 360 |
+
for p in list(model.parameters()):
|
| 361 |
+
c += reduce(
|
| 362 |
+
operator.mul, list(p.size() + (2,) if p.is_complex() else p.size())
|
| 363 |
+
)
|
| 364 |
+
return c
|
model/loss.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import Tensor, nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MseLoss(nn.Module):
|
| 9 |
+
def __init__(self, normalize: bool, is_masked: bool = False):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.normalize = normalize
|
| 12 |
+
self.is_masked = is_masked
|
| 13 |
+
|
| 14 |
+
def get_score_names(self) -> List[str]:
|
| 15 |
+
names = [
|
| 16 |
+
'mse', 'rmse', 'mae'
|
| 17 |
+
]
|
| 18 |
+
if self.normalize:
|
| 19 |
+
names += ['nmse']
|
| 20 |
+
return names
|
| 21 |
+
|
| 22 |
+
def forward(self, preds: Tensor, labels: Tensor) -> dict[str, Tensor]:
|
| 23 |
+
'''
|
| 24 |
+
Args:
|
| 25 |
+
- mask: 1 for valid pixels, 0 for invalid pixels.
|
| 26 |
+
'''
|
| 27 |
+
mse = F.mse_loss(input=preds, target=labels)
|
| 28 |
+
mae = F.l1_loss(input=preds, target=labels)
|
| 29 |
+
result = dict(
|
| 30 |
+
mse=mse,
|
| 31 |
+
rmse=torch.sqrt(mse),
|
| 32 |
+
mae=mae,
|
| 33 |
+
)
|
| 34 |
+
if self.normalize:
|
| 35 |
+
result['nmse'] = mse / torch.square(labels).mean()
|
| 36 |
+
# result['mre'] = mae / torch.abs(labels).mean()
|
| 37 |
+
return result
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def loss_name_to_fn(name: str, masked: bool = False) -> MseLoss:
|
| 41 |
+
name = name.lower()
|
| 42 |
+
if masked:
|
| 43 |
+
raise NotImplementedError
|
| 44 |
+
else:
|
| 45 |
+
if name == 'mse':
|
| 46 |
+
return MseLoss(normalize=False)
|
| 47 |
+
elif name == 'nmse':
|
| 48 |
+
return MseLoss(normalize=True)
|
| 49 |
+
else:
|
| 50 |
+
raise NotImplementedError
|
model/resnet.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import nn, Tensor
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
from .base_model import AutoCfdModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ResidualBlock(nn.Module):
|
| 11 |
+
def __init__(
|
| 12 |
+
self,
|
| 13 |
+
in_chan: int,
|
| 14 |
+
out_chan: int,
|
| 15 |
+
hidden_chan: int,
|
| 16 |
+
kernel_size: int,
|
| 17 |
+
stride: int = 1,
|
| 18 |
+
padding: int = 0,
|
| 19 |
+
dropout_rate: float = 0.2,
|
| 20 |
+
bias: bool = True,
|
| 21 |
+
use_1x1conv: bool = False,
|
| 22 |
+
):
|
| 23 |
+
super().__init__()
|
| 24 |
+
if in_chan != out_chan:
|
| 25 |
+
assert use_1x1conv
|
| 26 |
+
self.in_chan = in_chan
|
| 27 |
+
self.out_chan = out_chan
|
| 28 |
+
self.hidden_chan = hidden_chan
|
| 29 |
+
self.kernel_size = kernel_size
|
| 30 |
+
self.stride = stride
|
| 31 |
+
self.padding = padding
|
| 32 |
+
self.bias = bias
|
| 33 |
+
|
| 34 |
+
# Sub-modules
|
| 35 |
+
self.conv1 = nn.Conv2d(
|
| 36 |
+
in_chan,
|
| 37 |
+
hidden_chan,
|
| 38 |
+
kernel_size,
|
| 39 |
+
stride,
|
| 40 |
+
padding,
|
| 41 |
+
bias=bias,
|
| 42 |
+
padding_mode="replicate",
|
| 43 |
+
)
|
| 44 |
+
self.bn1 = nn.BatchNorm2d(hidden_chan)
|
| 45 |
+
self.dropout = nn.Dropout(p=dropout_rate)
|
| 46 |
+
self.act = nn.GELU()
|
| 47 |
+
self.conv2 = nn.Conv2d(
|
| 48 |
+
hidden_chan,
|
| 49 |
+
out_chan,
|
| 50 |
+
kernel_size,
|
| 51 |
+
stride,
|
| 52 |
+
padding,
|
| 53 |
+
bias=bias,
|
| 54 |
+
padding_mode="replicate",
|
| 55 |
+
)
|
| 56 |
+
self.bn2 = nn.BatchNorm2d(out_chan)
|
| 57 |
+
|
| 58 |
+
if use_1x1conv:
|
| 59 |
+
self.res_conv = nn.Conv2d(
|
| 60 |
+
in_chan,
|
| 61 |
+
out_chan,
|
| 62 |
+
kernel_size=1,
|
| 63 |
+
stride=stride,
|
| 64 |
+
padding=0,
|
| 65 |
+
bias=bias,
|
| 66 |
+
)
|
| 67 |
+
else:
|
| 68 |
+
self.res_conv = None
|
| 69 |
+
|
| 70 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 71 |
+
if self.res_conv is not None:
|
| 72 |
+
residual = self.res_conv(x)
|
| 73 |
+
else:
|
| 74 |
+
residual = x
|
| 75 |
+
x = self.conv1(x)
|
| 76 |
+
x = self.bn1(x)
|
| 77 |
+
x = self.dropout(x)
|
| 78 |
+
x = self.act(x)
|
| 79 |
+
x = self.conv2(x)
|
| 80 |
+
x = self.bn2(x)
|
| 81 |
+
x += residual
|
| 82 |
+
return x
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class ResNet(AutoCfdModel):
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
in_chan: int,
|
| 89 |
+
out_chan: int,
|
| 90 |
+
n_case_params: int,
|
| 91 |
+
loss_fn: nn.Module,
|
| 92 |
+
hidden_chan: int = 32,
|
| 93 |
+
num_blocks: int = 4,
|
| 94 |
+
kernel_size: int = 7,
|
| 95 |
+
padding: int = 3,
|
| 96 |
+
stride: int = 1,
|
| 97 |
+
):
|
| 98 |
+
super().__init__(loss_fn)
|
| 99 |
+
assert in_chan == out_chan
|
| 100 |
+
self.in_chan = in_chan
|
| 101 |
+
self.out_chan = out_chan
|
| 102 |
+
self.n_case_params = n_case_params
|
| 103 |
+
self.hidden_chan = hidden_chan
|
| 104 |
+
self.num_blocks = num_blocks
|
| 105 |
+
self.kernel_size = kernel_size
|
| 106 |
+
self.padding = padding
|
| 107 |
+
self.stride = stride
|
| 108 |
+
|
| 109 |
+
# Create sequence of residual blocks
|
| 110 |
+
blocks = [
|
| 111 |
+
ResidualBlock(
|
| 112 |
+
in_chan + 1 + n_case_params, # + 1 for mask
|
| 113 |
+
hidden_chan,
|
| 114 |
+
64,
|
| 115 |
+
kernel_size,
|
| 116 |
+
stride,
|
| 117 |
+
padding,
|
| 118 |
+
use_1x1conv=True,
|
| 119 |
+
),
|
| 120 |
+
]
|
| 121 |
+
for _ in range(num_blocks):
|
| 122 |
+
blocks.append(
|
| 123 |
+
ResidualBlock(
|
| 124 |
+
hidden_chan,
|
| 125 |
+
hidden_chan,
|
| 126 |
+
64,
|
| 127 |
+
kernel_size,
|
| 128 |
+
stride,
|
| 129 |
+
padding,
|
| 130 |
+
use_1x1conv=False,
|
| 131 |
+
)
|
| 132 |
+
)
|
| 133 |
+
blocks.append(
|
| 134 |
+
ResidualBlock(
|
| 135 |
+
hidden_chan,
|
| 136 |
+
out_chan,
|
| 137 |
+
64,
|
| 138 |
+
kernel_size,
|
| 139 |
+
stride,
|
| 140 |
+
padding,
|
| 141 |
+
use_1x1conv=True,
|
| 142 |
+
)
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
self.blocks = nn.Sequential(*blocks)
|
| 146 |
+
|
| 147 |
+
def forward(
|
| 148 |
+
self,
|
| 149 |
+
inputs: Tensor,
|
| 150 |
+
case_params: Tensor,
|
| 151 |
+
mask: Optional[Tensor] = None,
|
| 152 |
+
label: Optional[Tensor] = None,
|
| 153 |
+
) -> dict:
|
| 154 |
+
"""
|
| 155 |
+
Args:
|
| 156 |
+
- x: (B, in_chan, h, w)
|
| 157 |
+
- case_params: (B, 3)
|
| 158 |
+
- mask: (h, w) or (B, h, w). 1 for interior, 0 for boundaries.
|
| 159 |
+
- label: (B, out_chan, h, w)
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
(B, out_chan, h, w) or (B, out_chan, h, w), loss
|
| 163 |
+
"""
|
| 164 |
+
residual = inputs[:, : self.out_chan]
|
| 165 |
+
batch_size, n_chan, height, width = inputs.shape
|
| 166 |
+
if mask is None:
|
| 167 |
+
mask = torch.ones((batch_size, height, width)).to(inputs.device)
|
| 168 |
+
else:
|
| 169 |
+
if mask.dim() == 3:
|
| 170 |
+
mask = mask.unsqueeze(1) # (B, 1, h, w)
|
| 171 |
+
inputs = torch.cat([inputs, mask], dim=1) # (B, c + 1, h, w)
|
| 172 |
+
|
| 173 |
+
# Add case params as additional channels
|
| 174 |
+
case_params = case_params.unsqueeze(-1).unsqueeze(-1) # (B, c, 1, 1)
|
| 175 |
+
# (B, n_params, h, w)
|
| 176 |
+
case_params = case_params.expand(
|
| 177 |
+
-1, -1, inputs.shape[-2], inputs.shape[-1]
|
| 178 |
+
)
|
| 179 |
+
inputs = torch.cat(
|
| 180 |
+
[inputs, case_params], dim=1
|
| 181 |
+
) # (B, c + n_params, h, w)
|
| 182 |
+
|
| 183 |
+
inputs = self.blocks(inputs) # (B, c, h, w)
|
| 184 |
+
preds = inputs + residual
|
| 185 |
+
|
| 186 |
+
if mask is not None:
|
| 187 |
+
# Mask out predictions.
|
| 188 |
+
preds = preds * mask
|
| 189 |
+
|
| 190 |
+
if label is not None:
|
| 191 |
+
if mask is not None:
|
| 192 |
+
# Mask out labels
|
| 193 |
+
label = label * mask
|
| 194 |
+
loss = self.loss_fn(preds=preds, labels=label)
|
| 195 |
+
return dict(
|
| 196 |
+
preds=preds,
|
| 197 |
+
loss=loss,
|
| 198 |
+
)
|
| 199 |
+
else:
|
| 200 |
+
return dict(preds=preds)
|
| 201 |
+
|
| 202 |
+
def generate(
|
| 203 |
+
self,
|
| 204 |
+
inputs: Tensor,
|
| 205 |
+
case_params: Tensor,
|
| 206 |
+
mask: Optional[Tensor] = None,
|
| 207 |
+
):
|
| 208 |
+
outputs = self.forward(inputs, case_params=case_params, mask=mask)
|
| 209 |
+
preds = outputs["preds"]
|
| 210 |
+
return preds
|
| 211 |
+
|
| 212 |
+
def generate_many(
|
| 213 |
+
self,
|
| 214 |
+
inputs: Tensor,
|
| 215 |
+
case_params: Tensor,
|
| 216 |
+
steps: int,
|
| 217 |
+
mask: Tensor,
|
| 218 |
+
):
|
| 219 |
+
"""
|
| 220 |
+
x: (c, h, w)
|
| 221 |
+
mask: (h, w). 1 for interior, 0 for boundaries.
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
(steps, c, h, w)
|
| 225 |
+
"""
|
| 226 |
+
if inputs.dim() == 3:
|
| 227 |
+
inputs = inputs.unsqueeze(0) # (1, c, h, w)
|
| 228 |
+
case_params = case_params.unsqueeze(0) # (1, p)
|
| 229 |
+
mask = mask.unsqueeze(0) # (1, h, w)
|
| 230 |
+
cur_frame = inputs # (1, c, h, w)
|
| 231 |
+
frames = [cur_frame]
|
| 232 |
+
# boundaries = (1 - mask) * cur_frame # (1, c, h, w)
|
| 233 |
+
for _ in range(steps):
|
| 234 |
+
cur_frame = self.generate(
|
| 235 |
+
cur_frame, case_params=case_params, mask=mask
|
| 236 |
+
)
|
| 237 |
+
frames.append(cur_frame)
|
| 238 |
+
return frames
|
model/unet.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, List
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from torch import Tensor
|
| 5 |
+
|
| 6 |
+
from .base_model import AutoCfdModel
|
| 7 |
+
from onescience.modules.decoder.unet_decoder import UNetDecoder2D
|
| 8 |
+
from onescience.modules.encoder.unet_encoder import UNetEncoder2D
|
| 9 |
+
from onescience.modules.head.unet_head import UNetHead2D
|
| 10 |
+
|
| 11 |
+
class UNet(AutoCfdModel):
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
in_chan: int,
|
| 15 |
+
out_chan: int,
|
| 16 |
+
loss_fn: nn.Module,
|
| 17 |
+
n_case_params: int,
|
| 18 |
+
insert_case_params_at: str = "hidden",
|
| 19 |
+
bilinear: bool = False,
|
| 20 |
+
dim: int = 8,
|
| 21 |
+
):
|
| 22 |
+
assert insert_case_params_at in ["hidden", "input"]
|
| 23 |
+
super().__init__(loss_fn)
|
| 24 |
+
|
| 25 |
+
self.in_chan = in_chan
|
| 26 |
+
self.out_chan = out_chan
|
| 27 |
+
self.n_case_params = n_case_params
|
| 28 |
+
self.insert_case_params_at = insert_case_params_at
|
| 29 |
+
self.dim = dim
|
| 30 |
+
|
| 31 |
+
# 计算 Encoder 输入通道
|
| 32 |
+
encoder_in_chan = in_chan + 1 # + Mask
|
| 33 |
+
if insert_case_params_at == "input":
|
| 34 |
+
encoder_in_chan += n_case_params
|
| 35 |
+
|
| 36 |
+
# 1. Encoder
|
| 37 |
+
self.encoder = UNetEncoder2D(
|
| 38 |
+
in_channels=encoder_in_chan,
|
| 39 |
+
base_channels=dim,
|
| 40 |
+
num_stages=4,
|
| 41 |
+
bilinear=bilinear,
|
| 42 |
+
normtype="bn"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# 2. Hidden Injection
|
| 46 |
+
self.case_params_fc = None
|
| 47 |
+
if insert_case_params_at == "hidden":
|
| 48 |
+
bottleneck_dim = dim * 16
|
| 49 |
+
self.case_params_fc = nn.Linear(n_case_params, bottleneck_dim)
|
| 50 |
+
|
| 51 |
+
# 3. Decoder
|
| 52 |
+
self.decoder = UNetDecoder2D(
|
| 53 |
+
base_channels=dim,
|
| 54 |
+
num_stages=4,
|
| 55 |
+
bilinear=bilinear,
|
| 56 |
+
normtype="bn"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# 4. Head
|
| 60 |
+
self.head = UNetHead2D(
|
| 61 |
+
in_channels=dim,
|
| 62 |
+
out_channels=out_chan
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def forward(
|
| 66 |
+
self,
|
| 67 |
+
inputs: Tensor,
|
| 68 |
+
case_params: Tensor,
|
| 69 |
+
mask: Optional[Tensor] = None,
|
| 70 |
+
label: Optional[Tensor] = None,
|
| 71 |
+
):
|
| 72 |
+
batch_size, n_chan, height, width = inputs.shape
|
| 73 |
+
residual = inputs[:, : self.out_chan]
|
| 74 |
+
|
| 75 |
+
# 构造 Mask
|
| 76 |
+
if mask is None:
|
| 77 |
+
mask = torch.ones((batch_size, 1, height, width)).to(inputs.device)
|
| 78 |
+
else:
|
| 79 |
+
if mask.dim() == 3:
|
| 80 |
+
mask = mask.unsqueeze(1) # (B, H, W) -> (B, 1, H, W)
|
| 81 |
+
|
| 82 |
+
# 拼接 Mask
|
| 83 |
+
x_in = torch.cat([inputs, mask], dim=1)
|
| 84 |
+
|
| 85 |
+
# 拼接 Case Params
|
| 86 |
+
if self.insert_case_params_at == "input":
|
| 87 |
+
cp_spatial = case_params.view(batch_size, self.n_case_params, 1, 1)
|
| 88 |
+
cp_spatial = cp_spatial.expand(-1, -1, height, width)
|
| 89 |
+
x_in = torch.cat([x_in, cp_spatial], dim=1)
|
| 90 |
+
|
| 91 |
+
# Encoder
|
| 92 |
+
features = self.encoder(x_in)
|
| 93 |
+
|
| 94 |
+
# Hidden 注入
|
| 95 |
+
if self.insert_case_params_at == "hidden":
|
| 96 |
+
bottleneck = features[-1]
|
| 97 |
+
conds = self.case_params_fc(case_params)
|
| 98 |
+
conds = conds.view(batch_size, -1, 1, 1)
|
| 99 |
+
features[-1] = bottleneck + conds
|
| 100 |
+
|
| 101 |
+
# Decoder
|
| 102 |
+
decoded = self.decoder(features)
|
| 103 |
+
|
| 104 |
+
# Head
|
| 105 |
+
preds = self.head(decoded)
|
| 106 |
+
|
| 107 |
+
# Residual & Mask
|
| 108 |
+
preds = preds + residual
|
| 109 |
+
preds = preds * mask
|
| 110 |
+
|
| 111 |
+
if label is not None:
|
| 112 |
+
label = label * mask
|
| 113 |
+
loss = self.loss_fn(labels=label, preds=preds)
|
| 114 |
+
return {"preds": preds, "loss": loss}
|
| 115 |
+
|
| 116 |
+
return {"preds": preds}
|
| 117 |
+
|
| 118 |
+
def generate_many(
|
| 119 |
+
self, inputs: Tensor, case_params: Tensor, mask: Tensor, steps: int
|
| 120 |
+
) -> List[Tensor]:
|
| 121 |
+
preds = []
|
| 122 |
+
|
| 123 |
+
# 处理单样本输入 (增加 Batch 维)
|
| 124 |
+
if inputs.dim() == 3:
|
| 125 |
+
inputs = inputs.unsqueeze(0)
|
| 126 |
+
case_params = case_params.unsqueeze(0)
|
| 127 |
+
if mask.dim() == 2:
|
| 128 |
+
mask = mask.unsqueeze(0)
|
| 129 |
+
|
| 130 |
+
# 确保 Mask 是 (B, 1, H, W) 以匹配 forward 逻辑
|
| 131 |
+
if mask.dim() == 3:
|
| 132 |
+
mask = mask.unsqueeze(1)
|
| 133 |
+
|
| 134 |
+
cur_frame = inputs
|
| 135 |
+
for _ in range(steps):
|
| 136 |
+
out_dict = self.forward(cur_frame, case_params=case_params, mask=mask)
|
| 137 |
+
cur_frame = out_dict["preds"]
|
| 138 |
+
preds.append(cur_frame)
|
| 139 |
+
|
| 140 |
+
return preds
|
| 141 |
+
|
| 142 |
+
def generate(
|
| 143 |
+
self,
|
| 144 |
+
inputs: Tensor,
|
| 145 |
+
case_params: Tensor,
|
| 146 |
+
mask: Optional[Tensor] = None,
|
| 147 |
+
) -> Tensor:
|
| 148 |
+
outputs = self.forward(inputs, case_params=case_params, mask=mask)
|
| 149 |
+
return outputs["preds"]
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import shutil
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 11 |
+
|
| 12 |
+
from onescience.utils.YParams import YParams
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def resolve_path(path_value):
|
| 16 |
+
path = Path(path_value)
|
| 17 |
+
return path if path.is_absolute() else PROJECT_ROOT / path
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def make_case(case_dir, case_id, rows, cols, frames, rng):
|
| 21 |
+
case_dir.mkdir(parents=True, exist_ok=True)
|
| 22 |
+
vel_in = 1.0 + 0.05 * case_id
|
| 23 |
+
params = {
|
| 24 |
+
"vel_in": vel_in,
|
| 25 |
+
"density": 1.0 + 0.1 * (case_id % 3),
|
| 26 |
+
"viscosity": 0.001 + 0.0001 * (case_id % 5),
|
| 27 |
+
"height": 1.0,
|
| 28 |
+
"width": 1.0,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
y = np.linspace(0.0, 1.0, rows, dtype=np.float32)
|
| 32 |
+
x = np.linspace(0.0, 1.0, cols, dtype=np.float32)
|
| 33 |
+
yy, xx = np.meshgrid(y, x, indexing="ij")
|
| 34 |
+
u = np.empty((frames, rows, cols), dtype=np.float32)
|
| 35 |
+
v = np.empty_like(u)
|
| 36 |
+
|
| 37 |
+
phase = 0.2 * case_id
|
| 38 |
+
for t in range(frames):
|
| 39 |
+
amp = vel_in * (1.0 + 0.03 * t)
|
| 40 |
+
u[t] = amp * np.sin(np.pi * yy) * (1.0 - 0.3 * xx) + 0.01 * rng.standard_normal((rows, cols))
|
| 41 |
+
v[t] = 0.2 * amp * np.cos(np.pi * xx + phase) * yy + 0.01 * rng.standard_normal((rows, cols))
|
| 42 |
+
|
| 43 |
+
(case_dir / "case.json").write_text(json.dumps(params, indent=2), encoding="utf-8")
|
| 44 |
+
np.save(case_dir / "u.npy", u)
|
| 45 |
+
np.save(case_dir / "v.npy", v)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main():
|
| 49 |
+
cfg = YParams(str(PROJECT_ROOT / "conf" / "config.yaml"), "root")
|
| 50 |
+
data_dir = resolve_path(cfg.datapipe.source.data_dir)
|
| 51 |
+
fake_cfg = cfg.fake_data
|
| 52 |
+
data_cfg = cfg.datapipe.data
|
| 53 |
+
|
| 54 |
+
if data_dir.exists():
|
| 55 |
+
shutil.rmtree(data_dir)
|
| 56 |
+
|
| 57 |
+
rng = np.random.default_rng(fake_cfg.seed)
|
| 58 |
+
problem_dir = data_dir / "tube"
|
| 59 |
+
case_id = 0
|
| 60 |
+
for subset in ("prop", "bc", "geo"):
|
| 61 |
+
for subset_idx in range(fake_cfg.num_cases_per_subset):
|
| 62 |
+
make_case(
|
| 63 |
+
problem_dir / subset / f"case{case_id}",
|
| 64 |
+
case_id=case_id + subset_idx,
|
| 65 |
+
rows=data_cfg.num_rows,
|
| 66 |
+
cols=data_cfg.num_cols,
|
| 67 |
+
frames=fake_cfg.num_frames,
|
| 68 |
+
rng=rng,
|
| 69 |
+
)
|
| 70 |
+
case_id += 1
|
| 71 |
+
|
| 72 |
+
total_cases = fake_cfg.num_cases_per_subset * 3
|
| 73 |
+
print(f"Fake CFDBench data written to {data_dir}")
|
| 74 |
+
print(f"cases={total_cases}, frames={fake_cfg.num_frames}, grid=({data_cfg.num_rows}, {data_cfg.num_cols})")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import importlib.util
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from tqdm import tqdm
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 14 |
+
|
| 15 |
+
from model import build_model, infer_task_type
|
| 16 |
+
import onescience
|
| 17 |
+
from onescience.distributed.manager import DistributedManager
|
| 18 |
+
from onescience.utils.YParams import YParams
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def resolve_path(path_value):
|
| 22 |
+
path = Path(path_value)
|
| 23 |
+
return path if path.is_absolute() else PROJECT_ROOT / path
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def parse_args():
|
| 27 |
+
parser = argparse.ArgumentParser(description="Run CFDBench inference.")
|
| 28 |
+
parser.add_argument("--model", default=None, help="Override root.model.name and read weight/<model>.pt by default.")
|
| 29 |
+
return parser.parse_args()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_config(model_name=None):
|
| 33 |
+
cfg = YParams(str(PROJECT_ROOT / "conf" / "config.yaml"), "root")
|
| 34 |
+
if model_name:
|
| 35 |
+
cfg.model.name = model_name
|
| 36 |
+
elif os.environ.get("CFDBENCH_MODEL_NAME"):
|
| 37 |
+
cfg.model.name = os.environ["CFDBENCH_MODEL_NAME"]
|
| 38 |
+
cfg.datapipe.source.data_dir = str(resolve_path(cfg.datapipe.source.data_dir))
|
| 39 |
+
cfg.inference.output_dir = str(resolve_path(cfg.inference.output_dir))
|
| 40 |
+
return cfg
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def checkpoint_path(cfg):
|
| 44 |
+
path_value = cfg.inference.get("checkpoint_path", "auto")
|
| 45 |
+
if path_value == "auto":
|
| 46 |
+
return PROJECT_ROOT / "weight" / f"{cfg.model.name}.pt"
|
| 47 |
+
return resolve_path(path_value)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def output_path(cfg, task_type):
|
| 51 |
+
output_dir = Path(cfg.inference.output_dir)
|
| 52 |
+
if cfg.inference.get("group_by_model", False):
|
| 53 |
+
output_dir = output_dir / task_type / cfg.model.name
|
| 54 |
+
return output_dir
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def select_device(requested, dist):
|
| 58 |
+
if requested == "auto":
|
| 59 |
+
return dist.device
|
| 60 |
+
if requested.startswith("cuda") and not torch.cuda.is_available():
|
| 61 |
+
raise RuntimeError(f"Requested device {requested!r}, but CUDA is not available.")
|
| 62 |
+
return torch.device(requested)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def load_cfdbench_datapipe_class():
|
| 66 |
+
runtime_root = Path(onescience.__file__).resolve().parent
|
| 67 |
+
datapipe_file = runtime_root / "datapipes" / "cfd" / "cfdbench.py"
|
| 68 |
+
spec = importlib.util.spec_from_file_location("_onescience_cfdbench_datapipe", datapipe_file)
|
| 69 |
+
if spec is None or spec.loader is None:
|
| 70 |
+
raise ImportError(f"Cannot load CFDBench datapipe from {datapipe_file}")
|
| 71 |
+
module = importlib.util.module_from_spec(spec)
|
| 72 |
+
sys.modules[spec.name] = module
|
| 73 |
+
spec.loader.exec_module(module)
|
| 74 |
+
return module.CFDBenchDatapipe
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def predict_full_frame(model, batch, task_type):
|
| 78 |
+
if task_type == "auto":
|
| 79 |
+
return model.generate(
|
| 80 |
+
inputs=batch["inputs"],
|
| 81 |
+
case_params=batch["case_params"],
|
| 82 |
+
mask=batch.get("mask"),
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
labels = batch["label"]
|
| 86 |
+
return model.generate_one(
|
| 87 |
+
case_params=batch["case_params"],
|
| 88 |
+
t=batch["t"],
|
| 89 |
+
height=labels.shape[-2],
|
| 90 |
+
width=labels.shape[-1],
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def main():
|
| 95 |
+
args = parse_args()
|
| 96 |
+
DistributedManager.initialize()
|
| 97 |
+
dist = DistributedManager()
|
| 98 |
+
cfg = load_config(args.model)
|
| 99 |
+
task_type = infer_task_type(cfg.model.name)
|
| 100 |
+
cfg.datapipe.data.task_type = task_type
|
| 101 |
+
device = select_device(cfg.inference.get("device", "auto"), dist)
|
| 102 |
+
|
| 103 |
+
ckpt_path = checkpoint_path(cfg)
|
| 104 |
+
if not ckpt_path.is_file():
|
| 105 |
+
raise FileNotFoundError(f"Missing checkpoint: {ckpt_path}. Run scripts/train.py first or update root.inference.checkpoint_path.")
|
| 106 |
+
|
| 107 |
+
torch.set_num_threads(1)
|
| 108 |
+
CFDBenchDatapipe = load_cfdbench_datapipe_class()
|
| 109 |
+
datapipe = CFDBenchDatapipe(cfg.datapipe, distributed=False)
|
| 110 |
+
model = build_model(cfg).to(device)
|
| 111 |
+
model.load_state_dict(torch.load(ckpt_path, map_location=device, weights_only=True))
|
| 112 |
+
model.eval()
|
| 113 |
+
|
| 114 |
+
output_dir = output_path(cfg, task_type)
|
| 115 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
|
| 117 |
+
preds = []
|
| 118 |
+
score_lists = {name: [] for name in model.loss_fn.get_score_names()}
|
| 119 |
+
with torch.no_grad():
|
| 120 |
+
for step, batch in enumerate(tqdm(datapipe.test_dataloader(), desc="Inferring")):
|
| 121 |
+
if step >= cfg.inference.max_batches:
|
| 122 |
+
break
|
| 123 |
+
batch = {key: value.to(device) for key, value in batch.items()}
|
| 124 |
+
pred = predict_full_frame(model, batch, task_type)
|
| 125 |
+
label = batch["label"][:, : pred.shape[1]]
|
| 126 |
+
loss = model.loss_fn(preds=pred, labels=label)
|
| 127 |
+
preds.append(pred.detach().cpu())
|
| 128 |
+
for key, value in loss.items():
|
| 129 |
+
score_lists[key].append(float(value.detach().cpu()))
|
| 130 |
+
|
| 131 |
+
if not preds:
|
| 132 |
+
raise RuntimeError("Inference loader is empty. Check dataset split and data directory.")
|
| 133 |
+
|
| 134 |
+
torch.save(torch.cat(preds, dim=0), output_dir / "preds.pt")
|
| 135 |
+
means = {key: sum(values) / len(values) for key, values in score_lists.items() if values}
|
| 136 |
+
with open(output_dir / "scores.json", "w", encoding="utf-8") as f:
|
| 137 |
+
json.dump({"mean": means, "all": score_lists}, f, indent=2)
|
| 138 |
+
print(f"Saved predictions to {output_dir / 'preds.pt'}")
|
| 139 |
+
print(f"Scores: {means}")
|
| 140 |
+
|
| 141 |
+
DistributedManager.cleanup()
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 12 |
+
|
| 13 |
+
from onescience.utils.YParams import YParams
|
| 14 |
+
from model import infer_task_type
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def resolve_path(path_value):
|
| 18 |
+
path = Path(path_value)
|
| 19 |
+
return path if path.is_absolute() else PROJECT_ROOT / path
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def output_path(cfg):
|
| 23 |
+
task_type = infer_task_type(cfg.model.name)
|
| 24 |
+
output_dir = resolve_path(cfg.inference.output_dir)
|
| 25 |
+
if cfg.inference.get("group_by_model", False):
|
| 26 |
+
output_dir = output_dir / task_type / cfg.model.name
|
| 27 |
+
return output_dir
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_args():
|
| 31 |
+
parser = argparse.ArgumentParser(description="Summarize CFDBench inference outputs.")
|
| 32 |
+
parser.add_argument("--model", default=None, help="Override root.model.name and read that model's inference outputs.")
|
| 33 |
+
return parser.parse_args()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def main():
|
| 37 |
+
args = parse_args()
|
| 38 |
+
cfg = YParams(str(PROJECT_ROOT / "conf" / "config.yaml"), "root")
|
| 39 |
+
if args.model:
|
| 40 |
+
cfg.model.name = args.model
|
| 41 |
+
elif os.environ.get("CFDBENCH_MODEL_NAME"):
|
| 42 |
+
cfg.model.name = os.environ["CFDBENCH_MODEL_NAME"]
|
| 43 |
+
output_dir = output_path(cfg)
|
| 44 |
+
pred_path = output_dir / "preds.pt"
|
| 45 |
+
score_path = output_dir / "scores.json"
|
| 46 |
+
|
| 47 |
+
if not pred_path.is_file() or not score_path.is_file():
|
| 48 |
+
raise FileNotFoundError("Missing inference outputs. Run scripts/inference.py first.")
|
| 49 |
+
|
| 50 |
+
preds = torch.load(pred_path, map_location="cpu", weights_only=True)
|
| 51 |
+
scores = json.loads(score_path.read_text(encoding="utf-8"))
|
| 52 |
+
print(f"Prediction tensor: shape={tuple(preds.shape)}, dtype={preds.dtype}")
|
| 53 |
+
print(f"Prediction range: min={float(preds.min()):.4e}, max={float(preds.max()):.4e}")
|
| 54 |
+
print(f"Scores: {scores['mean']}")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import importlib.util
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 12 |
+
from torch.optim import Adam, lr_scheduler
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 18 |
+
|
| 19 |
+
from model import build_model, infer_task_type
|
| 20 |
+
import onescience
|
| 21 |
+
from onescience.distributed.manager import DistributedManager
|
| 22 |
+
from onescience.utils.YParams import YParams
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def resolve_path(path_value):
|
| 26 |
+
path = Path(path_value)
|
| 27 |
+
return path if path.is_absolute() else PROJECT_ROOT / path
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_args():
|
| 31 |
+
parser = argparse.ArgumentParser(description="Train CFDBench static or autoregressive models.")
|
| 32 |
+
parser.add_argument("--model", default=None, help="Override root.model.name, e.g. ffn, deeponet, fno, auto_ffn.")
|
| 33 |
+
return parser.parse_args()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_config(model_name=None):
|
| 37 |
+
cfg = YParams(str(PROJECT_ROOT / "conf" / "config.yaml"), "root")
|
| 38 |
+
if model_name:
|
| 39 |
+
cfg.model.name = model_name
|
| 40 |
+
elif os.environ.get("CFDBENCH_MODEL_NAME"):
|
| 41 |
+
cfg.model.name = os.environ["CFDBENCH_MODEL_NAME"]
|
| 42 |
+
cfg.datapipe.source.data_dir = str(resolve_path(cfg.datapipe.source.data_dir))
|
| 43 |
+
cfg.training.output_dir = str(resolve_path(cfg.training.output_dir))
|
| 44 |
+
return cfg
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def checkpoint_path(cfg):
|
| 48 |
+
name = cfg.training.get("checkpoint_name", "auto")
|
| 49 |
+
if name == "auto":
|
| 50 |
+
name = f"{cfg.model.name}.pt"
|
| 51 |
+
return PROJECT_ROOT / "weight" / name
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def output_path(cfg, task_type):
|
| 55 |
+
output_dir = Path(cfg.training.output_dir)
|
| 56 |
+
if cfg.training.get("group_by_model", False):
|
| 57 |
+
output_dir = output_dir / task_type / cfg.model.name
|
| 58 |
+
return output_dir
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def select_device(requested, dist):
|
| 62 |
+
if requested == "auto":
|
| 63 |
+
return dist.device
|
| 64 |
+
if requested.startswith("cuda") and not torch.cuda.is_available():
|
| 65 |
+
raise RuntimeError(f"Requested device {requested!r}, but CUDA is not available.")
|
| 66 |
+
return torch.device(requested)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def dump_json(data, path):
|
| 70 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 72 |
+
json.dump(data, f, indent=2)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def load_cfdbench_datapipe_class():
|
| 76 |
+
runtime_root = Path(onescience.__file__).resolve().parent
|
| 77 |
+
datapipe_file = runtime_root / "datapipes" / "cfd" / "cfdbench.py"
|
| 78 |
+
spec = importlib.util.spec_from_file_location("_onescience_cfdbench_datapipe", datapipe_file)
|
| 79 |
+
if spec is None or spec.loader is None:
|
| 80 |
+
raise ImportError(f"Cannot load CFDBench datapipe from {datapipe_file}")
|
| 81 |
+
module = importlib.util.module_from_spec(spec)
|
| 82 |
+
sys.modules[spec.name] = module
|
| 83 |
+
spec.loader.exec_module(module)
|
| 84 |
+
return module.CFDBenchDatapipe
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def mean_scores(score_lists):
|
| 88 |
+
return {key: float(np.mean(values)) for key, values in score_lists.items() if values}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def evaluate(model, loader, device, dist, desc="Evaluating"):
|
| 92 |
+
model_eval = model.module if hasattr(model, "module") else model
|
| 93 |
+
model_eval.eval()
|
| 94 |
+
score_lists = {name: [] for name in model_eval.loss_fn.get_score_names()}
|
| 95 |
+
|
| 96 |
+
with torch.no_grad():
|
| 97 |
+
iterator = tqdm(loader, desc=desc, disable=(dist.rank != 0))
|
| 98 |
+
for batch in iterator:
|
| 99 |
+
batch = {key: value.to(device) for key, value in batch.items()}
|
| 100 |
+
outputs = model_eval(**batch)
|
| 101 |
+
for key, value in outputs["loss"].items():
|
| 102 |
+
score_lists[key].append(float(value.detach().cpu()))
|
| 103 |
+
|
| 104 |
+
return {"mean": mean_scores(score_lists), "all": score_lists}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def train(cfg, model, datapipe, output_dir, device, dist):
|
| 108 |
+
train_cfg = cfg.training
|
| 109 |
+
train_loader, train_sampler = datapipe.train_dataloader()
|
| 110 |
+
val_loader, _ = datapipe.val_dataloader()
|
| 111 |
+
|
| 112 |
+
if len(train_loader) == 0:
|
| 113 |
+
raise RuntimeError("Training loader is empty. Increase fake_data.num_cases_per_subset or reduce batch_size.")
|
| 114 |
+
|
| 115 |
+
optimizer = Adam(model.parameters(), lr=train_cfg.lr)
|
| 116 |
+
scheduler = lr_scheduler.StepLR(optimizer, step_size=train_cfg.lr_step_size, gamma=train_cfg.lr_gamma)
|
| 117 |
+
train_losses = []
|
| 118 |
+
best_nmse = float("inf")
|
| 119 |
+
|
| 120 |
+
for epoch in range(train_cfg.num_epochs):
|
| 121 |
+
if train_sampler:
|
| 122 |
+
train_sampler.set_epoch(epoch)
|
| 123 |
+
model.train()
|
| 124 |
+
start = time.time()
|
| 125 |
+
|
| 126 |
+
iterator = tqdm(train_loader, desc=f"Epoch {epoch}", disable=(dist.rank != 0))
|
| 127 |
+
for step, batch in enumerate(iterator):
|
| 128 |
+
batch = {key: value.to(device) for key, value in batch.items()}
|
| 129 |
+
outputs = model(**batch)
|
| 130 |
+
loss = outputs["loss"][train_cfg.loss_name]
|
| 131 |
+
|
| 132 |
+
optimizer.zero_grad(set_to_none=True)
|
| 133 |
+
loss.backward()
|
| 134 |
+
optimizer.step()
|
| 135 |
+
|
| 136 |
+
loss_value = float(loss.detach().cpu())
|
| 137 |
+
train_losses.append(loss_value)
|
| 138 |
+
if dist.rank == 0 and (step + 1) % train_cfg.log_interval == 0:
|
| 139 |
+
iterator.set_postfix({"loss": f"{loss_value:.4e}"})
|
| 140 |
+
|
| 141 |
+
scheduler.step()
|
| 142 |
+
|
| 143 |
+
if dist.rank == 0 and (epoch + 1) % train_cfg.eval_interval == 0:
|
| 144 |
+
ckpt_dir = output_dir / f"ckpt-{epoch}"
|
| 145 |
+
ckpt_dir.mkdir(parents=True, exist_ok=True)
|
| 146 |
+
scores = evaluate(model, val_loader, device, dist, desc=f"Val {epoch}")
|
| 147 |
+
dump_json(scores, ckpt_dir / "dev_scores.json")
|
| 148 |
+
dump_json({"epoch": epoch, "train_loss": train_losses, "seconds": time.time() - start}, ckpt_dir / "scores.json")
|
| 149 |
+
|
| 150 |
+
nmse = scores["mean"].get(train_cfg.loss_name, float("inf"))
|
| 151 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 152 |
+
torch.save(model_to_save.state_dict(), ckpt_dir / "model.pt")
|
| 153 |
+
if nmse <= best_nmse:
|
| 154 |
+
best_nmse = nmse
|
| 155 |
+
weight_path = checkpoint_path(cfg)
|
| 156 |
+
torch.save(model_to_save.state_dict(), weight_path)
|
| 157 |
+
print(f"Saved best checkpoint to {weight_path}")
|
| 158 |
+
|
| 159 |
+
if dist.world_size > 1:
|
| 160 |
+
torch.distributed.barrier()
|
| 161 |
+
|
| 162 |
+
if dist.rank == 0:
|
| 163 |
+
dump_json(train_losses, output_dir / "train_losses.json")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def main(required_task_type="static", entry_name="scripts/train.py"):
|
| 167 |
+
args = parse_args()
|
| 168 |
+
DistributedManager.initialize()
|
| 169 |
+
dist = DistributedManager()
|
| 170 |
+
cfg = load_config(args.model)
|
| 171 |
+
task_type = infer_task_type(cfg.model.name)
|
| 172 |
+
if required_task_type is not None and task_type != required_task_type:
|
| 173 |
+
expected = "ffn/deeponet" if required_task_type == "static" else "auto_* / resnet / unet / fno"
|
| 174 |
+
raise ValueError(
|
| 175 |
+
f"{entry_name} is the {required_task_type} entry, but model.name={cfg.model.name!r} is {task_type}. "
|
| 176 |
+
f"Use a {expected} model, or run scripts/train_auto.py for autoregressive models."
|
| 177 |
+
)
|
| 178 |
+
cfg.datapipe.data.task_type = task_type
|
| 179 |
+
device = select_device(cfg.training.get("device", "auto"), dist)
|
| 180 |
+
|
| 181 |
+
output_dir = output_path(cfg, task_type)
|
| 182 |
+
if dist.rank == 0:
|
| 183 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 184 |
+
print(f"Config: {PROJECT_ROOT / 'conf' / 'config.yaml'}")
|
| 185 |
+
print(f"Model: {cfg.model.name} ({task_type})")
|
| 186 |
+
print(f"Data: {cfg.datapipe.source.data_dir}")
|
| 187 |
+
print(f"Output: {output_dir}")
|
| 188 |
+
print(f"Checkpoint: {checkpoint_path(cfg)}")
|
| 189 |
+
|
| 190 |
+
torch.set_num_threads(1)
|
| 191 |
+
CFDBenchDatapipe = load_cfdbench_datapipe_class()
|
| 192 |
+
datapipe = CFDBenchDatapipe(cfg.datapipe, distributed=(dist.world_size > 1))
|
| 193 |
+
model = build_model(cfg).to(device)
|
| 194 |
+
|
| 195 |
+
if dist.world_size > 1 and "train" in cfg.training.mode:
|
| 196 |
+
device_ids = [dist.local_rank] if device.type == "cuda" else None
|
| 197 |
+
model = DDP(model, device_ids=device_ids)
|
| 198 |
+
|
| 199 |
+
if "train" in cfg.training.mode:
|
| 200 |
+
train(cfg, model, datapipe, output_dir, device, dist)
|
| 201 |
+
|
| 202 |
+
if "test" in cfg.training.mode and dist.rank == 0:
|
| 203 |
+
scores = evaluate(model, datapipe.test_dataloader(), device, dist, desc="Test")
|
| 204 |
+
dump_json(scores, output_dir / "test_scores.json")
|
| 205 |
+
print(f"Test scores: {scores['mean']}")
|
| 206 |
+
|
| 207 |
+
DistributedManager.cleanup()
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
main()
|
scripts/train_auto.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
| 6 |
+
if str(SCRIPTS_DIR) not in sys.path:
|
| 7 |
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
| 8 |
+
|
| 9 |
+
from train import main
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
if __name__ == "__main__":
|
| 13 |
+
main(required_task_type="auto", entry_name="scripts/train_auto.py")
|
weight/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|