OneScience commited on
Commit
a62fa9c
·
verified ·
1 Parent(s): 7d0d55d

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,49 @@
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
 
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
  *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
  *.onnx filter=lfs diff=lfs merge=lfs -text
17
  *.ot filter=lfs diff=lfs merge=lfs -text
18
  *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
  *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
  *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
  *.tflite filter=lfs diff=lfs merge=lfs -text
30
  *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
  *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
  *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bin.* filter=lfs diff=lfs merge=lfs -text
5
  *.bz2 filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
11
  *.model filter=lfs diff=lfs merge=lfs -text
12
  *.msgpack filter=lfs diff=lfs merge=lfs -text
 
 
13
  *.onnx filter=lfs diff=lfs merge=lfs -text
14
  *.ot filter=lfs diff=lfs merge=lfs -text
15
  *.parquet filter=lfs diff=lfs merge=lfs -text
16
  *.pb filter=lfs diff=lfs merge=lfs -text
17
+
 
 
18
  *.pth filter=lfs diff=lfs merge=lfs -text
19
  *.rar filter=lfs diff=lfs merge=lfs -text
 
20
  saved_model/**/* filter=lfs diff=lfs merge=lfs -text
21
  *.tar.* filter=lfs diff=lfs merge=lfs -text
 
22
  *.tflite filter=lfs diff=lfs merge=lfs -text
23
  *.tgz filter=lfs diff=lfs merge=lfs -text
 
24
  *.xz filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
+ *.zstandard filter=lfs diff=lfs merge=lfs -text
27
+ *.tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ *.db* filter=lfs diff=lfs merge=lfs -text
29
+ *.ark* filter=lfs diff=lfs merge=lfs -text
30
+ **/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
31
+ **/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
32
+ **/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
33
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
34
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
35
+ *.gguf* filter=lfs diff=lfs merge=lfs -text
36
+ *.ggml filter=lfs diff=lfs merge=lfs -text
37
+ *.llamafile* filter=lfs diff=lfs merge=lfs -text
38
+ *.pt2 filter=lfs diff=lfs merge=lfs -text
39
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
40
+ *.npy filter=lfs diff=lfs merge=lfs -text
41
+ *.npz filter=lfs diff=lfs merge=lfs -text
42
+ *.pickle filter=lfs diff=lfs merge=lfs -text
43
+ *.pkl filter=lfs diff=lfs merge=lfs -text
44
+ *.tar filter=lfs diff=lfs merge=lfs -text
45
+ *.wasm filter=lfs diff=lfs merge=lfs -text
46
  *.zst filter=lfs diff=lfs merge=lfs -text
47
  *tfevents* filter=lfs diff=lfs merge=lfs -text
