OneScience commited on
Commit
338c3e4
·
verified ·
1 Parent(s): 5d35490

Upload folder using huggingface_hub

Browse files
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
+