OneScience commited on
Commit
da0023d
·
verified ·
1 Parent(s): 70486ad

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,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - pytorch
4
+ language:
5
+ - en
6
+ license: gpl-3.0
7
+ tags:
8
+ - OneScience
9
+ - KNO
10
+ - Koopman-operator
11
+ - neural-operator
12
+ - computational-fluid-dynamics
13
+ - Navier-Stokes
14
+ tasks:
15
+ - time-series-prediction
16
+ ---
17
+ <p align="center">
18
+ <strong>
19
+ <span style="font-size: 30px;">KNO</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Overview
24
+
25
+ KNO (Koopman Neural Operator) is a neural operator based on Koopman operator theory for learning the evolution of nonlinear dynamical systems. This model predicts Navier–Stokes time series on a two-dimensional regular grid, using the first 10 time steps to predict the subsequent 10 by default.
26
+
27
+ Paper: Koopman Neural Operator as a Mesh-free Solver of Non-linear Partial Differential Equations
28
+ https://doi.org/10.1016/j.jcp.2024.113194
29
+
30
+ # Model Description
31
+
32
+ KNO uses an encoder to map historical physical fields into a latent Koopman space, learns an approximately linear evolution operator in the Fourier domain, and reconstructs future physical fields with a decoder. The model supports either linear or nonlinear latent-state propagation and performs multistep flow prediction autoregressively.
33
+
34
+ # Use Cases
35
+
36
+ | Use Case | Description |
37
+ | :---: | :--- |
38
+ | Flow-field time-series prediction | Predict future Navier–Stokes states from historical vorticity fields |
39
+ | Koopman operator research | Study approximately linear evolution of nonlinear dynamical systems in latent space |
40
+ | CFD surrogate modeling | Learn mappings from historical to future physical fields on regular grids |
41
+ | Pipeline validation | Validate training and inference using the bundled weights or a small-scale configuration |
42
+
43
+ # Usage
44
+
45
+ ## 1. OneCode
46
+
47
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
48
+
49
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
50
+
51
+ ## 2. Manual Setup
52
+
53
+ **Hardware Requirements**
54
+
55
+ - A GPU or DCU is recommended for training and inference.
56
+ - A CPU can be used for small-scale pipeline validation, but full training will be slow.
57
+ - DCU users must install DTK and a PyTorch environment compatible with the target cluster.
58
+
59
+ ### Download the Model Package
60
+
61
+ ```bash
62
+ modelscope download --model OneScience/KNO --local_dir ./KNO
63
+ cd KNO
64
+ ```
65
+
66
+ ### Set Up the Runtime Environment
67
+
68
+ **DCU Environment**
69
+
70
+ ```bash
71
+ # Activate DTK and Conda first
72
+ conda create -n onescience311 python=3.11 -y
73
+ conda activate onescience311
74
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
75
+ ```
76
+
77
+ **GPU Environment**
78
+
79
+ ```bash
80
+ # Activate Conda first
81
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
82
+ conda activate onescience311
83
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
84
+ ```
85
+
86
+ ### Training Data
87
+
88
+ The model uses the standard Navier–Stokes dataset `NavierStokes_V1e-5_N1200_T20.mat`, in which the data variable `u` has shape `[1200, 64, 64, 20]`. Download the data with the following command and verify that the data path in `conf/config.yaml` is configured correctly:
89
+
90
+ ```bash
91
+ modelscope download --dataset OneScience/cfd_benchmark data/ns/NavierStokes_V1e-5_N1200_T20.mat --local_dir ./data
92
+ ```
93
+
94
+ ### Training
95
+
96
+ ```bash
97
+ python scripts/train.py
98
+ ```
99
+
100
+ The default weights are saved to `weight/kno_navier_stokes.pt`.
101
+
102
+ ### Model Weights
103
+
104
+ This repository provides weights trained on the standard Navier–Stokes dataset in the `weight/` directory.
105
+
106
+ ### Inference, Evaluation, and Visualization
107
+
108
+ The model package includes weights for pipeline validation. After preparing the data, run:
109
+
110
+ ```bash
111
+ python scripts/inference.py
112
+ ```
113
+
114
+ The script loads `weight/kno_navier_stokes.pt` by default. Predicted tensors and visualizations are saved to the `result/` directory. Training and inference parameters can be modified in `conf/config.yaml`.
115
+
116
+ # Official OneScience Resources
117
+
118
+ | Platform | OneScience Repository | Skills Repository |
119
+ | --- | --- | --- |
120
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
121
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
122
+
123
+ # Citations and License
124
+
125
+ - Xiong, W. et al. Koopman Neural Operator as a Mesh-free Solver of Non-linear Partial Differential Equations. Journal of Computational Physics, 2024.
126
+ - Xiong, W. et al. KoopmanLab: Machine Learning for Solving Complex Physics Equations. APL Machine Learning, 2023.
127
+ - The model implementation is derived from KoopmanLab under the GPL-3.0 license. This model package retains the GPL-3.0 license and the corresponding source attribution.
conf/config.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ name: "KNO2DNavierStokes"
26
+ input_channels: 10
27
+ output_channels: 1
28
+ op_size: 32
29
+ modes_x: 12
30
+ modes_y: 12
31
+ decompose: 6
32
+ linear_type: true
33
+ normalization: false
34
+
35
+ training:
36
+ epochs: 500
37
+ lr: 0.001
38
+ weight_decay: 0.00001
39
+ step_size: 100
40
+ gamma: 0.5
41
+ eval_interval: 10
42
+ patience: 100
43
+ teacher_forcing: true
44
+ max_grad_norm: null
45
+ max_train_batches: null
46
+ max_eval_batches: null
47
+ weight_dir: "weight"
48
+ checkpoint_name: "kno_navier_stokes.pt"
49
+
50
+ inference:
51
+ result_dir: "result"
52
+ num_samples: 4
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
data/.gitkeep ADDED
File without changes
download.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Download files larger than 1MB, excluding .sh .py .md .yaml .yml
3
+ # Total large files: 1
4
+ modelscope download --model OneScience/KNO weight/kno_navier_stokes.pt --local_dir ./
model/__pycache__/kno.cpython-311.pyc ADDED
Binary file (19.3 kB). View file
 