48
+
49
+ *.pt filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - ""
4
+ language:
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - UNO
10
+ - neural-operator
11
+ - computational-fluid-dynamics
12
+ - Navier-Stokes
13
+ tasks: []
14
+ ---
15
+ <p align="center">
16
+ <strong>
17
+ <span style="font-size: 30px;">UNO</span>
18
+ </strong>
19
+ </p>
20
+
21
+ # Model Overview
22
+
23
+ UNO (U-shaped Neural Operator) combines the multiscale encoder–decoder structure of U-Net with the spectral convolutions of a Fourier Neural Operator. It learns PDE solution operators across multiple spatial scales while preserving local flow-field details through skip connections.
24
+
25
+ This model package targets Navier–Stokes time-series prediction on two-dimensional regular grids. By default, it takes the first 10 time steps as input and predicts the subsequent 10 autoregressively.
26
+
27
+ Paper: U-NO: U-shaped Neural Operators
28
+ https://arxiv.org/abs/2204.11127
29
+
30
+ # Repository Overview
31
+
32
+ This repository is a minimal, self-contained, runnable UNO model package maintained by OneScience for ModelScope downloads, automated OneCode execution, and rapid local validation.
33
+
34
+ Supported capabilities:
35
+
36
+ - Train a two-dimensional UNO model from a YAML configuration
37
+ - Perform multistep autoregressive prediction of Navier–Stokes flow fields
38
+ - Compute relative L2 errors and save predicted tensors and visualizations
39
+ - Override the sample count, temporal window, spatial downsampling, and model size from the command line
40
+ - Run on a CPU, GPU, or DCU
41
+
42
+ Unsupported capabilities:
43
+
44
+ - Pretrained weights are not bundled
45
+ - The approximately 394 MiB raw Navier–Stokes data file is not bundled
46
+ - The standalone model includes only the two-dimensional UNO implementation required by this example; the general-purpose OneScience 1D and 3D components are not included
47
+
48
+ # Use Cases
49
+
50
+ | Use Case | Description |
51
+ | :---: | :--- |
52
+ | Flow-field time-series prediction | Autoregressively predict future Navier–Stokes states from historical vorticity fields |
53
+ | Neural operator training | Evaluate the combination of Fourier spectral convolutions and a U-shaped multiscale architecture |
54
+ | CFD surrogate modeling | Learn mappings from historical to future fields on regular grids |
55
+ | Pipeline validation | Validate training and inference with a small sample set and a single epoch |
56
+
57
+ # File Structure
58
+
59
+ | Path | Purpose | Notes |
60
+ | :--- | :--- | :--- |
61
+ | `README.md` | Project documentation | English |
62
+ | `conf/config.yaml` | Data, model, training, and output configuration | Paths are resolved relative to the model package root |
63
+ | `model/uno.py` | Standalone two-dimensional UNO model | Does not depend on `onescience.modules` |
64
+ | `scripts/common.py` | Shared configuration, device, metric, and autoregressive utilities | Used by both training and inference |
65
+ | `scripts/train.py` | Training and validation script | Saves the checkpoint with the best relative L2 error |
66
+ | `scripts/inference.py` | Inference, evaluation, and visualization script | Loads `weight/*.pt` |
67
+ | `data/` | Navier–Stokes data directory | Stores the benchmark `.mat` file |
68
+ | `weight/` | Model weight directory | Checkpoints are written automatically during training |
69
+ | `result/` | Inference result directory | Created automatically on first inference |
70
+
71
+ # Usage
72
+
73
+ ## 1. OneCode
74
+
75
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
76
+
77
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
78
+
79
+ ## 2. Manual Setup
80
+
81
+ **Hardware Requirements**
82
+
83
+ - A GPU or DCU is recommended for full training.
84
+ - A CPU can be used for pipeline validation with a reduced model and dataset, but full training will be slow.
85
+ - DCU users must install DTK and a PyTorch environment compatible with the target cluster.
86
+
87
+ ### Download the Model Package
88
+
89
+ ```bash
90
+ modelscope download --model OneScience/UNO --local_dir ./UNO
91
+ cd UNO
92
+ ```
93
+
94
+ ### Set Up the Runtime Environment
95
+
96
+ **DCU Environment**
97
+
98
+ ```bash
99
+ # Activate DTK and Conda first
100
+ conda create -n onescience311 python=3.11 -y
101
+ conda activate onescience311
102
+ # Installation with uv is also supported
103
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
104
+ ```
105
+
106
+ **GPU Environment**
107
+ ```bash
108
+ # Activate Conda first
109
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
110
+ conda activate onescience311
111
+ # Installation with uv is also supported
112
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
113
+ ```
114
+
115
+ ## 3. Quick Start
116
+
117
+ ### Prepare the Data
118
+
119
+ Download the following file from the Transolver `PDE-Solving-StandardBenchmark`:
120
+
121
+ ```text
122
+ NavierStokes_V1e-5_N1200_T20.mat
123
+ ```
124
+
125
+ Place the file in `data/`:
126
+
127
+ ```text
128
+ data/
129
+ NavierStokes_V1e-5_N1200_T20.mat
130
+ ```
131
+
132
+ Data downloads and documentation:
133
+
134
+ - https://github.com/thuml/Transolver/tree/main/PDE-Solving-StandardBenchmark
135
+ - https://drive.google.com/drive/folders/1UnbQh2WWc6knEHbLn-ZaXrKUZhp7pjt-
136
+
137
+ The OneScience community also provides the training data. Download it with the following command and verify that the data path in `conf/config.yaml` is configured correctly:
138
+
139
+ ```bash
140
+ modelscope download --dataset OneScience/cfd_benchmark data/ns/NavierStokes_V1e-5_N1200_T20.mat --local_dir ./data
141
+ ```
142
+
143
+ ### Training
144
+
145
+ ```bash
146
+ python scripts/train.py
147
+ ```
148
+
149
+ The default checkpoint is saved to `weight/uno_navier_stokes.pt`. The training script is controlled entirely by `conf/config.yaml` and does not accept command-line configuration arguments.
150
+
151
+ ### Model Weights
152
+
153
+ This repository provides weights trained on the standard Navier–Stokes dataset in the `weight/` directory.
154
+
155
+ ### Inference, Evaluation, and Visualization
156
+
157
+ ```bash
158
+ python scripts/inference.py
159
+ ```
160
+
161
+ The script loads `weight/uno_navier_stokes.pt` by default and generates the following files under `result/`:
162
+
163
+ ```text
164
+ result/
165
+ prediction_sample.pt
166
+ prediction_sample.png
167
+ ```
168
+
169
+ The inference script is likewise controlled entirely by `conf/config.yaml` and does not accept command-line configuration arguments. The weight path is determined jointly by `training.weight_dir` and `training.checkpoint_name`; the output directory and number of saved samples are controlled by `inference.result_dir` and `inference.num_samples`, respectively.
170
+
171
+ # Configuration
172
+
173
+ `conf/config.yaml` contains five sections:
174
+
175
+ - `common`: device and random seed
176
+ - `datapipe`: data file, sample splits, temporal windows, downsampling, and DataLoader settings
177
+ - `model`: hidden channels, Fourier modes, normalization, and spatial padding
178
+ - `training`: optimizer, learning-rate schedule, early stopping, and weight directory
179
+ - `inference`: inference result directory and number of saved samples
180
+
181
+ The model automatically updates `in_dim` from `t_in * out_dim` in the configuration, so the model input dimension does not need to be synchronized manually when the history window changes.
182
+
183
+ # Data Format
184
+
185
+ The `.mat` file must contain a variable named `u` with the following standard shape:
186
+
187
+ ```text
188
+ [1200, 64, 64, 20]
189
+ ```
190
+
191
+ | Dimension | Meaning |
192
+ | --- | --- |
193
+ | `1200` | Number of independent flow-field samples |
194
+ | `64, 64` | Height and width of the two-dimensional regular grid |
195
+ | `20` | Number of consecutive time steps |
196
+
197
+ The data pipeline produces:
198
+
199
+ - `pos`: `[H*W, 2]`, normalized two-dimensional coordinates
200
+ - `x`: `[H*W, t_in*out_dim]`, historical states
201
+ - `y`: `[H*W, t_out*out_dim]`, future states
202
+
203
+ # Official OneScience Resources
204
+
205
+ | Platform | OneScience Repository | Skills Repository |
206
+ | --- | --- | --- |
207
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
208
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
209
+
210
+ # Citations and License
211
+
212
+ - Rahman, M. A., Ross, Z. E., and Azizzadenesheli, K. U-NO: U-shaped Neural Operators. arXiv:2204.11127, 2022.
213
+ - Li, Z. et al. Fourier Neural Operator for Parametric Partial Differential Equations. arXiv:2010.08895, 2020.
214
+ - This model package is released under the Apache-2.0 license and retains attribution to the original paper and data sources.
215
+
216
+
conf/config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root:
2
+ common:
3
+ device: "auto"
4
+ seed: 0
5
+
6
+ datapipe:
7
+ source:
8
+ data_dir: "/public/share/sugonhpcapp01/onestore/onedatasets/CFD_Benchmark/ns"
9
+ file_name: "NavierStokes_V1e-5_N1200_T20.mat"
10
+ data:
11
+ ntrain: 1000
12
+ ntest: 200
13
+ t_in: 10
14
+ t_out: 10
15
+ out_dim: 1
16
+ downsamplex: 1
17
+ downsampley: 1
18
+ normalize: false
19
+ dataloader:
20
+ batch_size: 8
21
+ num_workers: 4
22
+ pin_memory: true
23
+
24
+ model:
25
+ in_dim: 10
26
+ out_dim: 1
27
+ hidden_dim: 64
28
+ modes: 12
29
+ space_dim: 2
30
+ include_pos: true
31
+ normtype: "in"
32
+ bilinear: true
33
+ activation: "gelu"
34
+ pad_to_multiple: 16
35
+
36
+ training:
37
+ epochs: 500
38
+ lr: 0.001
39
+ weight_decay: 0.00001
40
+ step_size: 100
41
+ gamma: 0.5
42
+ eval_interval: 1
43
+ patience: 100
44
+ teacher_forcing: false
45
+ max_grad_norm: null
46
+ max_train_batches: null
47
+ max_eval_batches: null
48
+ weight_dir: "weight"
49
+ checkpoint_name: "uno_navier_stokes.pt"
50
+
51
+ inference:
52
+ result_dir: "result"
53
+ num_samples: 4
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
data/.gitkeep ADDED
File without changes
model/__pycache__/uno.cpython-311.pyc ADDED
Binary file (23 kB). View file
 
