OneScience commited on
Commit
8b16bae
·
verified ·
1 Parent(s): 5150871

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -1,35 +1,65 @@
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
  *.pt filter=lfs diff=lfs merge=lfs -text
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
+ *.wav filter=lfs diff=lfs merge=lfs -text
49
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
50
+ *.flac filter=lfs diff=lfs merge=lfs -text
51
+ *.ogg filter=lfs diff=lfs merge=lfs -text
52
+ *.m4a filter=lfs diff=lfs merge=lfs -text
53
+ *.aac filter=lfs diff=lfs merge=lfs -text
54
+ *.opus filter=lfs diff=lfs merge=lfs -text
55
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
56
+ *.mov filter=lfs diff=lfs merge=lfs -text
57
+ *.avi filter=lfs diff=lfs merge=lfs -text
58
+ *.mkv filter=lfs diff=lfs merge=lfs -text
59
+ *.webm filter=lfs diff=lfs merge=lfs -text
60
+ *.obj filter=lfs diff=lfs merge=lfs -text
61
+ *.glb filter=lfs diff=lfs merge=lfs -text
62
+ *.gltf filter=lfs diff=lfs merge=lfs -text
63
+ *.ply filter=lfs diff=lfs merge=lfs -text
64
+ *.stl filter=lfs diff=lfs merge=lfs -text
65
+ *.fbx filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ frameworks:
3
+ - pytorch
4
+ language:
5
+ - en
6
+ license: apache-2.0
7
+ tags:
8
+ - OneScience
9
+ - FactFormer
10
+ - neural-operator
11
+ - factorized-attention
12
+ - computational-fluid-dynamics
13
+ - Kolmogorov-flow
14
+ tasks:
15
+ - time-series-prediction
16
+ ---
17
+ <p align="center">
18
+ <strong>
19
+ <span style="font-size: 30px;">FactFormer</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Overview
24
+
25
+ FactFormer (Factorized Transformer) is a Transformer architecture for PDE surrogate modeling. This model predicts the temporal evolution of the vorticity field in two-dimensional Kolmogorov flow, using the first 10 time steps to predict the subsequent 16 by default.
26
+
27
+ Paper: [Scalable Transformer for PDE Surrogate Modeling](https://arxiv.org/abs/2305.17560)
28
+
29
+ # Model Description
30
+
31
+ FactFormer factorizes global attention on a two-dimensional regular grid into attention operations along the two spatial axes. This preserves long-range spatial interactions while reducing computational and memory costs. The model predicts future states in blocks through latent propagation and rolls out the complete forecast window autoregressively.
32
+
33
+ # Use Cases
34
+
35
+ | Use Case | Description |
36
+ | :---: | :--- |
37
+ | Vorticity time-series prediction | Predict future Kolmogorov-flow states from historical two-dimensional vorticity fields |
38
+ | PDE surrogate modeling | Learn mappings from historical to future physical fields on regular grids |
39
+ | Factorized-attention research | Evaluate axial attention for high-resolution physical-field modeling |
40
+ | Pipeline validation | Validate training and inference using the bundled weights or a small-scale configuration |
41
+
42
+ # Usage
43
+
44
+ ## 1. OneCode
45
+
46
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
47
+
48
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
49
+
50
+ ## 2. Manual Setup
51
+
52
+ **Hardware Requirements**
53
+
54
+ - A GPU or DCU is recommended for training and inference.
55
+ - A CPU can be used for small-scale pipeline validation, although training with the default configuration will be slow.
56
+ - DCU users must install DTK and a PyTorch environment compatible with the target cluster.
57
+
58
+ ### Download the Model Package
59
+
60
+ ```bash
61
+ modelscope download --model OneScience/FactFormer --local_dir ./FactFormer
62
+ cd FactFormer
63
+ ```
64
+
65
+ ### Set Up the Runtime Environment
66
+
67
+ **DCU Environment**
68
+
69
+ ```bash
70
+ # Activate DTK and Conda first
71
+ conda create -n onescience311 python=3.11 -y
72
+ conda activate onescience311
73
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
74
+ ```
75
+
76
+ **GPU Environment**
77
+
78
+ ```bash
79
+ # Activate Conda first
80
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
81
+ conda activate onescience311
82
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
83
+ ```
84
+
85
+ ### Training Data
86
+
87
+ The model uses two-dimensional Kolmogorov-flow data with an original array shape of `[120, 320, 256, 256]`, representing trajectories, time steps, and the two spatial dimensions, respectively. Because the data file is large, it is read on demand using memory mapping.
88
+
89
+ Download the data with the following command and verify that the data path in `conf/config.yaml` is configured correctly:
90
+
91
+ ```bash
92
+ modelscope download --dataset OneScience/Kolmogorov_flow_2d --local_dir ./data
93
+ ```
94
+
95
+ ### Training
96
+
97
+ ```bash
98
+ python scripts/train.py
99
+ ```
100
+
101
+ The default weights are saved to `weight/factformer_kolmogorov.pt`.
102
+
103
+ ### Model Weights
104
+
105
+ This repository provides weights trained on the two-dimensional Kolmogorov-flow dataset in the `weight/` directory.
106
+
107
+ ### Inference, Evaluation, and Visualization
108
+
109
+ The model package includes weights for pipeline validation. After preparing the data, run:
110
+
111
+ ```bash
112
+ python scripts/inference.py
113
+ ```
114
+
115
+ The script loads `weight/factformer_kolmogorov.pt` by default. Predicted tensors and visualizations are saved to the `result/` directory. Training and inference parameters can be modified in `conf/config.yaml`.
116
+
117
+ # Official OneScience Resources
118
+
119
+ | Platform | OneScience Repository | Skills Repository |
120
+ | --- | --- | --- |
121
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
122
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
123
+
124
+ # Citations and License
125
+
126
+ - Li, Shu, and Barati Farimani. [Scalable Transformer for PDE Surrogate Modeling](https://arxiv.org/abs/2305.17560).
127
+ - The Kolmogorov-flow data is used for two-dimensional vorticity prediction at Re=1000.
128
+ - This model package is released under the Apache-2.0 license and retains attribution to the original paper and data sources.
conf/config.yaml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root:
2
+ common:
3
+ device: "auto"
4
+ seed: 1234
5
+
6
+ datapipe:
7
+ source:
8
+ data_dir: "/public/share/sugonhpcapp01/onestore/onedatasets/Kolmogorov_flow_2d"
9
+ file_name: "kf_2d_re1000_256_120seed.npy"
10
+ data:
11
+ train_num: 100
12
+ test_num: 20
13
+ resolution: 128
14
+ interval: 2
15
+ t_in: 10
16
+ t_out: 16
17
+ out_dim: 1
18
+ normalize: true
19
+ val_mode: false
20
+ split_seed: 1234
21
+ stats_file: "data/km2d_stat.npz"
22
+ stats_max_trajectories: 8
23
+ dataloader:
24
+ batch_size: 4
25
+ num_workers: 2
26
+ pin_memory: true
27
+
28
+ model:
29
+ in_dim: 10
30
+ out_dim: 1
31
+ hidden_dim: 128
32
+ depth: 4
33
+ heads: 8
34
+ mlp_ratio: 2
35
+ dropout: 0.0
36
+ activation: "gelu"
37
+ include_pos: true
38
+ space_dim: 2
39
+ latent_multiplier: 2.0
40
+ max_latent_steps: 4
41
+
42
+ training:
43
+ epochs: 50
44
+ lr: 0.0005
45
+ weight_decay: 0.0001
46
+ step_size: 20
47
+ gamma: 0.5
48
+ eval_interval: 1
49
+ patience: 20
50
+ train_latent_steps: 4
51
+ max_grad_norm: 1.0
52
+ max_train_batches: null
53
+ max_eval_batches: null
54
+ weight_dir: "weight"
55
+ checkpoint_name: "factformer_kolmogorov.pt"
56
+
57
+ inference:
58
+ result_dir: "result"
59
+ num_samples: 4
60
+ figure_name: "prediction_sample.png"
61
+ tensor_name: "prediction_sample.pt"
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
data/km2d_stat.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:67ab0c2eca4d3abfdb55b947488d4834c587d8682590a4b507534c4b94cd491a
3
+ size 508
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/FactFormer weight/factformer_kolmogorov.pt --local_dir ./
model/__pycache__/factformer.cpython-311.pyc ADDED
Binary file (22.4 kB). View file
 
model/factformer.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+
9
+ def _activation(name: str) -> nn.Module:
10
+ activations = {
11
+ "gelu": nn.GELU,
12
+ "relu": nn.ReLU,
13
+ "silu": nn.SiLU,
14
+ "swish": 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 StandardMLP(nn.Module):
24
+ """Pointwise MLP with state-dict names compatible with OneScience."""
25
+
26
+ def __init__(
27
+ self,
28
+ input_dim: int,
29
+ output_dim: int,
30
+ hidden_dims: Sequence[int],
31
+ activation: str = "gelu",
32
+ use_bias: bool = True,
33
+ ) -> None:
34
+ super().__init__()
35
+ dimensions = [int(input_dim), *(int(value) for value in hidden_dims), int(output_dim)]
36
+ self.layers = nn.ModuleList(
37
+ nn.Linear(in_features, out_features, bias=use_bias)
38
+ for in_features, out_features in zip(dimensions[:-1], dimensions[1:])
39
+ )
40
+ self.activation = _activation(activation)
41
+
42
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
43
+ for layer in self.layers[:-1]:
44
+ x = self.activation(layer(x))
45
+ return self.layers[-1](x)
46
+
47
+
48
+ class OneMlp(nn.Module):
49
+ """Small compatibility wrapper used by released OneScience checkpoints."""
50
+
51
+ def __init__(self, **kwargs) -> None:
52
+ super().__init__()
53
+ self.mlp = StandardMLP(**kwargs)
54
+
55
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
56
+ return self.mlp(x)
57
+
58
+
59
+ class _PoolingReducer(nn.Module):
60
+ def __init__(self, in_dim: int, hidden_dim: int, out_dim: int) -> None:
61
+ super().__init__()
62
+ self.to_in = nn.Linear(in_dim, hidden_dim, bias=False)
63
+ self.out_ffn = nn.Sequential(
64
+ nn.LayerNorm(hidden_dim),
65
+ nn.Linear(hidden_dim, hidden_dim),
66
+ nn.GELU(),
67
+ nn.Linear(hidden_dim, out_dim),
68
+ )
69
+
70
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
71
+ x = self.to_in(x)
72
+ if x.ndim > 3:
73
+ x = x.mean(dim=tuple(range(2, x.ndim - 1)))
74
+ return self.out_ffn(x)
75
+
76
+
77
+ class _SwapGridAxes(nn.Module):
78
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
79
+ return x.permute(0, 2, 1, 3)
80
+
81
+
82
+ class _FactAttnWeight(nn.Module):
83
+ def __init__(self, heads: int, dim_head: int, dropout: float) -> None:
84
+ super().__init__()
85
+ self.dim_head = int(dim_head)
86
+ self.heads = int(heads)
87
+ self.scale = self.dim_head**-0.5
88
+ self.softmax = nn.Softmax(dim=-1)
89
+ self.dropout = nn.Dropout(dropout)
90
+ self.to_q = nn.Linear(self.dim_head, self.dim_head, bias=False)
91
+ self.to_k = nn.Linear(self.dim_head, self.dim_head, bias=False)
92
+
93
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
94
+ batch, length, _ = x.shape
95
+ x = x.reshape(batch, length, self.heads, self.dim_head)
96
+ x = x.permute(0, 2, 1, 3).contiguous()
97
+ query = self.to_q(x)
98
+ key = self.to_k(x)
99
+ return self.softmax(torch.matmul(query, key.transpose(-1, -2)) * self.scale)
100
+
101
+
102
+ class FactAttention2D(nn.Module):
103
+ """Factorized axial attention for a structured two-dimensional grid."""
104
+
105
+ def __init__(
106
+ self,
107
+ dim: int,
108
+ heads: int,
109
+ dim_head: int,
110
+ dropout: float,
111
+ shapelist: Sequence[int],
112
+ ) -> None:
113
+ super().__init__()
114
+ if len(shapelist) != 2:
115
+ raise ValueError("FactAttention2D requires shapelist=(height, width)")
116
+ if dim != heads * dim_head:
117
+ raise ValueError("dim must equal heads * dim_head")
118
+ self.dim_head = int(dim_head)
119
+ self.heads = int(heads)
120
+ self.H, self.W = (int(value) for value in shapelist)
121
+ self.attn_x = _FactAttnWeight(self.heads, self.dim_head, dropout)
122
+ self.attn_y = _FactAttnWeight(self.heads, self.dim_head, dropout)
123
+ self.to_v = nn.Linear(self.dim_head, self.dim_head, bias=False)
124
+ self.to_x = nn.Sequential(_PoolingReducer(dim, dim, dim))
125
+ self.to_y = nn.Sequential(_SwapGridAxes(), _PoolingReducer(dim, dim, dim))
126
+ self.to_out = nn.Sequential(nn.Linear(dim, dim), nn.Dropout(dropout))
127
+
128
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
129
+ batch, length, channels = x.shape
130
+ if length != self.H * self.W:
131
+ raise ValueError(
132
+ f"Sequence length {length} does not match grid {self.H}x{self.W}"
133
+ )
134
+ grid = x.reshape(batch, self.H, self.W, channels).contiguous()
135
+ values = self.to_v(
136
+ grid.reshape(batch, self.H, self.W, self.heads, self.dim_head)
137
+ )
138
+ values = values.permute(0, 3, 1, 2, 4).contiguous()
139
+ result_x = torch.einsum(
140
+ "bhij,bhjmc->bhimc", self.attn_x(self.to_x(grid)), values
141
+ )
142
+ result_y = torch.einsum(
143
+ "bhlm,bhimc->bhilc", self.attn_y(self.to_y(grid)), result_x
144
+ )
145
+ result = result_y.permute(0, 2, 3, 1, 4).contiguous()
146
+ result = result.reshape(batch, length, channels)
147
+ return self.to_out(result)
148
+
149
+
150
+ class OneAttention(nn.Module):
151
+ """Compatibility wrapper retaining the original checkpoint hierarchy."""
152
+
153
+ def __init__(self, **kwargs) -> None:
154
+ super().__init__()
155
+ self.attentioner = FactAttention2D(**kwargs)
156
+
157
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
158
+ return self.attentioner(x)
159
+
160
+
161
+ class FactformerBlock(nn.Module):
162
+ def __init__(
163
+ self,
164
+ num_heads: int,
165
+ hidden_dim: int,
166
+ dropout: float,
167
+ activation: str,
168
+ mlp_ratio: int,
169
+ spatial_shape: Sequence[int],
170
+ ) -> None:
171
+ super().__init__()
172
+ self.ln_1 = nn.LayerNorm(hidden_dim)
173
+ self.Attn = OneAttention(
174
+ dim=hidden_dim,
175
+ heads=num_heads,
176
+ dim_head=hidden_dim // num_heads,
177
+ dropout=dropout,
178
+ shapelist=spatial_shape,
179
+ )
180
+ self.ln_2 = nn.LayerNorm(hidden_dim)
181
+ self.mlp = OneMlp(
182
+ input_dim=hidden_dim,
183
+ output_dim=hidden_dim,
184
+ hidden_dims=[hidden_dim * mlp_ratio],
185
+ activation=activation,
186
+ use_bias=True,
187
+ )
188
+
189
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
190
+ x = self.Attn(self.ln_1(x)) + x
191
+ return self.mlp(self.ln_2(x)) + x
192
+
193
+
194
+ class OneTransformer(nn.Module):
195
+ """Compatibility wrapper retaining the original checkpoint hierarchy."""
196
+
197
+ def __init__(self, **kwargs) -> None:
198
+ super().__init__()
199
+ self.transformer = FactformerBlock(**kwargs)
200
+
201
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
202
+ return self.transformer(x)
203
+
204
+
205
+ class FactFormer2D(nn.Module):
206
+ """FactFormer neural operator for structured 2D flow fields.
207
+
208
+ ``pos`` has shape ``(B, H*W, 2)`` and ``fx`` has shape
209
+ ``(B, H*W, t_in*out_dim)``. The output contains ``latent_steps`` future
210
+ fields concatenated along the final dimension.
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ in_dim: int,
216
+ out_dim: int,
217
+ spatial_shape: Sequence[int],
218
+ hidden_dim: int = 128,
219
+ depth: int = 4,
220
+ heads: int = 8,
221
+ mlp_ratio: int = 2,
222
+ dropout: float = 0.0,
223
+ activation: str = "gelu",
224
+ include_pos: bool = True,
225
+ space_dim: int = 2,
226
+ latent_multiplier: float = 2.0,
227
+ max_latent_steps: int = 4,
228
+ ) -> None:
229
+ super().__init__()
230
+ self.in_dim = int(in_dim)
231
+ self.out_dim = int(out_dim)
232
+ self.spatial_shape = tuple(int(value) for value in spatial_shape)
233
+ self.hidden_dim = int(hidden_dim)
234
+ self.depth = int(depth)
235
+ self.heads = int(heads)
236
+ self.include_pos = bool(include_pos)
237
+ self.space_dim = int(space_dim)
238
+ self.latent_dim = int(self.hidden_dim * float(latent_multiplier))
239
+ self.max_latent_steps = int(max_latent_steps)
240
+
241
+ if len(self.spatial_shape) != 2:
242
+ raise ValueError("FactFormer2D requires a two-dimensional spatial shape")
243
+ if self.hidden_dim % self.heads:
244
+ raise ValueError("hidden_dim must be divisible by heads")
245
+ if self.max_latent_steps < 1:
246
+ raise ValueError("max_latent_steps must be positive")
247
+
248
+ input_dim = self.in_dim + (self.space_dim if self.include_pos else 0)
249
+ self.preprocess = OneMlp(
250
+ input_dim=input_dim,
251
+ output_dim=self.hidden_dim,
252
+ hidden_dims=[self.hidden_dim * 2],
253
+ activation=activation,
254
+ use_bias=True,
255
+ )
256
+ self.blocks = nn.ModuleList(
257
+ OneTransformer(
258
+ num_heads=self.heads,
259
+ hidden_dim=self.hidden_dim,
260
+ dropout=float(dropout),
261
+ activation=activation,
262
+ mlp_ratio=int(mlp_ratio),
263
+ spatial_shape=self.spatial_shape,
264
+ )
265
+ for _ in range(self.depth)
266
+ )
267
+ self.expand_latent = nn.Linear(self.hidden_dim, self.latent_dim, bias=False)
268
+ self.latent_time_embedding = nn.Parameter(
269
+ torch.randn(1, self.max_latent_steps, 1, self.latent_dim) * 0.02
270
+ )
271
+ self.propagator = OneMlp(
272
+ input_dim=self.latent_dim,
273
+ output_dim=self.latent_dim,
274
+ hidden_dims=[self.hidden_dim],
275
+ activation=activation,
276
+ use_bias=True,
277
+ )
278
+ self.to_out = OneMlp(
279
+ input_dim=self.latent_dim,
280
+ output_dim=self.out_dim,
281
+ hidden_dims=[self.hidden_dim],
282
+ activation=activation,
283
+ use_bias=True,
284
+ )
285
+ self.initialize_weights()
286
+
287
+ def initialize_weights(self) -> None:
288
+ for module in self.modules():
289
+ if isinstance(module, nn.Linear):
290
+ nn.init.trunc_normal_(module.weight, std=0.02)
291
+ if module.bias is not None:
292
+ nn.init.zeros_(module.bias)
293
+ elif isinstance(module, nn.LayerNorm):
294
+ nn.init.ones_(module.weight)
295
+ nn.init.zeros_(module.bias)
296
+
297
+ def forward(
298
+ self,
299
+ pos: torch.Tensor,
300
+ fx: torch.Tensor,
301
+ latent_steps: int = 1,
302
+ ) -> torch.Tensor:
303
+ if fx.ndim != 3:
304
+ raise ValueError(f"fx must have shape (B, N, C), got {tuple(fx.shape)}")
305
+ if pos.ndim == 2:
306
+ pos = pos.unsqueeze(0)
307
+ if pos.shape[0] == 1 and fx.shape[0] > 1:
308
+ pos = pos.expand(fx.shape[0], -1, -1)
309
+ if pos.shape[:2] != fx.shape[:2]:
310
+ raise ValueError(f"Incompatible pos and fx shapes: {pos.shape}, {fx.shape}")
311
+ if not 1 <= int(latent_steps) <= self.max_latent_steps:
312
+ raise ValueError(
313
+ f"latent_steps must be in [1, {self.max_latent_steps}], got {latent_steps}"
314
+ )
315
+
316
+ hidden = torch.cat((pos, fx), dim=-1) if self.include_pos else fx
317
+ hidden = self.preprocess(hidden)
318
+ for block in self.blocks:
319
+ hidden = block(hidden)
320
+
321
+ latent = self.expand_latent(hidden)
322
+ outputs = []
323
+ for step in range(int(latent_steps)):
324
+ latent = latent + self.latent_time_embedding[:, step]
325
+ latent = self.propagator(latent) + latent
326
+ outputs.append(self.to_out(latent))
327
+ return torch.cat(outputs, dim=-1)
328
+
329
+
330
+ __all__ = ["FactFormer2D"]
scripts/__pycache__/common.cpython-311.pyc ADDED
Binary file (7.36 kB). View file
 
scripts/common.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ max_latent_steps: int,
79
+ ) -> torch.Tensor:
80
+ predictions = []
81
+ remaining = int(t_out)
82
+ while remaining > 0:
83
+ step_count = min(int(max_latent_steps), remaining)
84
+ prediction = model(pos, state, latent_steps=step_count)
85
+ predictions.append(prediction)
86
+ consumed = step_count * int(out_dim)
87
+ state = torch.cat((state[..., consumed:], prediction), dim=-1)
88
+ remaining -= step_count
89
+ return torch.cat(predictions, dim=-1)
90
+
91
+
92
+ def relative_l2(prediction: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
93
+ prediction = prediction.reshape(prediction.shape[0], -1)
94
+ target = target.reshape(target.shape[0], -1)
95
+ numerator = torch.linalg.vector_norm(prediction - target, dim=1)
96
+ denominator = torch.linalg.vector_norm(target, dim=1).clamp_min(1.0e-8)
97
+ return numerator / denominator
scripts/inference.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.factformer import FactFormer2D # noqa: E402
31
+ from onescience.datapipes.cfd import KolmogorovFlow2DDatapipe # 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
+ stats_file = datapipe["data"].get("stats_file")
44
+ if stats_file:
45
+ datapipe["data"]["stats_file"] = str(
46
+ project_path(stats_file, PROJECT_ROOT).resolve()
47
+ )
48
+
49
+
50
+ def checkpoint_datapipe_config(checkpoint: dict[str, Any]) -> dict[str, Any]:
51
+ if "datapipe_config" in checkpoint:
52
+ return checkpoint["datapipe_config"]
53
+ if "data_config" in checkpoint:
54
+ return checkpoint["data_config"]
55
+ raise KeyError("Checkpoint does not contain a datapipe configuration")
56
+
57
+
58
+ def validate_checkpoint_config(
59
+ datapipe_config: dict[str, Any], checkpoint: dict[str, Any]
60
+ ) -> None:
61
+ checkpoint_data = checkpoint_datapipe_config(checkpoint)["data"]
62
+ current_data = datapipe_config["data"]
63
+ for key in (
64
+ "resolution",
65
+ "interval",
66
+ "t_in",
67
+ "t_out",
68
+ "out_dim",
69
+ "normalize",
70
+ ):
71
+ if current_data[key] != checkpoint_data[key]:
72
+ raise ValueError(
73
+ f"Config datapipe.data.{key}={current_data[key]} does not match "
74
+ f"checkpoint value {checkpoint_data[key]}"
75
+ )
76
+
77
+
78
+ def build_model(
79
+ model_config: dict[str, Any], spatial_shape: tuple[int, int]
80
+ ) -> FactFormer2D:
81
+ return FactFormer2D(
82
+ in_dim=int(model_config["in_dim"]),
83
+ out_dim=int(model_config["out_dim"]),
84
+ spatial_shape=spatial_shape,
85
+ hidden_dim=int(model_config["hidden_dim"]),
86
+ depth=int(model_config["depth"]),
87
+ heads=int(model_config["heads"]),
88
+ mlp_ratio=int(model_config["mlp_ratio"]),
89
+ dropout=float(model_config["dropout"]),
90
+ activation=str(model_config["activation"]),
91
+ include_pos=bool(model_config["include_pos"]),
92
+ space_dim=int(model_config["space_dim"]),
93
+ latent_multiplier=float(model_config["latent_multiplier"]),
94
+ max_latent_steps=int(model_config["max_latent_steps"]),
95
+ )
96
+
97
+
98
+ def save_visualization(
99
+ prediction: torch.Tensor,
100
+ target: torch.Tensor,
101
+ spatial_shape: tuple[int, int],
102
+ output_path: Path,
103
+ ) -> None:
104
+ prediction = prediction.detach().cpu()
105
+ target = target.detach().cpu()
106
+ height, width = spatial_shape
107
+ frame_count = prediction.shape[-1]
108
+ frames = sorted({0, frame_count // 2, frame_count - 1})
109
+ sample_prediction = prediction[0].reshape(height, width, frame_count)
110
+ sample_target = target[0].reshape(height, width, frame_count)
111
+ rows = (
112
+ ("Target", sample_target),
113
+ ("Prediction", sample_prediction),
114
+ ("Abs Error", (sample_prediction - sample_target).abs()),
115
+ )
116
+ figure, axes = plt.subplots(
117
+ 3, len(frames), figsize=(4 * len(frames), 9), squeeze=False
118
+ )
119
+ for row_index, (title, values) in enumerate(rows):
120
+ for column_index, frame in enumerate(frames):
121
+ axis = axes[row_index][column_index]
122
+ image = axis.imshow(values[..., frame], origin="lower", cmap="viridis")
123
+ axis.set_title(f"{title} t+{frame + 1}")
124
+ axis.set_xticks([])
125
+ axis.set_yticks([])
126
+ colorbar = figure.colorbar(image, ax=axis, fraction=0.046, pad=0.04)
127
+ colorbar.formatter = FormatStrFormatter("%.3g")
128
+ colorbar.update_ticks()
129
+ figure.tight_layout()
130
+ figure.savefig(output_path, dpi=150)
131
+ plt.close(figure)
132
+
133
+
134
+ def main() -> None:
135
+ config_path = DEFAULT_CONFIG.resolve()
136
+ config = load_config(config_path)
137
+ prepare_config(config)
138
+ training = config["training"]
139
+ inference = config["inference"]
140
+ datapipe_config = config["datapipe"]
141
+ num_samples = int(inference["num_samples"])
142
+ if num_samples < 1:
143
+ raise ValueError("inference.num_samples must be positive")
144
+ checkpoint_path = (
145
+ project_path(training["weight_dir"], PROJECT_ROOT).resolve()
146
+ / str(training["checkpoint_name"])
147
+ )
148
+ if not checkpoint_path.exists():
149
+ raise FileNotFoundError(f"Checkpoint not found: {checkpoint_path}")
150
+
151
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
152
+ validate_checkpoint_config(datapipe_config, checkpoint)
153
+ model_config = dict(checkpoint["model_config"])
154
+ spatial_shape = tuple(int(value) for value in checkpoint["spatial_shape"])
155
+ device = resolve_device(str(config["common"]["device"]))
156
+ result_dir = project_path(inference["result_dir"], PROJECT_ROOT).resolve()
157
+
158
+ print(f"Config: {config_path}")
159
+ print(f"Checkpoint: {checkpoint_path}")
160
+ print(
161
+ "Data: "
162
+ f"{Path(datapipe_config['source']['data_dir']) / datapipe_config['source']['file_name']}"
163
+ )
164
+ print(f"Device: {device}")
165
+
166
+ DistributedManager.initialize()
167
+ try:
168
+ datapipe = KolmogorovFlow2DDatapipe(
169
+ to_attr_dict(datapipe_config),
170
+ distributed=False,
171
+ normalizer_state=checkpoint.get("normalizer"),
172
+ )
173
+ test_loader, _ = datapipe.test_dataloader()
174
+ if tuple(datapipe.spatial_shape) != spatial_shape:
175
+ raise ValueError(
176
+ f"Checkpoint grid {spatial_shape} does not match data grid "
177
+ f"{datapipe.spatial_shape}"
178
+ )
179
+ model = build_model(model_config, spatial_shape).to(device)
180
+ model.load_state_dict(checkpoint["model_state"])
181
+ model.eval()
182
+
183
+ batch = next(iter(test_loader))
184
+ pos = batch["pos"].to(device)
185
+ state = batch["x"].to(device)
186
+ target = batch["y"].to(device)
187
+ with torch.no_grad():
188
+ prediction = rollout(
189
+ model,
190
+ pos,
191
+ state,
192
+ int(datapipe_config["data"]["t_out"]),
193
+ int(datapipe_config["data"]["out_dim"]),
194
+ int(model_config["max_latent_steps"]),
195
+ )
196
+ prediction = datapipe.decode_solution(prediction)
197
+ target = datapipe.decode_solution(target)
198
+ error = relative_l2(prediction, target).mean().item()
199
+ sample_count = min(num_samples, prediction.shape[0])
200
+ result_dir.mkdir(parents=True, exist_ok=True)
201
+ tensor_path = result_dir / str(inference["tensor_name"])
202
+ image_path = result_dir / str(inference["figure_name"])
203
+ torch.save(
204
+ {
205
+ "prediction": prediction[:sample_count].cpu(),
206
+ "target": target[:sample_count].cpu(),
207
+ "spatial_shape": spatial_shape,
208
+ "relative_l2": error,
209
+ },
210
+ tensor_path,
211
+ )
212
+ save_visualization(prediction[:1], target[:1], spatial_shape, image_path)
213
+ print(f"Relative L2: {error:.6e}")
214
+ print(f"Saved tensor: {tensor_path}")
215
+ print(f"Saved figure: {image_path}")
216
+ finally:
217
+ DistributedManager.cleanup()
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()
scripts/train.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 torch.nn.parallel import DistributedDataParallel as DDP
11
+ from tqdm import tqdm
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
+ seed_everything,
28
+ to_attr_dict,
29
+ to_plain_dict,
30
+ )
31
+ from model.factformer import FactFormer2D # noqa: E402
32
+ from onescience.datapipes.cfd import KolmogorovFlow2DDatapipe # noqa: E402
33
+ from onescience.distributed.manager import DistributedManager # noqa: E402
34
+
35
+
36
+ DEFAULT_CONFIG = PROJECT_ROOT / "conf" / "config.yaml"
37
+
38
+
39
+ def prepare_config(config: dict[str, Any]) -> None:
40
+ datapipe = config["datapipe"]
41
+ data = datapipe["data"]
42
+ model = config["model"]
43
+ datapipe["source"]["data_dir"] = str(
44
+ project_path(datapipe["source"]["data_dir"], PROJECT_ROOT).resolve()
45
+ )
46
+ stats_file = data.get("stats_file")
47
+ if stats_file:
48
+ data["stats_file"] = str(project_path(stats_file, PROJECT_ROOT).resolve())
49
+ model["in_dim"] = int(data["t_in"]) * int(data["out_dim"])
50
+ model["out_dim"] = int(data["out_dim"])
51
+
52
+
53
+ def validate_config(config: dict[str, Any]) -> None:
54
+ data = config["datapipe"]["data"]
55
+ loader = config["datapipe"]["dataloader"]
56
+ model = config["model"]
57
+ training = config["training"]
58
+ positive_values = {
59
+ "train_num": data["train_num"],
60
+ "test_num": data["test_num"],
61
+ "resolution": data["resolution"],
62
+ "interval": data["interval"],
63
+ "t_in": data["t_in"],
64
+ "t_out": data["t_out"],
65
+ "batch_size": loader["batch_size"],
66
+ "hidden_dim": model["hidden_dim"],
67
+ "depth": model["depth"],
68
+ "heads": model["heads"],
69
+ "mlp_ratio": model["mlp_ratio"],
70
+ "max_latent_steps": model["max_latent_steps"],
71
+ "epochs": training["epochs"],
72
+ "eval_interval": training["eval_interval"],
73
+ "train_latent_steps": training["train_latent_steps"],
74
+ }
75
+ for name, value in positive_values.items():
76
+ if int(value) < 1:
77
+ raise ValueError(f"{name} must be positive, got {value}")
78
+ if int(loader["num_workers"]) < 0:
79
+ raise ValueError("num_workers cannot be negative")
80
+ if int(model["hidden_dim"]) % int(model["heads"]):
81
+ raise ValueError("hidden_dim must be divisible by heads")
82
+ if int(training["train_latent_steps"]) > int(model["max_latent_steps"]):
83
+ raise ValueError("train_latent_steps cannot exceed model.max_latent_steps")
84
+ if int(training["train_latent_steps"]) > int(data["t_out"]):
85
+ raise ValueError("train_latent_steps cannot exceed datapipe.data.t_out")
86
+ for name in ("max_train_batches", "max_eval_batches"):
87
+ value = training.get(name)
88
+ if value is not None and int(value) < 1:
89
+ raise ValueError(f"{name} must be positive when set")
90
+
91
+
92
+ def build_model(
93
+ model_config: dict[str, Any], spatial_shape: tuple[int, int]
94
+ ) -> FactFormer2D:
95
+ return FactFormer2D(
96
+ in_dim=int(model_config["in_dim"]),
97
+ out_dim=int(model_config["out_dim"]),
98
+ spatial_shape=spatial_shape,
99
+ hidden_dim=int(model_config["hidden_dim"]),
100
+ depth=int(model_config["depth"]),
101
+ heads=int(model_config["heads"]),
102
+ mlp_ratio=int(model_config["mlp_ratio"]),
103
+ dropout=float(model_config["dropout"]),
104
+ activation=str(model_config["activation"]),
105
+ include_pos=bool(model_config["include_pos"]),
106
+ space_dim=int(model_config["space_dim"]),
107
+ latent_multiplier=float(model_config["latent_multiplier"]),
108
+ max_latent_steps=int(model_config["max_latent_steps"]),
109
+ )
110
+
111
+
112
+ def evaluate(
113
+ model: torch.nn.Module,
114
+ loader: torch.utils.data.DataLoader,
115
+ device: torch.device,
116
+ datapipe: KolmogorovFlow2DDatapipe,
117
+ t_out: int,
118
+ out_dim: int,
119
+ max_latent_steps: int,
120
+ max_batches: int | None,
121
+ distributed: bool,
122
+ rank: int,
123
+ ) -> tuple[float, float]:
124
+ model.eval()
125
+ totals = torch.zeros(4, dtype=torch.float64, device=device)
126
+ with torch.no_grad():
127
+ iterator = tqdm(loader, desc="Evaluating", disable=rank != 0)
128
+ for batch_index, batch in enumerate(iterator):
129
+ if max_batches is not None and batch_index >= max_batches:
130
+ break
131
+ pos = batch["pos"].to(device)
132
+ state = batch["x"].to(device)
133
+ target = batch["y"].to(device)
134
+ prediction = rollout(
135
+ model, pos, state, t_out, out_dim, max_latent_steps
136
+ )
137
+ prediction = datapipe.decode_solution(prediction)
138
+ target = datapipe.decode_solution(target)
139
+ totals[0] += F.mse_loss(prediction, target, reduction="sum")
140
+ totals[1] += target.numel()
141
+ totals[2] += relative_l2(prediction, target).sum()
142
+ totals[3] += target.shape[0]
143
+ if distributed:
144
+ torch.distributed.all_reduce(totals)
145
+ return (
146
+ (totals[0] / totals[1].clamp_min(1)).item(),
147
+ (totals[2] / totals[3].clamp_min(1)).item(),
148
+ )
149
+
150
+
151
+ def main() -> None:
152
+ config_path = DEFAULT_CONFIG.resolve()
153
+ config = load_config(config_path)
154
+ prepare_config(config)
155
+ validate_config(config)
156
+ common = config["common"]
157
+ datapipe_config = config["datapipe"]
158
+ model_config = config["model"]
159
+ training = config["training"]
160
+ seed_everything(int(common["seed"]))
161
+
162
+ DistributedManager.initialize()
163
+ dist = DistributedManager()
164
+ distributed = dist.world_size > 1
165
+ device = dist.device if str(common["device"]) == "auto" else resolve_device(
166
+ str(common["device"])
167
+ )
168
+ weight_dir = project_path(training["weight_dir"], PROJECT_ROOT).resolve()
169
+ checkpoint_path = weight_dir / str(training["checkpoint_name"])
170
+
171
+ if dist.rank == 0:
172
+ print(f"Config: {config_path}")
173
+ print(
174
+ "Data: "
175
+ f"{Path(datapipe_config['source']['data_dir']) / datapipe_config['source']['file_name']}"
176
+ )
177
+ print(f"Device: {device}")
178
+ print(
179
+ f"Samples: train={datapipe_config['data']['train_num']} "
180
+ f"test={datapipe_config['data']['test_num']} "
181
+ f"t_in={datapipe_config['data']['t_in']} "
182
+ f"t_out={datapipe_config['data']['t_out']}"
183
+ )
184
+
185
+ started = time.time()
186
+ try:
187
+ datapipe = KolmogorovFlow2DDatapipe(
188
+ to_attr_dict(datapipe_config), distributed=distributed
189
+ )
190
+ train_loader, train_sampler = datapipe.train_dataloader()
191
+ test_loader, _ = datapipe.test_dataloader()
192
+ spatial_shape = tuple(datapipe.spatial_shape)
193
+ model = build_model(model_config, spatial_shape).to(device)
194
+ if distributed:
195
+ device_ids = [dist.local_rank] if device.type == "cuda" else None
196
+ model = DDP(model, device_ids=device_ids)
197
+ if dist.rank == 0:
198
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
199
+ print(f"Spatial shape: {spatial_shape}")
200
+ print(f"Parameters: {parameter_count:,}")
201
+
202
+ optimizer = torch.optim.AdamW(
203
+ model.parameters(),
204
+ lr=float(training["lr"]),
205
+ weight_decay=float(training["weight_decay"]),
206
+ )
207
+ scheduler = torch.optim.lr_scheduler.StepLR(
208
+ optimizer,
209
+ step_size=int(training["step_size"]),
210
+ gamma=float(training["gamma"]),
211
+ )
212
+ data = datapipe_config["data"]
213
+ t_out = int(data["t_out"])
214
+ out_dim = int(data["out_dim"])
215
+ max_latent_steps = int(model_config["max_latent_steps"])
216
+ train_latent_steps = int(training["train_latent_steps"])
217
+ max_train_batches = training.get("max_train_batches")
218
+ max_train_batches = None if max_train_batches is None else int(max_train_batches)
219
+ max_eval_batches = training.get("max_eval_batches")
220
+ max_eval_batches = None if max_eval_batches is None else int(max_eval_batches)
221
+ best_relative_l2 = float("inf")
222
+ stale_evaluations = 0
223
+
224
+ for epoch in range(1, int(training["epochs"]) + 1):
225
+ if train_sampler is not None:
226
+ train_sampler.set_epoch(epoch)
227
+ model.train()
228
+ epoch_loss = 0.0
229
+ batch_count = 0
230
+ iterator = tqdm(train_loader, desc=f"Epoch {epoch}", disable=dist.rank != 0)
231
+ for batch_index, batch in enumerate(iterator):
232
+ if max_train_batches is not None and batch_index >= max_train_batches:
233
+ break
234
+ pos = batch["pos"].to(device)
235
+ state = batch["x"].to(device)
236
+ target = batch["y"].to(device)[..., : train_latent_steps * out_dim]
237
+ optimizer.zero_grad(set_to_none=True)
238
+ prediction = model(pos, state, latent_steps=train_latent_steps)
239
+ loss = F.mse_loss(prediction, target)
240
+ if not torch.isfinite(loss):
241
+ raise FloatingPointError(f"Non-finite training loss at epoch {epoch}")
242
+ loss.backward()
243
+ if training.get("max_grad_norm") is not None:
244
+ torch.nn.utils.clip_grad_norm_(
245
+ model.parameters(), float(training["max_grad_norm"])
246
+ )
247
+ optimizer.step()
248
+ epoch_loss += loss.item()
249
+ batch_count += 1
250
+ if dist.rank == 0:
251
+ iterator.set_postfix(loss=f"{loss.item():.3e}")
252
+ scheduler.step()
253
+ if batch_count == 0:
254
+ raise RuntimeError("No training batches were processed")
255
+
256
+ should_evaluate = (
257
+ epoch % int(training["eval_interval"]) == 0
258
+ or epoch == int(training["epochs"])
259
+ )
260
+ if not should_evaluate:
261
+ continue
262
+ validation_mse, validation_relative_l2 = evaluate(
263
+ model,
264
+ test_loader,
265
+ device,
266
+ datapipe,
267
+ t_out,
268
+ out_dim,
269
+ max_latent_steps,
270
+ max_eval_batches,
271
+ distributed,
272
+ dist.rank,
273
+ )
274
+ if dist.rank == 0:
275
+ print(
276
+ f"epoch={epoch:4d} train_mse={epoch_loss / batch_count:.6e} "
277
+ f"val_mse={validation_mse:.6e} "
278
+ f"val_relative_l2={validation_relative_l2:.6e}"
279
+ )
280
+ if validation_relative_l2 < best_relative_l2:
281
+ best_relative_l2 = validation_relative_l2
282
+ stale_evaluations = 0
283
+ weight_dir.mkdir(parents=True, exist_ok=True)
284
+ model_to_save = model.module if isinstance(model, DDP) else model
285
+ torch.save(
286
+ {
287
+ "epoch": epoch,
288
+ "model_state": model_to_save.state_dict(),
289
+ "model_config": to_plain_dict(model_config),
290
+ "datapipe_config": to_plain_dict(datapipe_config),
291
+ "training_config": to_plain_dict(training),
292
+ "spatial_shape": spatial_shape,
293
+ "normalizer": datapipe.get_normalizer_state(),
294
+ "best_relative_l2": best_relative_l2,
295
+ },
296
+ checkpoint_path,
297
+ )
298
+ print(f"Saved checkpoint: {checkpoint_path}")
299
+ else:
300
+ stale_evaluations += 1
301
+
302
+ stop = torch.tensor(
303
+ [int(stale_evaluations >= int(training["patience"]))], device=device
304
+ )
305
+ if distributed:
306
+ torch.distributed.broadcast(stop, src=0)
307
+ if stop.item():
308
+ if dist.rank == 0:
309
+ print("Early stopping triggered")
310
+ break
311
+
312
+ if dist.rank == 0:
313
+ print(f"Elapsed: {time.time() - started:.1f}s")
314
+ finally:
315
+ DistributedManager.cleanup()
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()
weight/factformer_kolmogorov.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:873ec2ca9fef3940da35aaecdc68dc9079401d0a60ddeca7559625fd8e94765e
3
+ size 3672514