model/kno.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+
7
+ class EncoderMLP(nn.Module):
8
+ def __init__(self, t_len: int, op_size: int):
9
+ super().__init__()
10
+ self.layer = nn.Linear(t_len, op_size)
11
+
12
+ def forward(self, x):
13
+ return self.layer(x)
14
+
15
+
16
+ class DecoderMLP(nn.Module):
17
+ def __init__(self, t_len: int, op_size: int):
18
+ super().__init__()
19
+ self.layer = nn.Linear(op_size, t_len)
20
+
21
+ def forward(self, x):
22
+ return self.layer(x)
23
+
24
+
25
+ class EncoderConv1D(nn.Module):
26
+ def __init__(self, t_len: int, op_size: int):
27
+ super().__init__()
28
+ self.layer = nn.Conv1d(t_len, op_size, 1)
29
+
30
+ def forward(self, x):
31
+ x = x.permute(0, 2, 1)
32
+ x = self.layer(x)
33
+ return x.permute(0, 2, 1)
34
+
35
+
36
+ class DecoderConv1D(nn.Module):
37
+ def __init__(self, t_len: int, op_size: int):
38
+ super().__init__()
39
+ self.layer = nn.Conv1d(op_size, t_len, 1)
40
+
41
+ def forward(self, x):
42
+ x = x.permute(0, 2, 1)
43
+ x = self.layer(x)
44
+ return x.permute(0, 2, 1)
45
+
46
+
47
+ class EncoderConv2D(nn.Module):
48
+ def __init__(self, t_len: int, op_size: int):
49
+ super().__init__()
50
+ self.layer = nn.Conv2d(t_len, op_size, 1)
51
+
52
+ def forward(self, x):
53
+ x = x.permute(0, 3, 1, 2)
54
+ x = self.layer(x)
55
+ return x.permute(0, 2, 3, 1)
56
+
57
+
58
+ class DecoderConv2D(nn.Module):
59
+ def __init__(self, t_len: int, op_size: int):
60
+ super().__init__()
61
+ self.layer = nn.Conv2d(op_size, t_len, 1)
62
+
63
+ def forward(self, x):
64
+ x = x.permute(0, 3, 1, 2)
65
+ x = self.layer(x)
66
+ return x.permute(0, 2, 3, 1)
67
+
68
+
69
+ class KoopmanOperator1D(nn.Module):
70
+ def __init__(self, op_size: int, modes_x: int = 16):
71
+ super().__init__()
72
+ self.op_size = int(op_size)
73
+ self.modes_x = int(modes_x)
74
+ if min(self.op_size, self.modes_x) < 1:
75
+ raise ValueError("op_size and modes_x must be positive")
76
+ scale = 1.0 / (self.op_size * self.op_size)
77
+ self.koopman_matrix = nn.Parameter(
78
+ scale * torch.rand(self.op_size, self.op_size, self.modes_x, dtype=torch.cfloat)
79
+ )
80
+
81
+ def time_marching(self, input_tensor, weights):
82
+ return torch.einsum("btx,tfx->bfx", input_tensor, weights)
83
+
84
+ def forward(self, x):
85
+ x_ft = torch.fft.rfft(x)
86
+ out_ft = torch.zeros(x_ft.shape, dtype=torch.cfloat, device=x.device)
87
+ modes_x = min(self.modes_x, x_ft.shape[-1])
88
+ out_ft[:, :, :modes_x] = self.time_marching(
89
+ x_ft[:, :, :modes_x],
90
+ self.koopman_matrix[:, :, :modes_x],
91
+ )
92
+ return torch.fft.irfft(out_ft, n=x.size(-1))
93
+
94
+
95
+ class KNO1D(nn.Module):
96
+ def __init__(
97
+ self,
98
+ encoder: nn.Module,
99
+ decoder: nn.Module,
100
+ op_size: int,
101
+ modes_x: int = 16,
102
+ decompose: int = 4,
103
+ linear_type: bool = True,
104
+ normalization: bool = False,
105
+ ):
106
+ super().__init__()
107
+ self.op_size = int(op_size)
108
+ self.decompose = int(decompose)
109
+ if self.decompose < 1:
110
+ raise ValueError("decompose must be positive")
111
+ self.enc = encoder
112
+ self.dec = decoder
113
+ self.koopman_layer = KoopmanOperator1D(self.op_size, modes_x=modes_x)
114
+ self.w0 = nn.Conv1d(self.op_size, self.op_size, 1)
115
+ self.linear_type = bool(linear_type)
116
+ self.normalization = bool(normalization)
117
+ if self.normalization:
118
+ self.norm_layer = nn.BatchNorm1d(self.op_size)
119
+
120
+ def forward(self, x):
121
+ x_reconstruct = self.dec(torch.tanh(self.enc(x)))
122
+ x = torch.tanh(self.enc(x)).permute(0, 2, 1)
123
+ x_w = x
124
+ for _ in range(self.decompose):
125
+ x1 = self.koopman_layer(x)
126
+ x = x + x1 if self.linear_type else torch.tanh(x + x1)
127
+ shortcut = self.w0(x_w)
128
+ if self.normalization:
129
+ shortcut = self.norm_layer(shortcut)
130
+ x = torch.tanh(shortcut + x).permute(0, 2, 1)
131
+ return self.dec(x), x_reconstruct
132
+
133
+
134
+ class KoopmanOperator2D(nn.Module):
135
+ def __init__(self, op_size: int, modes_x: int = 12, modes_y: int = 12):
136
+ super().__init__()
137
+ self.op_size = int(op_size)
138
+ self.modes_x = int(modes_x)
139
+ self.modes_y = int(modes_y)
140
+ if min(self.op_size, self.modes_x, self.modes_y) < 1:
141
+ raise ValueError("op_size, modes_x and modes_y must be positive")
142
+ scale = 1.0 / (self.op_size * self.op_size)
143
+ self.koopman_matrix = nn.Parameter(
144
+ scale * torch.rand(
145
+ self.op_size,
146
+ self.op_size,
147
+ self.modes_x,
148
+ self.modes_y,
149
+ dtype=torch.cfloat,
150
+ )
151
+ )
152
+
153
+ def time_marching(self, input_tensor, weights):
154
+ return torch.einsum("btxy,tfxy->bfxy", input_tensor, weights)
155
+
156
+ def forward(self, x):
157
+ x_ft = torch.fft.rfft2(x)
158
+ out_ft = torch.zeros(x_ft.shape, dtype=torch.cfloat, device=x.device)
159
+ modes_x = min(self.modes_x, max(1, x_ft.shape[-2] // 2))
160
+ modes_y = min(self.modes_y, x_ft.shape[-1])
161
+ weights = self.koopman_matrix[:, :, :modes_x, :modes_y]
162
+ out_ft[:, :, :modes_x, :modes_y] = self.time_marching(
163
+ x_ft[:, :, :modes_x, :modes_y],
164
+ weights,
165
+ )
166
+ out_ft[:, :, -modes_x:, :modes_y] = self.time_marching(
167
+ x_ft[:, :, -modes_x:, :modes_y],
168
+ weights,
169
+ )
170
+ return torch.fft.irfft2(out_ft, s=(x.size(-2), x.size(-1)))
171
+
172
+
173
+ class KNO2D(nn.Module):
174
+ def __init__(
175
+ self,
176
+ encoder: nn.Module,
177
+ decoder: nn.Module,
178
+ op_size: int,
179
+ modes_x: int = 12,
180
+ modes_y: int = 12,
181
+ decompose: int = 6,
182
+ linear_type: bool = True,
183
+ normalization: bool = False,
184
+ ):
185
+ super().__init__()
186
+ self.op_size = int(op_size)
187
+ self.decompose = int(decompose)
188
+ if self.decompose < 1:
189
+ raise ValueError("decompose must be positive")
190
+ self.enc = encoder
191
+ self.dec = decoder
192
+ self.koopman_layer = KoopmanOperator2D(self.op_size, modes_x=modes_x, modes_y=modes_y)
193
+ self.w0 = nn.Conv2d(self.op_size, self.op_size, 1)
194
+ self.linear_type = bool(linear_type)
195
+ self.normalization = bool(normalization)
196
+ if self.normalization:
197
+ self.norm_layer = nn.BatchNorm2d(self.op_size)
198
+
199
+ def forward(self, x):
200
+ x_reconstruct = self.dec(torch.tanh(self.enc(x)))
201
+ x = torch.tanh(self.enc(x)).permute(0, 3, 1, 2)
202
+ x_w = x
203
+ for _ in range(self.decompose):
204
+ x1 = self.koopman_layer(x)
205
+ x = x + x1 if self.linear_type else torch.tanh(x + x1)
206
+ shortcut = self.w0(x_w)
207
+ if self.normalization:
208
+ shortcut = self.norm_layer(shortcut)
209
+ x = torch.tanh(shortcut + x).permute(0, 2, 3, 1)
210
+ return self.dec(x), x_reconstruct
211
+
212
+
213
+ class KNO2DNavierStokes(nn.Module):
214
+ """KNO2D adapter for flattened Navier-Stokes batches.
215
+
216
+ ``pos`` is accepted for API parity with FNO but is not used by the Koopman
217
+ operator. ``fx`` is shaped ``(B, H*W, t_in*out_dim)`` and the output is
218
+ shaped ``(B, H*W, out_dim)`` for one-step autoregressive rollout.
219
+ """
220
+
221
+ def __init__(
222
+ self,
223
+ input_channels: int,
224
+ output_channels: int,
225
+ spatial_shape,
226
+ op_size: int = 32,
227
+ modes_x: int = 12,
228
+ modes_y: int = 12,
229
+ decompose: int = 6,
230
+ linear_type: bool = True,
231
+ normalization: bool = False,
232
+ ):
233
+ super().__init__()
234
+ if int(output_channels) != 1:
235
+ raise ValueError("KNO2DNavierStokes currently expects output_channels=1.")
236
+ self.input_channels = int(input_channels)
237
+ self.output_channels = int(output_channels)
238
+ self.spatial_shape = tuple(int(v) for v in spatial_shape)
239
+ if len(self.spatial_shape) != 2 or min(self.spatial_shape) < 1:
240
+ raise ValueError("spatial_shape must contain two positive dimensions")
241
+ if self.input_channels < 1:
242
+ raise ValueError("input_channels must be positive")
243
+ encoder = EncoderConv2D(self.input_channels, int(op_size))
244
+ decoder = DecoderConv2D(self.output_channels, int(op_size))
245
+ self.kno = KNO2D(
246
+ encoder=encoder,
247
+ decoder=decoder,
248
+ op_size=int(op_size),
249
+ modes_x=int(modes_x),
250
+ modes_y=int(modes_y),
251
+ decompose=int(decompose),
252
+ linear_type=bool(linear_type),
253
+ normalization=bool(normalization),
254
+ )
255
+
256
+ def forward(self, pos, fx):
257
+ batch_size, num_points, channels = fx.shape
258
+ height, width = self.spatial_shape
259
+ if pos.shape[:2] != (batch_size, num_points) or pos.shape[-1] != 2:
260
+ raise ValueError(
261
+ f"Expected pos [B, {num_points}, 2], got {tuple(pos.shape)}"
262
+ )
263
+ if num_points != height * width:
264
+ raise ValueError(f"Expected {height * width} points, got {num_points}.")
265
+ if channels != self.input_channels:
266
+ raise ValueError(f"Expected {self.input_channels} channels, got {channels}.")
267
+ x = fx.reshape(batch_size, height, width, channels)
268
+ pred, _ = self.kno(x)
269
+ return pred.reshape(batch_size, num_points, self.output_channels)
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,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import matplotlib
8
+
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ import torch
12
+ from matplotlib.ticker import FormatStrFormatter
13
+
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(PROJECT_ROOT))
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ LOCAL_WORKSPACE = PROJECT_ROOT.parents[1]
19
+ if (LOCAL_WORKSPACE / "onescience" / "src" / "onescience" / "datapipes").is_dir():
20
+ sys.path.insert(0, str(LOCAL_WORKSPACE))
21
+
22
+ from common import ( # noqa: E402
23
+ load_config,
24
+ project_path,
25
+ relative_l2,
26
+ resolve_device,
27
+ rollout,
28
+ to_attr_dict,
29
+ )
30
+ from model.kno import KNO2DNavierStokes # noqa: E402
31
+ from onescience.datapipes.cfd import NavierStokesDatapipe # noqa: E402
32
+ from onescience.distributed.manager import DistributedManager # noqa: E402
33
+
34
+
35
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
36
+
37
+
38
+ def prepare_config(config: dict[str, Any]) -> None:
39
+ datapipe = config["datapipe"]
40
+ datapipe["source"]["data_dir"] = str(
41
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
42
+ )
43
+
44
+
45
+ def validate_checkpoint_config(
46
+ datapipe_config: dict[str, Any],
47
+ checkpoint: dict[str, Any],
48
+ ) -> None:
49
+ checkpoint_datapipe = checkpoint.get(
50
+ "datapipe_config", checkpoint.get("data_config")
51
+ )
52
+ if checkpoint_datapipe is None:
53
+ raise KeyError("Checkpoint contains no datapipe configuration")
54
+ checkpoint_data = checkpoint_datapipe["data"]
55
+ current_data = datapipe_config["data"]
56
+ for key in ("t_in", "t_out", "out_dim", "downsamplex", "downsampley", "normalize"):
57
+ if current_data[key] != checkpoint_data[key]:
58
+ raise ValueError(
59
+ f"Config value datapipe.data.{key}={current_data[key]} does not match "
60
+ f"checkpoint value {checkpoint_data[key]}"
61
+ )
62
+
63
+
64
+ def build_model(
65
+ model_config: dict[str, Any],
66
+ spatial_shape: tuple[int, int],
67
+ datapipe_config: dict[str, Any],
68
+ ) -> KNO2DNavierStokes:
69
+ data_config = datapipe_config["data"]
70
+ return KNO2DNavierStokes(
71
+ input_channels=int(
72
+ model_config.get(
73
+ "input_channels",
74
+ int(data_config["t_in"]) * int(data_config["out_dim"]),
75
+ )
76
+ ),
77
+ output_channels=int(
78
+ model_config.get("output_channels", data_config["out_dim"])
79
+ ),
80
+ spatial_shape=spatial_shape,
81
+ op_size=int(model_config["op_size"]),
82
+ modes_x=int(model_config["modes_x"]),
83
+ modes_y=int(model_config["modes_y"]),
84
+ decompose=int(model_config["decompose"]),
85
+ linear_type=bool(model_config["linear_type"]),
86
+ normalization=bool(model_config["normalization"]),
87
+ )
88
+
89
+
90
+ def save_visualization(
91
+ prediction: torch.Tensor,
92
+ target: torch.Tensor,
93
+ spatial_shape: tuple[int, int],
94
+ output_path: Path,
95
+ ) -> None:
96
+ prediction = prediction.detach().cpu()
97
+ target = target.detach().cpu()
98
+ height, width = spatial_shape
99
+ frame_count = prediction.shape[-1]
100
+ frames = sorted({0, frame_count // 2, frame_count - 1})
101
+ sample_prediction = prediction[0].reshape(height, width, frame_count)
102
+ sample_target = target[0].reshape(height, width, frame_count)
103
+ rows = (
104
+ ("Target", sample_target),
105
+ ("Prediction", sample_prediction),
106
+ ("Abs Error", (sample_prediction - sample_target).abs()),
107
+ )
108
+ figure, axes = plt.subplots(3, len(frames), figsize=(4 * len(frames), 9), squeeze=False)
109
+ for row_index, (title, values) in enumerate(rows):
110
+ for column_index, frame in enumerate(frames):
111
+ axis = axes[row_index][column_index]
112
+ image = axis.imshow(values[..., frame], origin="lower", cmap="viridis")
113
+ axis.set_title(f"{title} t+{frame + 1}")
114
+ axis.set_xticks([])
115
+ axis.set_yticks([])
116
+ colorbar = figure.colorbar(image, ax=axis, fraction=0.046, pad=0.04)
117
+ colorbar.formatter = FormatStrFormatter("%.3g")
118
+ colorbar.update_ticks()
119
+ figure.tight_layout()
120
+ figure.savefig(output_path, dpi=150)
121
+ plt.close(figure)
122
+
123
+
124
+ def main() -> None:
125
+ config_path = DEFAULT_CONFIG.resolve()
126
+ config = load_config(config_path)
127
+ prepare_config(config)
128
+ training_config = config["training"]
129
+ inference_config = config["inference"]
130
+ datapipe_config = config["datapipe"]
131
+ num_samples = int(inference_config["num_samples"])
132
+ if num_samples < 1:
133
+ raise ValueError("num_samples must be positive")
134
+ checkpoint_path = (
135
+ project_path(training_config["weight_dir"], PROJECT_ROOT).resolve()
136
+ / str(training_config["checkpoint_name"])
137
+ )
138
+ if not checkpoint_path.exists():
139
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
140
+
141
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
142
+ model_config = dict(checkpoint["model_config"])
143
+ validate_checkpoint_config(datapipe_config, checkpoint)
144
+ data_file = (
145
+ Path(datapipe_config["source"]["data_dir"])
146
+ / datapipe_config["source"]["file_name"]
147
+ )
148
+ device = resolve_device(str(config["common"]["device"]))
149
+ result_dir = project_path(
150
+ inference_config["result_dir"], PROJECT_ROOT
151
+ ).resolve()
152
+
153
+ print(f"Checkpoint: {checkpoint_path}")
154
+ print(f"Data: {data_file}")
155
+ print(f"Device: {device}")
156
+ DistributedManager.initialize()
157
+ try:
158
+ datapipe = NavierStokesDatapipe(
159
+ to_attr_dict(datapipe_config),
160
+ distributed=False,
161
+ normalizer_state=checkpoint.get("normalizer"),
162
+ )
163
+ test_loader, _ = datapipe.test_dataloader()
164
+ spatial_shape = tuple(checkpoint["spatial_shape"])
165
+ if tuple(datapipe.spatial_shape) != spatial_shape:
166
+ raise ValueError(
167
+ f"Checkpoint grid {spatial_shape} does not match data grid {datapipe.spatial_shape}"
168
+ )
169
+ model = build_model(model_config, spatial_shape, datapipe_config).to(device)
170
+ model.load_state_dict(checkpoint["model_state"])
171
+ model.eval()
172
+
173
+ batch = next(iter(test_loader))
174
+ pos = batch["pos"].to(device)
175
+ state = batch["x"].to(device)
176
+ target = batch["y"].to(device)
177
+ with torch.no_grad():
178
+ prediction = rollout(
179
+ model,
180
+ pos,
181
+ state,
182
+ int(datapipe_config["data"]["t_out"]),
183
+ int(datapipe_config["data"]["out_dim"]),
184
+ )
185
+ prediction = datapipe.decode_solution(prediction)
186
+ target = datapipe.decode_solution(target)
187
+ error = relative_l2(prediction, target).mean().item()
188
+ sample_count = min(num_samples, prediction.shape[0])
189
+ result_dir.mkdir(parents=True, exist_ok=True)
190
+ tensor_path = result_dir / "prediction_sample.pt"
191
+ image_path = result_dir / "prediction_sample.png"
192
+ torch.save(
193
+ {
194
+ "prediction": prediction[:sample_count].cpu(),
195
+ "target": target[:sample_count].cpu(),
196
+ "spatial_shape": spatial_shape,
197
+ "relative_l2": error,
198
+ },
199
+ tensor_path,
200
+ )
201
+ save_visualization(prediction[:1], target[:1], spatial_shape, image_path)
202
+ print(f"Relative L2: {error:.6e}")
203
+ print(f"Saved tensor: {tensor_path}")
204
+ print(f"Saved figure: {image_path}")
205
+ finally:
206
+ DistributedManager.cleanup()
207
+
208
+
209
+ if __name__ == "__main__":
210
+ main()
scripts/train.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.kno import KNO2DNavierStokes # 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["input_channels"] = int(data["t_in"]) * int(data["out_dim"])
46
+ model["output_channels"] = 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
+ "op_size": model["op_size"],
63
+ "modes_x": model["modes_x"],
64
+ "modes_y": model["modes_y"],
65
+ "decompose": model["decompose"],
66
+ "epochs": training["epochs"],
67
+ "eval_interval": training["eval_interval"],
68
+ }
69
+ for name, value in positive_values.items():
70
+ if int(value) < 1:
71
+ raise ValueError(f"{name} must be positive, got {value}")
72
+ if int(loader["num_workers"]) < 0:
73
+ raise ValueError("num_workers cannot be negative")
74
+ for name in ("max_train_batches", "max_eval_batches"):
75
+ value = training.get(name)
76
+ if value is not None and int(value) < 1:
77
+ raise ValueError(f"{name} must be positive when set")
78
+
79
+
80
+ def build_model(
81
+ model_config: dict[str, Any], spatial_shape: tuple[int, int]
82
+ ) -> KNO2DNavierStokes:
83
+ return KNO2DNavierStokes(
84
+ input_channels=int(model_config["input_channels"]),
85
+ output_channels=int(model_config["output_channels"]),
86
+ spatial_shape=spatial_shape,
87
+ op_size=int(model_config["op_size"]),
88
+ modes_x=int(model_config["modes_x"]),
89
+ modes_y=int(model_config["modes_y"]),
90
+ decompose=int(model_config["decompose"]),
91
+ linear_type=bool(model_config["linear_type"]),
92
+ normalization=bool(model_config["normalization"]),
93
+ )
94
+
95
+
96
+ def evaluate(
97
+ model: torch.nn.Module,
98
+ loader: torch.utils.data.DataLoader,
99
+ device: torch.device,
100
+ t_out: int,
101
+ out_dim: int,
102
+ max_batches: int | None,
103
+ ) -> tuple[float, float]:
104
+ model.eval()
105
+ squared_error = 0.0
106
+ element_count = 0
107
+ relative_error = 0.0
108
+ sample_count = 0
109
+ with torch.no_grad():
110
+ for batch_index, batch in enumerate(tqdm(loader, desc="Evaluating")):
111
+ if max_batches is not None and batch_index >= max_batches:
112
+ break
113
+ pos = batch["pos"].to(device)
114
+ state = batch["x"].to(device)
115
+ target = batch["y"].to(device)
116
+ prediction = rollout(model, pos, state, t_out, out_dim)
117
+ squared_error += F.mse_loss(prediction, target, reduction="sum").item()
118
+ element_count += target.numel()
119
+ relative_error += relative_l2(prediction, target).sum().item()
120
+ sample_count += target.shape[0]
121
+ return (
122
+ squared_error / max(element_count, 1),
123
+ relative_error / max(sample_count, 1),
124
+ )
125
+
126
+
127
+ def main() -> None:
128
+ config_path = DEFAULT_CONFIG.resolve()
129
+ config = load_config(config_path)
130
+ prepare_config(config)
131
+ validate_config(config)
132
+
133
+ common = config["common"]
134
+ data_config = config["datapipe"]
135
+ model_config = config["model"]
136
+ training = config["training"]
137
+ device = resolve_device(str(common["device"]))
138
+ seed_everything(int(common["seed"]))
139
+ weight_dir = project_path(training["weight_dir"], PROJECT_ROOT).resolve()
140
+ checkpoint_path = weight_dir / str(training["checkpoint_name"])
141
+
142
+ print(f"Config: {config_path}")
143
+ print(f"Data: {Path(data_config['source']['data_dir']) / data_config['source']['file_name']}")
144
+ print(f"Device: {device}")
145
+ print(
146
+ f"Samples: train={data_config['data']['ntrain']} test={data_config['data']['ntest']} "
147
+ f"t_in={data_config['data']['t_in']} t_out={data_config['data']['t_out']}"
148
+ )
149
+
150
+ DistributedManager.initialize()
151
+ started = time.time()
152
+ try:
153
+ datapipe = NavierStokesDatapipe(to_attr_dict(data_config), distributed=False)
154
+ train_loader, _ = datapipe.train_dataloader()
155
+ test_loader, _ = datapipe.test_dataloader()
156
+ model = build_model(model_config, datapipe.spatial_shape).to(device)
157
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
158
+ print(f"Spatial shape: {datapipe.spatial_shape}")
159
+ print(f"Parameters: {parameter_count:,}")
160
+
161
+ optimizer = torch.optim.AdamW(
162
+ model.parameters(),
163
+ lr=float(training["lr"]),
164
+ weight_decay=float(training["weight_decay"]),
165
+ )
166
+ scheduler = torch.optim.lr_scheduler.StepLR(
167
+ optimizer,
168
+ step_size=int(training["step_size"]),
169
+ gamma=float(training["gamma"]),
170
+ )
171
+ t_out = int(data_config["data"]["t_out"])
172
+ out_dim = int(data_config["data"]["out_dim"])
173
+ max_train_batches = training.get("max_train_batches")
174
+ max_train_batches = None if max_train_batches is None else int(max_train_batches)
175
+ max_eval_batches = training.get("max_eval_batches")
176
+ max_eval_batches = None if max_eval_batches is None else int(max_eval_batches)
177
+ best_relative_l2 = float("inf")
178
+ stale_evaluations = 0
179
+
180
+ for epoch in range(1, int(training["epochs"]) + 1):
181
+ model.train()
182
+ epoch_loss = 0.0
183
+ batch_count = 0
184
+ iterator = tqdm(train_loader, desc=f"Epoch {epoch}")
185
+ for batch_index, batch in enumerate(iterator):
186
+ if max_train_batches is not None and batch_index >= max_train_batches:
187
+ break
188
+ pos = batch["pos"].to(device)
189
+ state = batch["x"].to(device)
190
+ target = batch["y"].to(device)
191
+ optimizer.zero_grad(set_to_none=True)
192
+ prediction = rollout(
193
+ model,
194
+ pos,
195
+ state,
196
+ t_out,
197
+ out_dim,
198
+ teacher_forcing=bool(training["teacher_forcing"]),
199
+ target=target,
200
+ )
201
+ loss = F.mse_loss(prediction, target)
202
+ if not torch.isfinite(loss):
203
+ raise FloatingPointError(f"Non-finite training loss at epoch {epoch}")
204
+ loss.backward()
205
+ if training.get("max_grad_norm") is not None:
206
+ torch.nn.utils.clip_grad_norm_(
207
+ model.parameters(), float(training["max_grad_norm"])
208
+ )
209
+ optimizer.step()
210
+ epoch_loss += loss.item()
211
+ batch_count += 1
212
+ iterator.set_postfix(loss=f"{loss.item():.3e}")
213
+ scheduler.step()
214
+ if batch_count == 0:
215
+ raise RuntimeError("No training batches were processed")
216
+
217
+ should_evaluate = (
218
+ epoch % int(training["eval_interval"]) == 0
219
+ or epoch == int(training["epochs"])
220
+ )
221
+ if not should_evaluate:
222
+ continue
223
+ validation_mse, validation_relative_l2 = evaluate(
224
+ model,
225
+ test_loader,
226
+ device,
227
+ t_out,
228
+ out_dim,
229
+ max_eval_batches,
230
+ )
231
+ print(
232
+ f"epoch={epoch:4d} train_mse={epoch_loss / batch_count:.6e} "
233
+ f"val_mse={validation_mse:.6e} val_relative_l2={validation_relative_l2:.6e}"
234
+ )
235
+ if validation_relative_l2 < best_relative_l2:
236
+ best_relative_l2 = validation_relative_l2
237
+ stale_evaluations = 0
238
+ weight_dir.mkdir(parents=True, exist_ok=True)
239
+ torch.save(
240
+ {
241
+ "epoch": epoch,
242
+ "model_state": model.state_dict(),
243
+ "model_config": to_plain_dict(model_config),
244
+ "datapipe_config": to_plain_dict(data_config),
245
+ "training_config": to_plain_dict(training),
246
+ "spatial_shape": tuple(datapipe.spatial_shape),
247
+ "normalizer": datapipe.get_normalizer_state(),
248
+ "best_relative_l2": best_relative_l2,
249
+ },
250
+ checkpoint_path,
251
+ )
252
+ print(f"Saved checkpoint: {checkpoint_path}")
253
+ else:
254
+ stale_evaluations += 1
255
+ if stale_evaluations >= int(training["patience"]):
256
+ print("Early stopping triggered")
257
+ break
258
+
259
+ print(f"Elapsed: {time.time() - started:.1f}s")
260
+ finally:
261
+ DistributedManager.cleanup()
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()
weight/kno_navier_stokes.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c9c6f9a16f0196a5da6aec3d7d30f2ef1209782d29ebe8789c89075ab6b0a735
3
+ size 1189285