model/uno.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ def _activation(name: str) -> nn.Module:
11
+ activations = {
12
+ "gelu": nn.GELU,
13
+ "relu": nn.ReLU,
14
+ "silu": nn.SiLU,
15
+ "tanh": nn.Tanh,
16
+ }
17
+ try:
18
+ return activations[name.lower()]()
19
+ except KeyError as error:
20
+ raise ValueError(f"Unsupported activation: {name}") from error
21
+
22
+
23
+ class MLP(nn.Module):
24
+ def __init__(self, input_dim: int, hidden_dim: int, output_dim: int, activation: str) -> None:
25
+ super().__init__()
26
+ self.layers = nn.Sequential(
27
+ nn.Linear(input_dim, hidden_dim),
28
+ _activation(activation),
29
+ nn.Linear(hidden_dim, output_dim),
30
+ )
31
+
32
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
33
+ return self.layers(x)
34
+
35
+
36
+ class SpectralConv2d(nn.Module):
37
+ """Two-dimensional Fourier convolution over the lowest frequency modes."""
38
+
39
+ def __init__(self, in_channels: int, out_channels: int, modes1: int, modes2: int) -> None:
40
+ super().__init__()
41
+ self.in_channels = in_channels
42
+ self.out_channels = out_channels
43
+ self.modes1 = modes1
44
+ self.modes2 = modes2
45
+ scale = 1.0 / (in_channels * out_channels)
46
+ shape = (in_channels, out_channels, modes1, modes2)
47
+ self.weights1 = nn.Parameter(scale * torch.rand(*shape, dtype=torch.cfloat))
48
+ self.weights2 = nn.Parameter(scale * torch.rand(*shape, dtype=torch.cfloat))
49
+
50
+ @staticmethod
51
+ def _multiply(inputs: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
52
+ return torch.einsum("bixy,ioxy->boxy", inputs, weights)
53
+
54
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
55
+ batch_size, _, height, width = x.shape
56
+ x_ft = torch.fft.rfft2(x)
57
+ out_ft = torch.zeros(
58
+ batch_size,
59
+ self.out_channels,
60
+ height,
61
+ width // 2 + 1,
62
+ dtype=torch.cfloat,
63
+ device=x.device,
64
+ )
65
+ modes1 = min(self.modes1, max(1, height // 2))
66
+ modes2 = min(self.modes2, width // 2 + 1)
67
+ out_ft[:, :, :modes1, :modes2] = self._multiply(
68
+ x_ft[:, :, :modes1, :modes2],
69
+ self.weights1[:, :, :modes1, :modes2],
70
+ )
71
+ out_ft[:, :, -modes1:, :modes2] = self._multiply(
72
+ x_ft[:, :, -modes1:, :modes2],
73
+ self.weights2[:, :, :modes1, :modes2],
74
+ )
75
+ return torch.fft.irfft2(out_ft, s=(height, width))
76
+
77
+
78
+ def _normalization(normtype: str, channels: int) -> nn.Module:
79
+ normalized = normtype.lower()
80
+ if normalized == "bn":
81
+ return nn.BatchNorm2d(channels)
82
+ if normalized == "in":
83
+ return nn.InstanceNorm2d(channels, affine=True)
84
+ if normalized in {"none", "identity"}:
85
+ return nn.Identity()
86
+ raise ValueError(f"Unsupported normtype: {normtype}")
87
+
88
+
89
+ class DoubleConv2d(nn.Module):
90
+ def __init__(
91
+ self,
92
+ in_channels: int,
93
+ out_channels: int,
94
+ mid_channels: int | None = None,
95
+ normtype: str = "in",
96
+ ) -> None:
97
+ super().__init__()
98
+ mid_channels = mid_channels or out_channels
99
+ self.layers = nn.Sequential(
100
+ nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),
101
+ _normalization(normtype, mid_channels),
102
+ nn.ReLU(inplace=True),
103
+ nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),
104
+ _normalization(normtype, out_channels),
105
+ nn.ReLU(inplace=True),
106
+ )
107
+
108
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
109
+ return self.layers(x)
110
+
111
+
112
+ class Down2d(nn.Module):
113
+ def __init__(self, in_channels: int, out_channels: int, normtype: str) -> None:
114
+ super().__init__()
115
+ self.layers = nn.Sequential(
116
+ nn.MaxPool2d(2),
117
+ DoubleConv2d(in_channels, out_channels, normtype=normtype),
118
+ )
119
+
120
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
121
+ return self.layers(x)
122
+
123
+
124
+ class Up2d(nn.Module):
125
+ def __init__(
126
+ self,
127
+ in_channels: int,
128
+ out_channels: int,
129
+ bilinear: bool,
130
+ normtype: str,
131
+ ) -> None:
132
+ super().__init__()
133
+ self.bilinear = bilinear
134
+ if bilinear:
135
+ self.up = None
136
+ mid_channels = in_channels // 2
137
+ else:
138
+ self.up = nn.ConvTranspose2d(
139
+ in_channels,
140
+ in_channels // 2,
141
+ kernel_size=2,
142
+ stride=2,
143
+ )
144
+ mid_channels = None
145
+ self.conv = DoubleConv2d(
146
+ in_channels,
147
+ out_channels,
148
+ mid_channels=mid_channels,
149
+ normtype=normtype,
150
+ )
151
+
152
+ def forward(self, x: torch.Tensor, skip: torch.Tensor) -> torch.Tensor:
153
+ if self.bilinear:
154
+ x = F.interpolate(x, size=skip.shape[-2:], mode="bilinear", align_corners=True)
155
+ else:
156
+ x = self.up(x)
157
+ if x.shape[-2:] != skip.shape[-2:]:
158
+ x = F.interpolate(x, size=skip.shape[-2:], mode="nearest")
159
+ return self.conv(torch.cat((skip, x), dim=1))
160
+
161
+
162
+ class UNO(nn.Module):
163
+ """Two-dimensional U-shaped neural operator for regular CFD grids.
164
+
165
+ ``pos`` has shape ``[B, H*W, 2]`` and ``fx`` has shape
166
+ ``[B, H*W, in_dim]``. The returned tensor has shape
167
+ ``[B, H*W, out_dim]``.
168
+ """
169
+
170
+ def __init__(
171
+ self,
172
+ in_dim: int,
173
+ out_dim: int,
174
+ spatial_shape: Sequence[int],
175
+ hidden_dim: int = 64,
176
+ modes: int = 12,
177
+ space_dim: int = 2,
178
+ include_pos: bool = True,
179
+ normtype: str = "in",
180
+ bilinear: bool = True,
181
+ activation: str = "gelu",
182
+ pad_to_multiple: int = 16,
183
+ ) -> None:
184
+ super().__init__()
185
+ self.in_dim = int(in_dim)
186
+ self.out_dim = int(out_dim)
187
+ self.spatial_shape = tuple(int(value) for value in spatial_shape)
188
+ self.hidden_dim = int(hidden_dim)
189
+ self.modes = int(modes)
190
+ self.space_dim = int(space_dim)
191
+ self.include_pos = bool(include_pos)
192
+ self.pad_to_multiple = int(pad_to_multiple)
193
+ if len(self.spatial_shape) != 2:
194
+ raise ValueError(f"This standalone UNO supports 2D grids, got {self.spatial_shape}")
195
+ if self.space_dim != 2:
196
+ raise ValueError("space_dim must be 2 for the standalone 2D UNO")
197
+ if min(self.hidden_dim, self.modes, self.pad_to_multiple) < 1:
198
+ raise ValueError("hidden_dim, modes and pad_to_multiple must be positive")
199
+
200
+ input_dim = self.in_dim + (self.space_dim if self.include_pos else 0)
201
+ self.preprocess = MLP(input_dim, self.hidden_dim * 2, self.hidden_dim, activation)
202
+ factor = 2 if bilinear else 1
203
+
204
+ self.inc = DoubleConv2d(self.hidden_dim, self.hidden_dim, normtype=normtype)
205
+ self.down1 = Down2d(self.hidden_dim, self.hidden_dim * 2, normtype)
206
+ self.down2 = Down2d(self.hidden_dim * 2, self.hidden_dim * 4, normtype)
207
+ self.down3 = Down2d(self.hidden_dim * 4, self.hidden_dim * 8, normtype)
208
+ self.down4 = Down2d(self.hidden_dim * 8, self.hidden_dim * 16 // factor, normtype)
209
+
210
+ self.up1 = Up2d(self.hidden_dim * 16, self.hidden_dim * 8 // factor, bilinear, normtype)
211
+ self.up2 = Up2d(self.hidden_dim * 8, self.hidden_dim * 4 // factor, bilinear, normtype)
212
+ self.up3 = Up2d(self.hidden_dim * 4, self.hidden_dim * 2 // factor, bilinear, normtype)
213
+ self.up4 = Up2d(self.hidden_dim * 2, self.hidden_dim, bilinear, normtype)
214
+ self.outc = nn.Conv2d(self.hidden_dim, self.hidden_dim, kernel_size=1)
215
+
216
+ augmented = self._augmented_shape(self.spatial_shape)
217
+ self.padding = tuple(target - source for target, source in zip(augmented, self.spatial_shape))
218
+ down_channels = [
219
+ self.hidden_dim,
220
+ self.hidden_dim * 2,
221
+ self.hidden_dim * 4,
222
+ self.hidden_dim * 8,
223
+ self.hidden_dim * 16 // factor,
224
+ ]
225
+ up_channels = [
226
+ self.hidden_dim * 16 // factor,
227
+ self.hidden_dim * 8 // factor,
228
+ self.hidden_dim * 4 // factor,
229
+ self.hidden_dim * 2 // factor,
230
+ self.hidden_dim,
231
+ ]
232
+ divisors = [2, 4, 8, 16, 32]
233
+ self.spectral_down = nn.ModuleList(
234
+ self._spectral(channels, augmented, divisor)
235
+ for channels, divisor in zip(down_channels, divisors)
236
+ )
237
+ self.pointwise_down = nn.ModuleList(
238
+ nn.Conv2d(channels, channels, kernel_size=1) for channels in down_channels
239
+ )
240
+ self.spectral_up = nn.ModuleList(
241
+ self._spectral(channels, augmented, divisor)
242
+ for channels, divisor in zip(up_channels, reversed(divisors))
243
+ )
244
+ self.pointwise_up = nn.ModuleList(
245
+ nn.Conv2d(channels, channels, kernel_size=1) for channels in up_channels
246
+ )
247
+ self.fc1 = nn.Linear(self.hidden_dim, self.hidden_dim * 2)
248
+ self.fc2 = nn.Linear(self.hidden_dim * 2, self.out_dim)
249
+
250
+ def _augmented_shape(self, shape: Sequence[int]) -> tuple[int, int]:
251
+ return tuple(
252
+ size + (self.pad_to_multiple - size % self.pad_to_multiple) % self.pad_to_multiple
253
+ for size in shape
254
+ )
255
+
256
+ def _spectral(
257
+ self,
258
+ channels: int,
259
+ augmented_shape: Sequence[int],
260
+ divisor: int,
261
+ ) -> SpectralConv2d:
262
+ modes = [
263
+ max(1, min(self.modes, max(1, size // divisor)))
264
+ for size in augmented_shape
265
+ ]
266
+ return SpectralConv2d(channels, channels, modes[0], modes[1])
267
+
268
+ @staticmethod
269
+ def _operator(
270
+ x: torch.Tensor,
271
+ spectral: nn.Module,
272
+ pointwise: nn.Module,
273
+ ) -> torch.Tensor:
274
+ return F.gelu(spectral(x) + pointwise(x))
275
+
276
+ def _pad(self, x: torch.Tensor) -> torch.Tensor:
277
+ pad_height, pad_width = self.padding
278
+ return F.pad(x, (0, pad_width, 0, pad_height)) if any(self.padding) else x
279
+
280
+ def _unpad(self, x: torch.Tensor) -> torch.Tensor:
281
+ height, width = self.spatial_shape
282
+ return x[..., :height, :width]
283
+
284
+ def forward(self, pos: torch.Tensor, fx: torch.Tensor | None = None) -> torch.Tensor:
285
+ batch_size, point_count, coordinates = pos.shape
286
+ expected_points = self.spatial_shape[0] * self.spatial_shape[1]
287
+ if point_count != expected_points or coordinates != self.space_dim:
288
+ raise ValueError(
289
+ f"Expected pos [B, {expected_points}, {self.space_dim}], got {tuple(pos.shape)}"
290
+ )
291
+ if fx is None:
292
+ if self.in_dim:
293
+ raise ValueError("fx is required when in_dim > 0")
294
+ features = pos if self.include_pos else pos.new_empty(batch_size, point_count, 0)
295
+ else:
296
+ if fx.shape != (batch_size, point_count, self.in_dim):
297
+ raise ValueError(
298
+ f"Expected fx [B, {point_count}, {self.in_dim}], got {tuple(fx.shape)}"
299
+ )
300
+ features = torch.cat((pos, fx), dim=-1) if self.include_pos else fx
301
+
302
+ x = self.preprocess(features)
303
+ x = x.permute(0, 2, 1).reshape(batch_size, self.hidden_dim, *self.spatial_shape)
304
+ x = self._pad(x)
305
+
306
+ x1 = self._operator(self.inc(x), self.spectral_down[0], self.pointwise_down[0])
307
+ x2 = self._operator(self.down1(x1), self.spectral_down[1], self.pointwise_down[1])
308
+ x3 = self._operator(self.down2(x2), self.spectral_down[2], self.pointwise_down[2])
309
+ x4 = self._operator(self.down3(x3), self.spectral_down[3], self.pointwise_down[3])
310
+ x5 = self._operator(self.down4(x4), self.spectral_down[4], self.pointwise_down[4])
311
+ x = self._operator(x5, self.spectral_up[0], self.pointwise_up[0])
312
+ x = self._operator(self.up1(x, x4), self.spectral_up[1], self.pointwise_up[1])
313
+ x = self._operator(self.up2(x, x3), self.spectral_up[2], self.pointwise_up[2])
314
+ x = self._operator(self.up3(x, x2), self.spectral_up[3], self.pointwise_up[3])
315
+ x = self._operator(self.up4(x, x1), self.spectral_up[4], self.pointwise_up[4])
316
+ x = self._unpad(self.outc(x))
317
+
318
+ x = x.reshape(batch_size, self.hidden_dim, -1).permute(0, 2, 1)
319
+ return self.fc2(F.gelu(self.fc1(x)))
scripts/__pycache__/common.cpython-311.pyc ADDED
Binary file (7.31 kB). View file
 
scripts/common.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from collections.abc import Mapping
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import torch
10
+ import yaml
11
+
12
+
13
+ class AttrDict(dict):
14
+ """Dictionary with recursive attribute access for OneScience datapipes."""
15
+
16
+ def __getattr__(self, key: str) -> Any:
17
+ try:
18
+ return self[key]
19
+ except KeyError as error:
20
+ raise AttributeError(key) from error
21
+
22
+ def __setattr__(self, key: str, value: Any) -> None:
23
+ self[key] = value
24
+
25
+
26
+ def to_attr_dict(value: Any) -> Any:
27
+ if isinstance(value, Mapping):
28
+ return AttrDict({key: to_attr_dict(item) for key, item in value.items()})
29
+ if isinstance(value, list):
30
+ return [to_attr_dict(item) for item in value]
31
+ return value
32
+
33
+
34
+ def to_plain_dict(value: Any) -> Any:
35
+ if isinstance(value, Mapping):
36
+ return {key: to_plain_dict(item) for key, item in value.items()}
37
+ if isinstance(value, list):
38
+ return [to_plain_dict(item) for item in value]
39
+ return value
40
+
41
+
42
+ def load_config(path: Path) -> dict[str, Any]:
43
+ with path.open("r", encoding="utf-8") as stream:
44
+ config = yaml.safe_load(stream)
45
+ if not isinstance(config, dict) or not isinstance(config.get("root"), dict):
46
+ raise ValueError(f"Config must contain a root mapping: {path}")
47
+ return config["root"]
48
+
49
+
50
+ def project_path(value: str | Path, project_root: Path) -> Path:
51
+ path = Path(value).expanduser()
52
+ return path if path.is_absolute() else project_root / path
53
+
54
+
55
+ def resolve_device(requested: str) -> torch.device:
56
+ if requested == "auto":
57
+ return torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
58
+ device = torch.device(requested)
59
+ if device.type == "cuda" and not torch.cuda.is_available():
60
+ raise RuntimeError("CUDA/DCU was requested but torch.cuda.is_available() is false")
61
+ return device
62
+
63
+
64
+ def seed_everything(seed: int) -> None:
65
+ random.seed(seed)
66
+ np.random.seed(seed)
67
+ torch.manual_seed(seed)
68
+ if torch.cuda.is_available():
69
+ torch.cuda.manual_seed_all(seed)
70
+
71
+
72
+ def rollout(
73
+ model: torch.nn.Module,
74
+ pos: torch.Tensor,
75
+ state: torch.Tensor,
76
+ t_out: int,
77
+ out_dim: int,
78
+ teacher_forcing: bool = False,
79
+ target: torch.Tensor | None = None,
80
+ ) -> torch.Tensor:
81
+ predictions = []
82
+ for step in range(t_out):
83
+ prediction = model(pos, state)
84
+ predictions.append(prediction)
85
+ next_frame = (
86
+ target[..., step * out_dim : (step + 1) * out_dim]
87
+ if teacher_forcing and target is not None
88
+ else prediction
89
+ )
90
+ state = torch.cat((state[..., out_dim:], next_frame), dim=-1)
91
+ return torch.cat(predictions, dim=-1)
92
+
93
+
94
+ def relative_l2(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
95
+ prediction = prediction.reshape(prediction.shape[0], -1)
96
+ target = target.reshape(target.shape[0], -1)
97
+ numerator = torch.linalg.vector_norm(prediction - target, dim=1)
98
+ denominator = torch.linalg.vector_norm(target, dim=1).clamp_min(1.0e-8)
99
+ return numerator / denominator
scripts/inference.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import matplotlib
8
+
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ import torch
12
+
13
+
14
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(PROJECT_ROOT))
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+ LOCAL_WORKSPACE = PROJECT_ROOT.parents[1]
18
+ if (LOCAL_WORKSPACE / "onescience" / "src" / "onescience" / "datapipes").is_dir():
19
+ sys.path.insert(0, str(LOCAL_WORKSPACE))
20
+
21
+ from common import ( # noqa: E402
22
+ load_config,
23
+ project_path,
24
+ relative_l2,
25
+ resolve_device,
26
+ rollout,
27
+ to_attr_dict,
28
+ )
29
+ from model.uno import UNO # noqa: E402
30
+ from onescience.datapipes.cfd import NavierStokesDatapipe # noqa: E402
31
+ from onescience.distributed.manager import DistributedManager # noqa: E402
32
+
33
+
34
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
35
+
36
+
37
+ def prepare_config(config: dict[str, Any]) -> None:
38
+ datapipe = config["datapipe"]
39
+ datapipe["source"]["data_dir"] = str(
40
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
41
+ )
42
+
43
+
44
+ def validate_checkpoint_config(
45
+ datapipe_config: dict[str, Any],
46
+ checkpoint: dict[str, Any],
47
+ ) -> None:
48
+ checkpoint_data = checkpoint["datapipe_config"]["data"]
49
+ current_data = datapipe_config["data"]
50
+ for key in ("t_in", "t_out", "out_dim", "downsamplex", "downsampley", "normalize"):
51
+ if current_data[key] != checkpoint_data[key]:
52
+ raise ValueError(
53
+ f"Config value datapipe.data.{key}={current_data[key]} does not match "
54
+ f"checkpoint value {checkpoint_data[key]}"
55
+ )
56
+
57
+
58
+ def build_model(model_config: dict[str, Any], spatial_shape: tuple[int, int]) -> UNO:
59
+ return UNO(
60
+ in_dim=int(model_config["in_dim"]),
61
+ out_dim=int(model_config["out_dim"]),
62
+ spatial_shape=spatial_shape,
63
+ hidden_dim=int(model_config["hidden_dim"]),
64
+ modes=int(model_config["modes"]),
65
+ space_dim=int(model_config["space_dim"]),
66
+ include_pos=bool(model_config["include_pos"]),
67
+ normtype=str(model_config["normtype"]),
68
+ bilinear=bool(model_config["bilinear"]),
69
+ activation=str(model_config["activation"]),
70
+ pad_to_multiple=int(model_config["pad_to_multiple"]),
71
+ )
72
+
73
+
74
+ def save_visualization(
75
+ prediction: torch.Tensor,
76
+ target: torch.Tensor,
77
+ spatial_shape: tuple[int, int],
78
+ output_path: Path,
79
+ ) -> None:
80
+ prediction = prediction.detach().cpu()
81
+ target = target.detach().cpu()
82
+ height, width = spatial_shape
83
+ frame_count = prediction.shape[-1]
84
+ frames = sorted({0, frame_count // 2, frame_count - 1})
85
+ sample_prediction = prediction[0].reshape(height, width, frame_count)
86
+ sample_target = target[0].reshape(height, width, frame_count)
87
+ rows = (
88
+ ("Target", sample_target),
89
+ ("Prediction", sample_prediction),
90
+ ("Abs Error", (sample_prediction - sample_target).abs()),
91
+ )
92
+ figure, axes = plt.subplots(3, len(frames), figsize=(4 * len(frames), 9), squeeze=False)
93
+ for row_index, (title, values) in enumerate(rows):
94
+ for column_index, frame in enumerate(frames):
95
+ axis = axes[row_index][column_index]
96
+ image = axis.imshow(values[..., frame], origin="lower", cmap="viridis")
97
+ axis.set_title(f"{title} t+{frame + 1}")
98
+ axis.set_xticks([])
99
+ axis.set_yticks([])
100
+ figure.colorbar(image, ax=axis, fraction=0.046, pad=0.04)
101
+ figure.tight_layout()
102
+ figure.savefig(output_path, dpi=150)
103
+ plt.close(figure)
104
+
105
+
106
+ def main() -> None:
107
+ config_path = DEFAULT_CONFIG.resolve()
108
+ config = load_config(config_path)
109
+ prepare_config(config)
110
+ training_config = config["training"]
111
+ inference_config = config["inference"]
112
+ datapipe_config = config["datapipe"]
113
+ num_samples = int(inference_config["num_samples"])
114
+ if num_samples < 1:
115
+ raise ValueError("num_samples must be positive")
116
+ checkpoint_path = (
117
+ project_path(training_config["weight_dir"], PROJECT_ROOT).resolve()
118
+ / str(training_config["checkpoint_name"])
119
+ )
120
+ if not checkpoint_path.exists():
121
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
122
+
123
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
124
+ model_config = dict(checkpoint["model_config"])
125
+ validate_checkpoint_config(datapipe_config, checkpoint)
126
+ data_file = (
127
+ Path(datapipe_config["source"]["data_dir"])
128
+ / datapipe_config["source"]["file_name"]
129
+ )
130
+ device = resolve_device(str(config["common"]["device"]))
131
+ result_dir = project_path(
132
+ inference_config["result_dir"], PROJECT_ROOT
133
+ ).resolve()
134
+
135
+ print(f"Checkpoint: {checkpoint_path}")
136
+ print(f"Data: {data_file}")
137
+ print(f"Device: {device}")
138
+ DistributedManager.initialize()
139
+ try:
140
+ datapipe = NavierStokesDatapipe(
141
+ to_attr_dict(datapipe_config),
142
+ distributed=False,
143
+ normalizer_state=checkpoint.get("normalizer"),
144
+ )
145
+ test_loader, _ = datapipe.test_dataloader()
146
+ spatial_shape = tuple(checkpoint["spatial_shape"])
147
+ if tuple(datapipe.spatial_shape) != spatial_shape:
148
+ raise ValueError(
149
+ f"Checkpoint grid {spatial_shape} does not match data grid {datapipe.spatial_shape}"
150
+ )
151
+ model = build_model(model_config, spatial_shape).to(device)
152
+ model.load_state_dict(checkpoint["model_state"])
153
+ model.eval()
154
+
155
+ batch = next(iter(test_loader))
156
+ pos = batch["pos"].to(device)
157
+ state = batch["x"].to(device)
158
+ target = batch["y"].to(device)
159
+ with torch.no_grad():
160
+ prediction = rollout(
161
+ model,
162
+ pos,
163
+ state,
164
+ int(datapipe_config["data"]["t_out"]),
165
+ int(datapipe_config["data"]["out_dim"]),
166
+ )
167
+ prediction = datapipe.decode_solution(prediction)
168
+ target = datapipe.decode_solution(target)
169
+ error = relative_l2(prediction, target).mean().item()
170
+ sample_count = min(num_samples, prediction.shape[0])
171
+ result_dir.mkdir(parents=True, exist_ok=True)
172
+ tensor_path = result_dir / "prediction_sample.pt"
173
+ image_path = result_dir / "prediction_sample.png"
174
+ torch.save(
175
+ {
176
+ "prediction": prediction[:sample_count].cpu(),
177
+ "target": target[:sample_count].cpu(),
178
+ "spatial_shape": spatial_shape,
179
+ "relative_l2": error,
180
+ },
181
+ tensor_path,
182
+ )
183
+ save_visualization(prediction[:1], target[:1], spatial_shape, image_path)
184
+ print(f"Relative L2: {error:.6e}")
185
+ print(f"Saved tensor: {tensor_path}")
186
+ print(f"Saved figure: {image_path}")
187
+ finally:
188
+ DistributedManager.cleanup()
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
scripts/train.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from tqdm import tqdm
11
+
12
+
13
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
14
+ sys.path.insert(0, str(PROJECT_ROOT))
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+ LOCAL_WORKSPACE = PROJECT_ROOT.parents[1]
17
+ if (LOCAL_WORKSPACE / "onescience" / "src" / "onescience" / "datapipes").is_dir():
18
+ sys.path.insert(0, str(LOCAL_WORKSPACE))
19
+
20
+ from common import ( # noqa: E402
21
+ load_config,
22
+ project_path,
23
+ relative_l2,
24
+ resolve_device,
25
+ rollout,
26
+ seed_everything,
27
+ to_attr_dict,
28
+ to_plain_dict,
29
+ )
30
+ from model.uno import UNO # noqa: E402
31
+ from onescience.datapipes.cfd import NavierStokesDatapipe # noqa: E402
32
+ from onescience.distributed.manager import DistributedManager # noqa: E402
33
+
34
+
35
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
36
+
37
+
38
+ def prepare_config(config: dict[str, Any]) -> None:
39
+ datapipe = config["datapipe"]
40
+ data = datapipe["data"]
41
+ model = config["model"]
42
+ datapipe["source"]["data_dir"] = str(
43
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
44
+ )
45
+ model["in_dim"] = int(data["t_in"]) * int(data["out_dim"])
46
+ model["out_dim"] = int(data["out_dim"])
47
+
48
+
49
+ def validate_config(config: dict[str, Any]) -> None:
50
+ data = config["datapipe"]["data"]
51
+ loader = config["datapipe"]["dataloader"]
52
+ model = config["model"]
53
+ training = config["training"]
54
+ positive_values = {
55
+ "ntrain": data["ntrain"],
56
+ "ntest": data["ntest"],
57
+ "t_in": data["t_in"],
58
+ "t_out": data["t_out"],
59
+ "downsamplex": data["downsamplex"],
60
+ "downsampley": data["downsampley"],
61
+ "batch_size": loader["batch_size"],
62
+ "hidden_dim": model["hidden_dim"],
63
+ "modes": model["modes"],
64
+ "epochs": training["epochs"],
65
+ "eval_interval": training["eval_interval"],
66
+ }
67
+ for name, value in positive_values.items():
68
+ if int(value) < 1:
69
+ raise ValueError(f"{name} must be positive, got {value}")
70
+ if int(loader["num_workers"]) < 0:
71
+ raise ValueError("num_workers cannot be negative")
72
+ for name in ("max_train_batches", "max_eval_batches"):
73
+ value = training.get(name)
74
+ if value is not None and int(value) < 1:
75
+ raise ValueError(f"{name} must be positive when set")
76
+
77
+
78
+ def build_model(model_config: dict[str, Any], spatial_shape: tuple[int, int]) -> UNO:
79
+ return UNO(
80
+ in_dim=int(model_config["in_dim"]),
81
+ out_dim=int(model_config["out_dim"]),
82
+ spatial_shape=spatial_shape,
83
+ hidden_dim=int(model_config["hidden_dim"]),
84
+ modes=int(model_config["modes"]),
85
+ space_dim=int(model_config["space_dim"]),
86
+ include_pos=bool(model_config["include_pos"]),
87
+ normtype=str(model_config["normtype"]),
88
+ bilinear=bool(model_config["bilinear"]),
89
+ activation=str(model_config["activation"]),
90
+ pad_to_multiple=int(model_config["pad_to_multiple"]),
91
+ )
92
+
93
+
94
+ def evaluate(
95
+ model: torch.nn.Module,
96
+ loader: torch.utils.data.DataLoader,
97
+ device: torch.device,
98
+ t_out: int,
99
+ out_dim: int,
100
+ max_batches: int | None,
101
+ ) -> tuple[float, float]:
102
+ model.eval()
103
+ squared_error = 0.0
104
+ element_count = 0
105
+ relative_error = 0.0
106
+ sample_count = 0
107
+ with torch.no_grad():
108
+ for batch_index, batch in enumerate(tqdm(loader, desc="Evaluating")):
109
+ if max_batches is not None and batch_index >= max_batches:
110
+ break
111
+ pos = batch["pos"].to(device)
112
+ state = batch["x"].to(device)
113
+ target = batch["y"].to(device)
114
+ prediction = rollout(model, pos, state, t_out, out_dim)
115
+ squared_error += F.mse_loss(prediction, target, reduction="sum").item()
116
+ element_count += target.numel()
117
+ relative_error += relative_l2(prediction, target).sum().item()
118
+ sample_count += target.shape[0]
119
+ return (
120
+ squared_error / max(element_count, 1),
121
+ relative_error / max(sample_count, 1),
122
+ )
123
+
124
+
125
+ def main() -> None:
126
+ config_path = DEFAULT_CONFIG.resolve()
127
+ config = load_config(config_path)
128
+ prepare_config(config)
129
+ validate_config(config)
130
+
131
+ common = config["common"]
132
+ data_config = config["datapipe"]
133
+ model_config = config["model"]
134
+ training = config["training"]
135
+ device = resolve_device(str(common["device"]))
136
+ seed_everything(int(common["seed"]))
137
+ weight_dir = project_path(training["weight_dir"], PROJECT_ROOT).resolve()
138
+ checkpoint_path = weight_dir / str(training["checkpoint_name"])
139
+
140
+ print(f"Config: {config_path}")
141
+ print(f"Data: {Path(data_config['source']['data_dir']) / data_config['source']['file_name']}")
142
+ print(f"Device: {device}")
143
+ print(
144
+ f"Samples: train={data_config['data']['ntrain']} test={data_config['data']['ntest']} "
145
+ f"t_in={data_config['data']['t_in']} t_out={data_config['data']['t_out']}"
146
+ )
147
+
148
+ DistributedManager.initialize()
149
+ started = time.time()
150
+ try:
151
+ datapipe = NavierStokesDatapipe(to_attr_dict(data_config), distributed=False)
152
+ train_loader, _ = datapipe.train_dataloader()
153
+ test_loader, _ = datapipe.test_dataloader()
154
+ model = build_model(model_config, datapipe.spatial_shape).to(device)
155
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
156
+ print(f"Spatial shape: {datapipe.spatial_shape}")
157
+ print(f"Parameters: {parameter_count:,}")
158
+
159
+ optimizer = torch.optim.AdamW(
160
+ model.parameters(),
161
+ lr=float(training["lr"]),
162
+ weight_decay=float(training["weight_decay"]),
163
+ )
164
+ scheduler = torch.optim.lr_scheduler.StepLR(
165
+ optimizer,
166
+ step_size=int(training["step_size"]),
167
+ gamma=float(training["gamma"]),
168
+ )
169
+ t_out = int(data_config["data"]["t_out"])
170
+ out_dim = int(data_config["data"]["out_dim"])
171
+ max_train_batches = training.get("max_train_batches")
172
+ max_train_batches = None if max_train_batches is None else int(max_train_batches)
173
+ max_eval_batches = training.get("max_eval_batches")
174
+ max_eval_batches = None if max_eval_batches is None else int(max_eval_batches)
175
+ best_relative_l2 = float("inf")
176
+ stale_evaluations = 0
177
+
178
+ for epoch in range(1, int(training["epochs"]) + 1):
179
+ model.train()
180
+ epoch_loss = 0.0
181
+ batch_count = 0
182
+ iterator = tqdm(train_loader, desc=f"Epoch {epoch}")
183
+ for batch_index, batch in enumerate(iterator):
184
+ if max_train_batches is not None and batch_index >= max_train_batches:
185
+ break
186
+ pos = batch["pos"].to(device)
187
+ state = batch["x"].to(device)
188
+ target = batch["y"].to(device)
189
+ optimizer.zero_grad(set_to_none=True)
190
+ prediction = rollout(
191
+ model,
192
+ pos,
193
+ state,
194
+ t_out,
195
+ out_dim,
196
+ teacher_forcing=bool(training["teacher_forcing"]),
197
+ target=target,
198
+ )
199
+ loss = F.mse_loss(prediction, target)
200
+ if not torch.isfinite(loss):
201
+ raise FloatingPointError(f"Non-finite training loss at epoch {epoch}")
202
+ loss.backward()
203
+ if training.get("max_grad_norm") is not None:
204
+ torch.nn.utils.clip_grad_norm_(
205
+ model.parameters(), float(training["max_grad_norm"])
206
+ )
207
+ optimizer.step()
208
+ epoch_loss += loss.item()
209
+ batch_count += 1
210
+ iterator.set_postfix(loss=f"{loss.item():.3e}")
211
+ scheduler.step()
212
+ if batch_count == 0:
213
+ raise RuntimeError("No training batches were processed")
214
+
215
+ should_evaluate = (
216
+ epoch % int(training["eval_interval"]) == 0
217
+ or epoch == int(training["epochs"])
218
+ )
219
+ if not should_evaluate:
220
+ continue
221
+ validation_mse, validation_relative_l2 = evaluate(
222
+ model,
223
+ test_loader,
224
+ device,
225
+ t_out,
226
+ out_dim,
227
+ max_eval_batches,
228
+ )
229
+ print(
230
+ f"epoch={epoch:4d} train_mse={epoch_loss / batch_count:.6e} "
231
+ f"val_mse={validation_mse:.6e} val_relative_l2={validation_relative_l2:.6e}"
232
+ )
233
+ if validation_relative_l2 < best_relative_l2:
234
+ best_relative_l2 = validation_relative_l2
235
+ stale_evaluations = 0
236
+ weight_dir.mkdir(parents=True, exist_ok=True)
237
+ torch.save(
238
+ {
239
+ "epoch": epoch,
240
+ "model_state": model.state_dict(),
241
+ "model_config": to_plain_dict(model_config),
242
+ "datapipe_config": to_plain_dict(data_config),
243
+ "training_config": to_plain_dict(training),
244
+ "spatial_shape": tuple(datapipe.spatial_shape),
245
+ "normalizer": datapipe.get_normalizer_state(),
246
+ "best_relative_l2": best_relative_l2,
247
+ },
248
+ checkpoint_path,
249
+ )
250
+ print(f"Saved checkpoint: {checkpoint_path}")
251
+ else:
252
+ stale_evaluations += 1
253
+ if stale_evaluations >= int(training["patience"]):
254
+ print("Early stopping triggered")
255
+ break
256
+
257
+ print(f"Elapsed: {time.time() - started:.1f}s")
258
+ finally:
259
+ DistributedManager.cleanup()
260
+
261
+
262
+ if __name__ == "__main__":
263
+ main()
weight/.gitkeep ADDED
File without changes
weight/uno_navier_stokes.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be9c922eb78c7dd4b6da63121900d5de9c2601a6acbe112f45103e00f1ca7bc3
3
+ size 340569754