Zhongning commited on
Commit
0ba2894
·
verified ·
1 Parent(s): 4d18cd8

Upload folder using huggingface_hub

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.ms_upload_cache ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version": 3, "repo_id": "OneScience/PrithviWxC", "files": {"README.md|1787118190.0|6111": {"hash": "4644dd18dcfd1c681633df078cd60b87a647c9cdf9505c83e0b9f6e03af55faf", "size": 6111, "status": "c"}, "conf/config.yaml|1787189055.0|2484": {"hash": "dc0ae83fd9cb50c61ce7a15fea77481f85c1f587fece5e63a5127a82ba273a67", "size": 2484, "status": "c"}, "model/__pycache__/prithvi_wxc.cpython-311.pyc|1787129097.0|6399": {"hash": "001025caa3762a8c91555f64ca871e687d234518abc23382206b83e0b3881978", "size": 6399, "status": "c"}, "model/__pycache__/prithvi_wxc_official.cpython-311.pyc|1787129243.0|76780": {"hash": "76c3889a828a1ee60630b10ea5a61dce9e25e8ef045fd71e2d150349f77e48ed", "size": 76780, "status": "c"}, "model/prithvi_wxc_official.py|1787129237.0|61202": {"hash": "58a3712e4739889be50dcb43106e7bfa43ed3c10e1ff44422a8192d6f343f992", "size": 61202, "status": "c"}, "configuration.json|1787037409.0|38": {"hash": "97d4072fc4a7a3b71e3184609fc491e77a324a982d9df860d9379c009dff84de", "size": 38, "status": "c"}, "LICENSE|1787037359.0|1106": {"hash": "c681a18eea18d4a07e4f429ca50d3f874c9d7982bb4836fff613218dfe47818e", "size": 1106, "status": "c"}, "model/prithvi_wxc.py|1787037757.0|5712": {"hash": "9f4186ae8f902dfa1b606c4ef53731c4923fda7c212a99941167f65ce37360d3", "size": 5712, "status": "c"}, "scripts/fake_data.py|1787118238.0|4985": {"hash": "b16a395251bffa0ee2948643d66c5d40a89fb63b397c23ec8703cd90f97f2859", "size": 4985, "status": "c"}, "scripts/inference.py|1787118190.0|4127": {"hash": "07920093609f16618001ef9a138ac1b843dd8985c1abfdc8accedf18518bec04", "size": 4127, "status": "c"}, "scripts/result.py|1787129368.0|8391": {"hash": "9a5b66ed2b41bbb766b3ce26a286be904d6d2483d8fde2ada6ea6c6044e6aed0", "size": 8391, "status": "c"}, "scripts/train.py|1787118190.0|10421": {"hash": "af5547fc972472f51bfcc719da9f6d1746492428e03a73345e2f23ac41f635b8", "size": 10421, "status": "c"}, "weight/.gitkeep|1787037589.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}}}
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Inter Agency Implementation and Advanced Concepts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - Earth science
9
+ - Weather forecasting
10
+ - Medium- to long-range weather forecasting
11
+ - Foundation models
12
+ - Vision Transformer
13
+ - ERA5
14
+ frameworks: PyTorch
15
+ datasets:
16
+ - OneScience/ERA5
17
+ ---
18
+ <p align="center">
19
+ <strong>
20
+ <span style="font-size: 30px;">Prithvi WxC</span>
21
+ </strong>
22
+ </p>
23
+
24
+
25
+ # Model Introduction
26
+
27
+ Prithvi WxC (Weather and Climate) was proposed by NASA-IMPACT, IBM, and other teams. It is a weather and climate foundation model based on a Vision Transformer (alternating local/global attention with Hiera and MaxViT), supporting forecasting (6-hour-step rollout) and climate simulation (internal error growth).
28
+
29
+ Paper:Prithvi WxC: Foundation Model for Weather and Climate
30
+
31
+ https://arxiv.org/abs/2409.13598
32
+
33
+ # Model Description
34
+
35
+ Prithvi WxC is a deterministic global weather foundation model: it takes atmospheric states at two consecutive 6-hour time steps, optionally with static fields, and outputs the target state. Longer lead times are obtained through autoregressive rollout.This repository is organized from the official `NASA-IMPACT/Prithvi-WxC` implementation and integrated with the OneScience data loading and training workflow.
36
+
37
+ # Use Cases
38
+
39
+ | Scenario | Description |
40
+ | :---: | :--- |
41
+ | Global weather and climate foundation model research | Train or fine-tune a Vision Transformer forecasting model on ERA5 data. |
42
+ | Long-horizon autoregressive rollout | Generate medium- to long-range forecasts autoregressively at 6-hour intervals. |
43
+ | Local quick validation | Use synthetic data to check data loading, training, inference, and result scripts. |
44
+ | ModelScope/OneCode execution | Download the model package, install dependencies, and run the scripts directly. |
45
+ | Multi-card training | Launch multi-process training with `torchrun`. |
46
+
47
+
48
+ # Usage
49
+
50
+ ## 1. OneCode Usage
51
+
52
+ Use the OneCode online environment for intelligent one-click AI4S programming:
53
+
54
+ [Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
55
+
56
+ ## 2. Manual Installation and Usage
57
+
58
+ **Hardware Requirements**
59
+
60
+ - GPU or DCU is recommended.
61
+ - CPU can be used for imports and small-configuration connectivity validation, but full training and inference are slower.
62
+ - DCU users must install DTK beforehand. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
63
+ - The paper-level configuration (`embed_dim=2560`, 25 encoder blocks, 5 decoder blocks, and approximately 2.3 billion parameters) requires substantial GPU memory.
64
+
65
+
66
+ ### Download the Model Package
67
+
68
+ ```bash
69
+ hf download OneScience-Group/PrithviWxC --local-dir ./PrithviWxC
70
+ cd PrithviWxC
71
+ ```
72
+
73
+ ### Install the Runtime Environment
74
+
75
+ **DCU Environment**
76
+
77
+ ```bash
78
+ # Activate DTK and CONDA first
79
+ conda create -n onescience311 python=3.11 -y
80
+ conda activate onescience311
81
+ # uv installation is supported
82
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
83
+ ```
84
+
85
+ **GPU Environment**
86
+ ```bash
87
+ # Activate CONDA first
88
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
89
+ conda activate onescience311
90
+ # uv installation is supported
91
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
92
+ ```
93
+
94
+ ### Training Data
95
+
96
+ The OneScience community provides ERA5 data for training (the current repository contains complete data slices subject to data-file size limits). Download it with the command below and confirm that the data path in `conf/config.yaml` is correct:
97
+
98
+ ```bash
99
+ hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
100
+ ```
101
+
102
+ For a quick workflow validation, run the synthetic data script first:
103
+
104
+ ```bash
105
+ python scripts/fake_data.py
106
+ ```
107
+
108
+ > Note: `scripts/fake_data.py` generates the `[T, C, H, W]` HDF5 data required by the two input time steps and generates `data/static/static.npy` (currently `[4, 32, 64]`) for training and inference.
109
+
110
+ ### Training
111
+
112
+ Single card:
113
+
114
+ ```bash
115
+ python scripts/train.py
116
+ ```
117
+
118
+ Multiple cards:
119
+
120
+ ```bash
121
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
122
+ ```
123
+
124
+ Training outputs:
125
+
126
+ ```text
127
+ data/checkpoints/model_bak.pth
128
+ data/checkpoints/trloss.npy
129
+ data/checkpoints/valoss.npy
130
+ ```
131
+
132
+ ### Training Weights
133
+ The `weight/` folder is reserved for model weights. Official weights with approximately 2.3 billion parameters (such as PrithviWxC_160_13b_2t_0p5d_v1.pt) are published on Hugging Face, but their structure differs from this repository's small configuration. Align the channel count and grid size before loading; weights are not provided by default, and users may train the model using the paper configuration.
134
+
135
+ ### Inference
136
+
137
+ Inference reads `data/checkpoints/model_bak.pth`:
138
+
139
+ ```bash
140
+ python scripts/inference.py
141
+ ```
142
+
143
+ Prediction results are written to:
144
+
145
+ ```text
146
+ result/output/
147
+ ```
148
+
149
+ ### Evaluation and Visualization
150
+
151
+ ```bash
152
+ python scripts/result.py
153
+ ```
154
+
155
+ Outputs include:
156
+
157
+ - `result/rmse.npy`
158
+ - `result/acc.npy`
159
+ - `result/loss.png`
160
+ - Forecast comparison plots for the specified date and variables
161
+
162
+
163
+ # Official Source and Reproduction Notes
164
+
165
+ - The model implementation comes from the official `NASA-IMPACT/Prithvi-WxC` (MIT License). The official implementation is embedded unchanged in `model/prithvi_wxc_official.py`; `model/prithvi_wxc.py` is only a YAML-driven thin wrapper (with identity normalization parameters for small-configuration connectivity validation).
166
+ - Commit fetched for the current case directory: `79dabfcd17abe77e2d5c696707c0164a04f2ec01` (2026-02-05).
167
+ - `conf/config.yaml` uses a small configuration (`embed_dim=32`, `n_blocks_encoder=1`, `n_blocks_decoder=1`) for connectivity validation by default; paper-level reproduction requires a 0.5°×0.625° grid, 160 channels, `embed_dim=2560`, and 13+12 encoder blocks/3+2 decoder blocks as specified in the paper.
168
+ - The following details are not disclosed in the paper and are assumptions in this reproduction:data normalization statistics (identity normalization is currently used; real statistics will be supplied with the data), masked-training details and pretraining schedule, and some hyperparameters (such as the relative positional encoding implementation).
169
+
170
+ # Official OneScience Information
171
+
172
+ | Platform | OneScience Main Repository | Skills Repository |
173
+ | --- | --- | --- |
174
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
175
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
176
+
177
+ # Citation and License
178
+
179
+ - This repository is an independent organization and adaptation of Prithvi WxC. The model source is based on the official `NASA-IMPACT/Prithvi-WxC` implementation by Schmude et al. (2024) and follows the MIT License.
180
+ - Please cite:Schmude et al. Prithvi WxC: Foundation Model for Weather and Climate. arXiv:2409.13598, 2024.
conf/config.yaml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prithvi WxC 训练配置示例
2
+ # 论文配置为 0.5°×0.625°(360/361×576)网格、160 动态变量、embed_dim=2560、
3
+ # 编码器 13 本地+12 全局块、解码器 3 本地+2 全局块(约 23 亿参数,需大规模显存)。
4
+ # 当前为连通性验证小配置(约百万级参数)。
5
+ model:
6
+ start_epoch: 0
7
+ max_epoch: 100
8
+ lr: 1E-3
9
+ patience: 50
10
+ checkpoint_dir: "./data/checkpoints"
11
+
12
+ # Prithvi WxC 结构参数
13
+ in_channels: 6 # 论文为 160(20 单层 + 10 变量 ×14 层)
14
+ input_size_time: 2 # 两个输入时刻
15
+ in_channels_static: 4 # 静态通道数(论文 MEMRA-2 为 4)
16
+ n_lats_px: 32 # 数据纬度方向像元数(论文约 360)
17
+ n_lons_px: 64 # 数据经度方向像元数(论文 576)
18
+ patch_size_px: [2, 2] # token 尺寸(论文 2×2 像素)
19
+ mask_unit_size_px: [8, 8] # 掩码单元尺寸
20
+ mask_ratio_inputs: 0.0 # 预训练为 0.5;预报滚动微调为 0.0
21
+ embed_dim: 32 # 隐藏维度(论文 2560)
22
+ n_blocks_encoder: 1 # 编码器本地-全局对数量(论文 13 对 -> 25 块)
23
+ n_blocks_decoder: 1 # 解码器本地-全局对数量(论文 5 块)
24
+ mlp_multiplier: 4.0
25
+ n_heads: 4 # 注意力头数(论文 16)
26
+ dropout: 0.0
27
+ drop_path: 0.0
28
+ parameter_dropout: 0.0
29
+ residual: "none" # none / temporal / climate
30
+ masking_mode: "global"
31
+ positional_encoding: "absolute"
32
+ encoder_shifting: false
33
+ decoder_shifting: false
34
+
35
+ # 整个数据读取流程
36
+ datapipe:
37
+ name: "ERA5"
38
+ task: "weather_forecasting"
39
+
40
+ dataset:
41
+ type: "hdf5"
42
+ data_dir: './data/'
43
+ train_time: [1951, 1952]
44
+ val_time: [1953]
45
+ test_time: [1954]
46
+ img_size: [32, 64]
47
+ verbose: true
48
+ cache: false
49
+
50
+ # 气象变量(论文 160 通道配置的子集,用于连通性验证)
51
+ channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature',
52
+ 'mean_sea_level_pressure', 'geopotential_500', 'temperature_850']
53
+
54
+ # DataLoader 配置
55
+ dataloader:
56
+ mask_dtype: "float32"
57
+ batch_size: 1
58
+ num_workers: 1
59
+ pin_memory: true
60
+ drop_last: true
61
+ shuffle: false
62
+ prefetch_factor: 2
63
+ persistent_workers: true
64
+
65
+ # 分布式配置
66
+ distributed:
67
+ enabled: true
68
+ sampler: "DistributedSampler"
69
+ rank: 0
70
+ world_size: 2
71
+ shuffle: true
72
+ seed: 42
73
+ drop_last: true
config.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "Prithvi WxC",
3
+ "model_type": "prithvi_wxc",
4
+ "architectures": [
5
+ "PrithviWxC"
6
+ ],
7
+ "framework": "PyTorch",
8
+ "domain": "climate-and-atmosphere",
9
+ "task": "global-weather-forecasting",
10
+ "implementation": {
11
+ "entry_point": "model/prithvi_wxc.py",
12
+ "scope": "YAML-driven wrapper around the vendored official Hiera-MaxViT encoder-decoder, with identity model scalers for the repository's small connectivity configuration"
13
+ },
14
+ "architecture": {
15
+ "family": "Hiera-MaxViT encoder-decoder Vision Transformer",
16
+ "attention_mechanism": "alternating local block attention and global grid attention",
17
+ "input_format": "BTCHW atmospheric history plus BCHW static fields",
18
+ "output_format": "BCHW next atmospheric state",
19
+ "tokenization": "Conv2d patch embedding followed by mask-unit grouping and pixel-shuffle reconstruction",
20
+ "activation": "GELU",
21
+ "normalization": "LayerNorm",
22
+ "repository_default_config": {
23
+ "purpose": "small connectivity validation with synthetic or reduced ERA5 data",
24
+ "in_channels": 6,
25
+ "input_size_time": 2,
26
+ "in_channels_static": 4,
27
+ "grid_size": [
28
+ 32,
29
+ 64
30
+ ],
31
+ "patch_size_px": [
32
+ 2,
33
+ 2
34
+ ],
35
+ "mask_unit_size_px": [
36
+ 8,
37
+ 8
38
+ ],
39
+ "mask_ratio_inputs": 0.0,
40
+ "embed_dim": 32,
41
+ "n_blocks_encoder": 1,
42
+ "n_blocks_decoder": 1,
43
+ "n_heads": 4,
44
+ "mlp_multiplier": 4.0,
45
+ "dropout": 0.0,
46
+ "drop_path": 0.0,
47
+ "parameter_dropout": 0.0,
48
+ "residual": "none",
49
+ "masking_mode": "global",
50
+ "positional_encoding": "absolute",
51
+ "encoder_shifting": false,
52
+ "decoder_shifting": false,
53
+ "normalization_scalers": "identity in the YAML wrapper"
54
+ },
55
+ "paper_reference_config": {
56
+ "grid_resolution_degrees": [
57
+ 0.5,
58
+ 0.625
59
+ ],
60
+ "grid_size": "approximately 360/361 x 576",
61
+ "dynamic_channels": 160,
62
+ "embed_dim": 2560,
63
+ "encoder_blocks": "13 local plus 12 global blocks",
64
+ "decoder_blocks": "3 local plus 2 global blocks",
65
+ "n_heads": 16,
66
+ "pretraining_mask_ratio_inputs": 0.5,
67
+ "forecast_finetuning_mask_ratio_inputs": 0.0,
68
+ "parameter_count": "approximately 2.3 billion",
69
+ "compatibility_note": "official 2.3B checkpoints do not match the repository default small configuration without aligning channels, grid size, and architecture"
70
+ }
71
+ },
72
+ "data": {
73
+ "dataset": "ERA5",
74
+ "storage": "HDF5 fields with shape TCHW",
75
+ "temporal_interval_hours": 6,
76
+ "input_steps": 2,
77
+ "output_steps": 1,
78
+ "forecast_lead_time_hours": 6,
79
+ "dynamic_channels": [
80
+ "10m_u_component_of_wind",
81
+ "10m_v_component_of_wind",
82
+ "2m_temperature",
83
+ "mean_sea_level_pressure",
84
+ "geopotential_500",
85
+ "temperature_850"
86
+ ],
87
+ "static_channels": 4,
88
+ "default_spatial_size": [
89
+ 32,
90
+ 64
91
+ ],
92
+ "default_year_splits": {
93
+ "train": [
94
+ 1951,
95
+ 1952
96
+ ],
97
+ "validation": [
98
+ 1953
99
+ ],
100
+ "test": [
101
+ 1954
102
+ ]
103
+ },
104
+ "synthetic_data_generator": "scripts/fake_data.py"
105
+ },
106
+ "configuration_sources": [
107
+ "conf/config.yaml",
108
+ "model/prithvi_wxc.py",
109
+ "model/prithvi_wxc_official.py",
110
+ "scripts/train.py",
111
+ "scripts/inference.py",
112
+ "scripts/fake_data.py",
113
+ "README.md",
114
+ "configuration.json"
115
+ ]
116
+ }
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
model/__pycache__/prithvi_wxc.cpython-311.pyc ADDED
Binary file (6.4 kB). View file
 
model/__pycache__/prithvi_wxc_official.cpython-311.pyc ADDED
Binary file (76.8 kB). View file
 
model/prithvi_wxc.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ #
5
+ # Copyright (c) 2024 Inter Agency Implementation and Advanced Concepts
6
+ #
7
+ # This module is a thin configuration wrapper around the official Prithvi WxC
8
+ # model (Schmude et al., arXiv:2409.13598), taken verbatim from the
9
+ # NASA-IMPACT/Prithvi-WxC repository (commit 79dabfcd17abe77e2d5c696707c0164a04f2ec01,
10
+ # MIT License). The official implementation is vendored as
11
+ # ``prithvi_wxc_official.py``; only scaler construction and tensor-level call
12
+ # signatures are added here for YAML-driven usage.
13
+ import torch
14
+ import torch.nn as nn
15
+
16
+ from model.prithvi_wxc_official import PrithviWxC as _PrithviWxC
17
+
18
+
19
+ def _identity_scalers(n, device="cpu"):
20
+ return torch.zeros(n, device=device), torch.ones(n, device=device)
21
+
22
+
23
+ class PrithviWxC(nn.Module):
24
+ """
25
+ Config-driven Prithvi WxC wrapper.
26
+
27
+ The official model is an encoder-decoder vision Transformer (Hiera + MaxViT,
28
+ alternating local/global attention) that maps an input state history to a
29
+ target state. This wrapper builds the official network with identity
30
+ scalers so that a small YAML config can be exercised; loading the 2.3B
31
+ checkpoints published on Hugging Face is possible by calling
32
+ ``load_state_dict`` with the official checkpoint state (the buffer names,
33
+ e.g. ``input_scalers_mu``, are identical).
34
+
35
+ Args:
36
+ in_channels: number of input/output parameters.
37
+ input_size_time: number of input timestamps (paper uses 2).
38
+ in_channels_static: number of static input channels (>= 1).
39
+ n_lats_px / n_lons_px: grid size in pixels.
40
+ patch_size_px: patch/token size in pixels.
41
+ mask_unit_size_px: mask unit size in pixels.
42
+ mask_ratio_inputs: input masking ratio (0..1, 0 disables).
43
+ embed_dim / n_blocks_encoder / n_blocks_decoder / mlp_multiplier / n_heads:
44
+ transformer architecture hyper-parameters (paper: 2560 / 25 / 5 / 4 / 16).
45
+ dropout / drop_path / parameter_dropout: regularization ratios.
46
+ residual: one of "none" / "temporal" / "climate".
47
+ masking_mode: "global" / "local" / "both".
48
+ positional_encoding: "absolute" or "fourier".
49
+ encoder_shifting / decoder_shifting: Swin-style shifting.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ in_channels=4,
55
+ input_size_time=2,
56
+ in_channels_static=4,
57
+ n_lats_px=32,
58
+ n_lons_px=64,
59
+ patch_size_px=(2, 2),
60
+ mask_unit_size_px=(8, 8),
61
+ mask_ratio_inputs=0.0,
62
+ embed_dim=16,
63
+ n_blocks_encoder=1,
64
+ n_blocks_decoder=1,
65
+ mlp_multiplier=4.0,
66
+ n_heads=4,
67
+ dropout=0.0,
68
+ drop_path=0.0,
69
+ parameter_dropout=0.0,
70
+ residual="none",
71
+ masking_mode="global",
72
+ positional_encoding="absolute",
73
+ encoder_shifting=False,
74
+ decoder_shifting=False,
75
+ ):
76
+ super().__init__()
77
+ self.in_channels = int(in_channels)
78
+ self.input_size_time = int(input_size_time)
79
+ self.in_channels_static = int(in_channels_static)
80
+ self.n_lats_px = int(n_lats_px)
81
+ self.n_lons_px = int(n_lons_px)
82
+ self.residual = residual
83
+ self.positional_encoding = positional_encoding
84
+
85
+ mu_in, sigma_in = _identity_scalers(self.in_channels)
86
+ mu_s, sigma_s = _identity_scalers(self.in_channels_static)
87
+
88
+ self.model = _PrithviWxC(
89
+ in_channels=self.in_channels,
90
+ input_size_time=self.input_size_time,
91
+ in_channels_static=self.in_channels_static,
92
+ input_scalers_mu=mu_in,
93
+ input_scalers_sigma=sigma_in,
94
+ input_scalers_epsilon=1e-6,
95
+ static_input_scalers_mu=mu_s,
96
+ static_input_scalers_sigma=sigma_s,
97
+ static_input_scalers_epsilon=1e-6,
98
+ output_scalers=sigma_in,
99
+ n_lats_px=self.n_lats_px,
100
+ n_lons_px=self.n_lons_px,
101
+ patch_size_px=tuple(patch_size_px),
102
+ mask_unit_size_px=tuple(mask_unit_size_px),
103
+ mask_ratio_inputs=float(mask_ratio_inputs),
104
+ mask_ratio_targets=0.0,
105
+ embed_dim=int(embed_dim),
106
+ n_blocks_encoder=int(n_blocks_encoder),
107
+ n_blocks_decoder=int(n_blocks_decoder),
108
+ mlp_multiplier=int(mlp_multiplier),
109
+ n_heads=int(n_heads),
110
+ dropout=float(dropout),
111
+ drop_path=float(drop_path),
112
+ parameter_dropout=float(parameter_dropout),
113
+ residual=residual,
114
+ masking_mode=masking_mode,
115
+ positional_encoding=positional_encoding,
116
+ encoder_shifting=bool(encoder_shifting),
117
+ decoder_shifting=bool(decoder_shifting),
118
+ )
119
+
120
+ def forward(self, x, static, input_time=None, lead_time=None, climate=None):
121
+ """
122
+ Args:
123
+ x: Tensor of shape [batch, time, parameter, lat, lon].
124
+ static: Tensor of shape [batch, static_channel, lat, lon].
125
+ input_time: Tensor of shape [batch] (optional, default zeros).
126
+ lead_time: Tensor of shape [batch] (optional, default zeros).
127
+ climate: Optional Tensor of shape [batch, parameter, lat, lon].
128
+ Returns:
129
+ Tensor of shape [batch, parameter, lat, lon].
130
+ """
131
+ if input_time is None:
132
+ input_time = torch.zeros(x.shape[0], device=x.device)
133
+ if lead_time is None:
134
+ lead_time = torch.zeros(x.shape[0], device=x.device)
135
+ batch = {
136
+ "x": x,
137
+ "y": x[:, -1],
138
+ "static": static,
139
+ "input_time": input_time,
140
+ "lead_time": lead_time,
141
+ }
142
+ if climate is not None:
143
+ batch["climate"] = climate
144
+ return self.model(batch)
model/prithvi_wxc_official.py ADDED
@@ -0,0 +1,1637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from importlib.metadata import version
2
+ TORCH_VERSION = version('torch')
3
+
4
+ from functools import cached_property
5
+ from typing import Optional
6
+ import numpy as np
7
+
8
+ import torch
9
+ from torch import Tensor
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ from torch.utils.checkpoint import checkpoint
13
+ if TORCH_VERSION > '2.3.0':
14
+ from torch.nn.attention import SDPBackend, sdpa_kernel
15
+
16
+
17
+ # DropPath code is straight from timm
18
+ # (https://huggingface.co/spaces/Roll20/pet_score/blame/main/lib/timm/models/layers/drop.py)
19
+ # Primarily since we currently don't have timm in the environment.
20
+ def drop_path(
21
+ x, drop_prob: float = 0.0, training: bool = False, scale_by_keep: bool = True
22
+ ):
23
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
24
+
25
+ This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
26
+ the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
27
+ See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
28
+ changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
29
+ 'survival rate' as the argument.
30
+ """
31
+ if drop_prob == 0.0 or not training:
32
+ return x
33
+ keep_prob = 1 - drop_prob
34
+ shape = (x.shape[0],) + (1,) * (
35
+ x.ndim - 1
36
+ ) # work with diff dim tensors, not just 2D ConvNets
37
+ random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
38
+ if keep_prob > 0.0 and scale_by_keep:
39
+ random_tensor.div_(keep_prob)
40
+ return x * random_tensor
41
+
42
+
43
+ class DropPath(nn.Module):
44
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
45
+
46
+ def __init__(self, drop_prob=None, scale_by_keep=True):
47
+ super(DropPath, self).__init__()
48
+ self.drop_prob = drop_prob
49
+ self.scale_by_keep = scale_by_keep
50
+
51
+ def forward(self, x):
52
+ return drop_path(x, self.drop_prob, self.training, self.scale_by_keep)
53
+
54
+
55
+ class Mlp(nn.Module):
56
+ """
57
+ Multi layer perceptron.
58
+ """
59
+
60
+ def __init__(
61
+ self, features: int, hidden_features: int, dropout: float = 0.0
62
+ ) -> None:
63
+ """
64
+ Args:
65
+ features: Input/output dimension.
66
+ hidden_features: Hidden dimension.
67
+ dropout: Dropout.
68
+ """
69
+ super().__init__()
70
+ self.net = nn.Sequential(
71
+ nn.Linear(features, hidden_features),
72
+ nn.GELU(),
73
+ nn.Dropout(dropout),
74
+ nn.Linear(hidden_features, features),
75
+ nn.Dropout(dropout),
76
+ )
77
+
78
+ def forward(self, x: Tensor) -> Tensor:
79
+ """
80
+ Args:
81
+ Tensor of shape [..., channel]
82
+ Returns:
83
+ Tensor of same shape as x.
84
+ """
85
+ return self.net(x)
86
+
87
+
88
+ class LayerNormPassThrough(nn.LayerNorm):
89
+ """
90
+ Normalising layer that allows the attention mask to be passed through
91
+ """
92
+
93
+ def __init__(self, *args, **kwargs):
94
+ super().__init__(*args, **kwargs)
95
+
96
+ def forward(self, d: tuple[Tensor, Tensor | None]) -> tuple[Tensor, Tensor | None]:
97
+ """
98
+ Forwards function
99
+ Args:
100
+ d: tuple of the data tensor and the attention mask
101
+ Returns:
102
+ output: normalised output data
103
+ attn_mask: the attention mask that was passed in
104
+ """
105
+ input, attn_mask = d
106
+ output = F.layer_norm(
107
+ input, self.normalized_shape, self.weight, self.bias, self.eps
108
+ )
109
+ return output, attn_mask
110
+
111
+
112
+ class MultiheadAttention(nn.Module):
113
+ """
114
+ Multihead attention layer for inputs of shape [..., sequence, features].
115
+
116
+ Uses `scaled_dot_product_attention` to obtain a memory efficient attention
117
+ computation (https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html).
118
+ This follows:
119
+ - Dao et la. "FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness"
120
+ (https://arxiv.org/abs/2205.14135)
121
+ - Rabe, Staats "Self-attention Does Not Need O(n2) Memory" (https://arxiv.org/abs/2112.05682)
122
+
123
+ Note: Even though the documentation page for `scaled_dot_product_attention`
124
+ states that tensors can have any number of dimensions as long as the shapes
125
+ are along the lines of `(B, ..., S, E)`, the fused and memory efficient
126
+ mechanisms we enforce here require a 4D input. Some experimentatino shows
127
+ that this should be of shape `(B, H, S, E)`, where `H` represents heads.
128
+ However, as of right now this is not confirmed int he documentation.
129
+ """
130
+
131
+ def __init__(self, features: int, n_heads: int, dropout: float) -> None:
132
+ """
133
+ Args:
134
+ features: Number of features for inputs to the layer.
135
+ n_heads: Number of attention heads. Should be a factor of features.
136
+ (I.e. the layer uses features // n_heads.)
137
+ dropout: Dropout.
138
+ """
139
+ super().__init__()
140
+
141
+ if not (features % n_heads) == 0:
142
+ raise ValueError(
143
+ f"Number of features {features} is not divisible by number of heads {n_heads}."
144
+ )
145
+
146
+ self.features = features
147
+ self.n_heads = n_heads
148
+ self.dropout = dropout
149
+
150
+ self.qkv_layer = torch.nn.Linear(features, features * 3, bias=False)
151
+ self.w_layer = torch.nn.Linear(features, features, bias=False)
152
+
153
+ def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:
154
+ """
155
+ Args:
156
+ d: tuple containing Tensor of shape [..., sequence, features] and
157
+ the attention mask
158
+ Returns:
159
+ Tensor of shape [..., sequence, features]
160
+ """
161
+ x, attn_mask = d
162
+
163
+ if not x.shape[-1] == self.features:
164
+ raise ValueError(
165
+ f"Expecting tensor with last dimension of size {self.features}."
166
+ )
167
+
168
+ passenger_dims = x.shape[:-2]
169
+ B = passenger_dims.numel()
170
+ S = x.shape[-2]
171
+ C = x.shape[-1]
172
+ x = x.reshape(B, S, C)
173
+
174
+ # x [B, S, C]
175
+ # q, k, v [B, H, S, C/H]
176
+ q, k, v = (
177
+ self.qkv_layer(x)
178
+ .view(B, S, self.n_heads, 3 * (C // self.n_heads))
179
+ .transpose(1, 2)
180
+ .chunk(chunks=3, dim=3)
181
+ )
182
+
183
+ # Let us enforce either flash (A100+) or memory efficient attention,
184
+ # falling back to math on CPU / DCU(昇腾) shim where fused kernels are unavailable.
185
+ if TORCH_VERSION > '2.3.0':
186
+ with sdpa_kernel([SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]):
187
+ # x [B, H, S, C//H]
188
+ x = F.scaled_dot_product_attention(
189
+ q, k, v, dropout_p=self.dropout
190
+ )
191
+ else:
192
+ with torch.backends.cuda.sdp_kernel(
193
+ enable_flash=True, enable_math=False, enable_mem_efficient=True
194
+ ):
195
+ # x [B, H, S, C//H]
196
+ x = F.scaled_dot_product_attention(q, k, v, dropout_p=self.dropout)
197
+
198
+ # x [B, S, C]
199
+ x = x.transpose(1, 2).reshape(B, S, C)
200
+
201
+ # x [B, S, C]
202
+ x = self.w_layer(x)
203
+
204
+ # Back to input shape
205
+ x = x.view(*passenger_dims, S, self.features)
206
+ return x
207
+
208
+
209
+ class Transformer(nn.Module):
210
+ """
211
+ Transformer for inputs of shape [..., S, features].
212
+ """
213
+
214
+ def __init__(
215
+ self,
216
+ features: int,
217
+ mlp_multiplier: int,
218
+ n_heads: int,
219
+ dropout: float,
220
+ drop_path: float,
221
+ ) -> None:
222
+ """
223
+ Args:
224
+ features: Number of features for inputs to the layer.
225
+ mlp_multiplier: Model will use features*mlp_multiplier hidden units.
226
+ n_heads: Number of attention heads. Should be a factor of features.
227
+ (I.e. the layer uses features // n_heads.)
228
+ dropout: Dropout.
229
+ drop_path: DropPath.
230
+ """
231
+ super().__init__()
232
+
233
+ self.features = features
234
+ self.mlp_multiplier = mlp_multiplier
235
+ self.n_heads = n_heads
236
+ self.dropout = dropout
237
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
238
+
239
+ self.attention = nn.Sequential(
240
+ LayerNormPassThrough(features),
241
+ MultiheadAttention(features, n_heads, dropout),
242
+ )
243
+
244
+ self.ff = nn.Sequential(
245
+ nn.LayerNorm(features),
246
+ Mlp(
247
+ features=features,
248
+ hidden_features=features * mlp_multiplier,
249
+ dropout=dropout,
250
+ ),
251
+ )
252
+
253
+ def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:
254
+ """
255
+ Args:
256
+ x: Tensor of shape [..., sequence, features]
257
+ Returns:
258
+ Tensor of shape [..., sequence, features]
259
+ """
260
+ x, attn_mask = d
261
+ if not x.shape[-1] == self.features:
262
+ raise ValueError(
263
+ f"Expecting tensor with last dimension of size {self.features}."
264
+ )
265
+
266
+ attention_x = self.attention(d)
267
+
268
+ x = x + self.drop_path(attention_x)
269
+ x = x + self.drop_path(self.ff(x))
270
+
271
+ return x
272
+
273
+ class _Shift(nn.Module):
274
+ """
275
+ Private base class for the shifter. This allows some behaviour to be easily
276
+ handled when the shifter isn't used.
277
+ """
278
+
279
+ def __init__(self):
280
+ super().__init__()
281
+
282
+ self._shifted = False
283
+
284
+ @torch.no_grad()
285
+ def reset(self) -> None:
286
+ """
287
+ Resets the bool tracking whether the data is shifted
288
+ """
289
+ self._shifted: bool = False
290
+
291
+ def forward(self, data: Tensor) -> tuple[Tensor, dict[bool, None]]:
292
+ return data, {True: None, False: None}
293
+
294
+
295
+ class SWINShift(_Shift):
296
+ """
297
+ Handles the shifting of patches similar to how SWIN works. However if we
298
+ shift the latitudes then the poles will wrap and potentially that might be
299
+ problematic. The possition tokens should handle it but masking is safer.
300
+ """
301
+
302
+ def __init__(
303
+ self,
304
+ mu_shape: tuple[int, int],
305
+ global_shape: tuple[int, int],
306
+ local_shape: tuple[int, int],
307
+ patch_shape: tuple[int, int],
308
+ n_context_tokens: int = 2,
309
+ ) -> None:
310
+ """
311
+ Args:
312
+ mu_shape: the shape to the masking units
313
+ global_shape: number of global patches in lat and lon
314
+ local_shape: size of the local patches
315
+ patch_shape: patch size
316
+ n_context_token: number of additional context tokens at start of _each_ local sequence
317
+ """
318
+ super().__init__()
319
+
320
+ self._mu_shape = ms = mu_shape
321
+ self._g_shape = gs = global_shape
322
+ self._l_shape = ls = local_shape
323
+ self._p_shape = ps = patch_shape
324
+ self._lat_patch = (gs[0], ls[0], gs[1], ls[1])
325
+ self._n_context_tokens = n_context_tokens
326
+
327
+ self._g_shift_to = tuple(int(0.5 * x / p) for x, p in zip(ms, ps))
328
+ self._g_shift_from = tuple(-int(0.5 * x / p) for x, p in zip(ms, ps))
329
+
330
+ # Define the attention masks for the shifted MaxViT.
331
+ nglobal = global_shape[0] * global_shape[1]
332
+ nlocal = local_shape[0] * local_shape[1] + self._n_context_tokens # "+ 1" for leadtime
333
+
334
+ lm = torch.ones((nglobal, 1, nlocal, nlocal), dtype=bool)
335
+ mwidth = int(0.5 * local_shape[1]) * local_shape[0]
336
+ lm[
337
+ : gs[1],
338
+ :,
339
+ self._n_context_tokens : mwidth + self._n_context_tokens,
340
+ self._n_context_tokens : mwidth + self._n_context_tokens,
341
+ ] = False
342
+ self.register_buffer("local_mask", lm)
343
+
344
+ gm = torch.ones((nlocal, 1, nglobal, nglobal), dtype=bool)
345
+ gm[: int(0.5 * ls[1]) * ls[0], :, : gs[1], : gs[1]] = False
346
+ self.register_buffer("global_mask", gm)
347
+
348
+ def _to_grid_global(self, x: Tensor) -> Tensor:
349
+ """
350
+ Shuffle and reshape the data from the global/local setting back to the
351
+ lat/lon grid setting
352
+ Args:
353
+ x: the data tensor to be shuffled.
354
+ Returns:
355
+ x: data in the global/local setting
356
+ """
357
+ nbatch, *other = x.shape
358
+
359
+ y1 = x.view(nbatch, *self._g_shape, *self._l_shape, -1)
360
+ y2 = y1.permute(0, 5, 1, 3, 2, 4).contiguous()
361
+
362
+ s = y2.shape
363
+ return y2.view((nbatch, -1, s[2] * s[3], s[4] * s[5]))
364
+
365
+ def _to_grid_local(self, x: Tensor) -> Tensor:
366
+ """
367
+ Shuffle and reshape the data from the local/global setting to the
368
+ lat/lon grid setting
369
+ Args:
370
+ x: the data tensor to be shuffled.
371
+ Returns:
372
+ x: data in the lat/lon setting.
373
+ """
374
+ x = x.transpose(2, 1).contiguous()
375
+ return self._to_grid_global(x)
376
+
377
+ def _from_grid_global(self, x: Tensor) -> Tensor:
378
+ """
379
+ Shuffle and reshape the data from the lat/lon grid to the global/local
380
+ setting
381
+ Args:
382
+ x: the data tensor to be shuffled.
383
+ Returns:
384
+ x: data in the global/local setting
385
+ """
386
+ nbatch, *other = x.shape
387
+
388
+ z1 = x.view(nbatch, -1, *self._lat_patch)
389
+ z2 = z1.permute(0, 2, 4, 3, 5, 1).contiguous()
390
+
391
+ s = z2.shape
392
+ return z2.view(nbatch, s[1] * s[2], s[3] * s[4], -1)
393
+
394
+ def _from_grid_local(self, x: Tensor) -> Tensor:
395
+ """
396
+ Shuffle and reshape the data from the lat/lon grid to the local/global
397
+ setting
398
+ Args:
399
+ x: the data tensor to be shuffled.
400
+ Returns:
401
+ x: data in the local/global setting
402
+ """
403
+ x = self._from_grid_global(x)
404
+ return x.transpose(2, 1).contiguous()
405
+
406
+ def _shift(self, x: Tensor) -> Tensor:
407
+ """
408
+ Shifts data in the gridded lat/lon setting by half the mask unit shape
409
+ Args:
410
+ x: data to be shifted
411
+ Returns:
412
+ x: either the hsifted or unshifted data
413
+ """
414
+ shift = self._g_shift_from if self._shifted else self._g_shift_to
415
+ x_shifted = torch.roll(x, shift, (-2, -1))
416
+
417
+ self._shifted = not self._shifted
418
+ return x_shifted
419
+
420
+ def _sep_lt(self, x: Tensor) -> tuple[Tensor, Tensor]:
421
+ """
422
+ Seperate off the leadtime from the local patches
423
+ Args:
424
+ x: data to have leadtime removed from
425
+ Returns:
426
+ lt: leadtime
427
+ x: data without the lead time in the local patch
428
+ """
429
+ lt_it = x[:, : self._n_context_tokens, :, :]
430
+ x_stripped = x[:, self._n_context_tokens :, :, :]
431
+
432
+ return lt_it, x_stripped
433
+
434
+ def forward(self, data: Tensor) -> tuple[Tensor, Tensor]:
435
+ """
436
+ Shift or unshift the the data depending on whether the data is already
437
+ shifted, as defined by self._shifted
438
+ Args:
439
+ data: data to be shifted
440
+ Returns:
441
+
442
+ """
443
+ lt, x = self._sep_lt(data)
444
+
445
+ x_grid = self._to_grid_local(x)
446
+ x_shifted = self._shift(x_grid)
447
+ x_patched = self._from_grid_local(x_shifted)
448
+
449
+ # Mask has to be repeated based on batch size
450
+ n_batch = x_grid.shape[0]
451
+ local_rep = [n_batch] + [1] * (self.local_mask.ndim - 1)
452
+ global_rep = [n_batch] + [1] * (self.global_mask.ndim - 1)
453
+
454
+ if self._shifted:
455
+ attn_mask = {
456
+ True: self.local_mask.repeat(local_rep),
457
+ False: self.global_mask.repeat(global_rep),
458
+ }
459
+ else:
460
+ attn_mask = {True: None, False: None}
461
+
462
+ return torch.cat((lt, x_patched), axis=1), attn_mask
463
+
464
+ class SWINShiftNoBuffer(_Shift):
465
+ """
466
+ Handles the shifting of patches similar to how SWIN works. However if we
467
+ shift the latitudes then the poles will wrap and potentially that might be
468
+ problematic. The possition tokens should handle it but masking is safer.
469
+ """
470
+
471
+ def __init__(
472
+ self,
473
+ mu_shape: tuple[int, int],
474
+ global_shape: tuple[int, int],
475
+ local_shape: tuple[int, int],
476
+ patch_shape: tuple[int, int],
477
+ n_context_tokens: int = 2,
478
+ ) -> None:
479
+ """
480
+ Args:
481
+ mu_shape: the shape to the masking units
482
+ global_shape: number of global patches in lat and lon
483
+ local_shape: size of the local patches
484
+ patch_shape: patch size
485
+ n_context_token: number of additional context tokens at start of _each_ local sequence
486
+ """
487
+ super().__init__()
488
+
489
+ self._mu_shape = ms = mu_shape
490
+ self._g_shape = gs = global_shape
491
+ self._l_shape = ls = local_shape
492
+ self._p_shape = ps = patch_shape
493
+ self._lat_patch = (gs[0], ls[0], gs[1], ls[1])
494
+ self._n_context_tokens = n_context_tokens
495
+
496
+ self._g_shift_to = tuple(int(0.5 * x / p) for x, p in zip(ms, ps))
497
+ self._g_shift_from = tuple(-int(0.5 * x / p) for x, p in zip(ms, ps))
498
+
499
+ # Define the attention masks for the shifted MaxViT.
500
+ nglobal = global_shape[0] * global_shape[1]
501
+ nlocal = local_shape[0] * local_shape[1] + self._n_context_tokens # "+ 1" for leadtime
502
+
503
+ lm = torch.ones((nglobal, 1, nlocal, nlocal), dtype=bool)
504
+ mwidth = int(0.5 * local_shape[1]) * local_shape[0]
505
+ lm[
506
+ : gs[1],
507
+ :,
508
+ self._n_context_tokens : mwidth + self._n_context_tokens,
509
+ self._n_context_tokens : mwidth + self._n_context_tokens,
510
+ ] = False
511
+ self.local_mask = lm
512
+
513
+ gm = torch.ones((nlocal, 1, nglobal, nglobal), dtype=bool)
514
+ gm[: int(0.5 * ls[1]) * ls[0], :, : gs[1], : gs[1]] = False
515
+ self.global_mask = gm
516
+
517
+ def _to_grid_global(self, x: Tensor) -> Tensor:
518
+ """
519
+ Shuffle and reshape the data from the global/local setting back to the
520
+ lat/lon grid setting
521
+ Args:
522
+ x: the data tensor to be shuffled.
523
+ Returns:
524
+ x: data in the global/local setting
525
+ """
526
+ nbatch, *other = x.shape
527
+
528
+ y1 = x.view(nbatch, *self._g_shape, *self._l_shape, -1)
529
+ y2 = y1.permute(0, 5, 1, 3, 2, 4).contiguous()
530
+
531
+ s = y2.shape
532
+ return y2.view((nbatch, -1, s[2] * s[3], s[4] * s[5]))
533
+
534
+ def _to_grid_local(self, x: Tensor) -> Tensor:
535
+ """
536
+ Shuffle and reshape the data from the local/global setting to the
537
+ lat/lon grid setting
538
+ Args:
539
+ x: the data tensor to be shuffled.
540
+ Returns:
541
+ x: data in the lat/lon setting.
542
+ """
543
+ x = x.transpose(2, 1).contiguous()
544
+ return self._to_grid_global(x)
545
+
546
+ def _from_grid_global(self, x: Tensor) -> Tensor:
547
+ """
548
+ Shuffle and reshape the data from the lat/lon grid to the global/local
549
+ setting
550
+ Args:
551
+ x: the data tensor to be shuffled.
552
+ Returns:
553
+ x: data in the global/local setting
554
+ """
555
+ nbatch, *other = x.shape
556
+
557
+ z1 = x.view(nbatch, -1, *self._lat_patch)
558
+ z2 = z1.permute(0, 2, 4, 3, 5, 1).contiguous()
559
+
560
+ s = z2.shape
561
+ return z2.view(nbatch, s[1] * s[2], s[3] * s[4], -1)
562
+
563
+ def _from_grid_local(self, x: Tensor) -> Tensor:
564
+ """
565
+ Shuffle and reshape the data from the lat/lon grid to the local/global
566
+ setting
567
+ Args:
568
+ x: the data tensor to be shuffled.
569
+ Returns:
570
+ x: data in the local/global setting
571
+ """
572
+ x = self._from_grid_global(x)
573
+ return x.transpose(2, 1).contiguous()
574
+
575
+ def _shift(self, x: Tensor) -> Tensor:
576
+ """
577
+ Shifts data in the gridded lat/lon setting by half the mask unit shape
578
+ Args:
579
+ x: data to be shifted
580
+ Returns:
581
+ x: either the hsifted or unshifted data
582
+ """
583
+ shift = self._g_shift_from if self._shifted else self._g_shift_to
584
+ x_shifted = torch.roll(x, shift, (-2, -1))
585
+
586
+ self._shifted = not self._shifted
587
+ return x_shifted
588
+
589
+ def _sep_lt(self, x: Tensor) -> tuple[Tensor, Tensor]:
590
+ """
591
+ Seperate off the leadtime from the local patches
592
+ Args:
593
+ x: data to have leadtime removed from
594
+ Returns:
595
+ lt: leadtime
596
+ x: data without the lead time in the local patch
597
+ """
598
+ lt_it = x[:, : self._n_context_tokens, :, :]
599
+ x_stripped = x[:, self._n_context_tokens :, :, :]
600
+
601
+ return lt_it, x_stripped
602
+
603
+ def forward(self, data: Tensor) -> tuple[Tensor, Tensor]:
604
+ """
605
+ Shift or unshift the the data depending on whether the data is already
606
+ shifted, as defined by self._shifted
607
+ Args:
608
+ data: data to be shifted
609
+ Returns:
610
+
611
+ """
612
+ lt, x = self._sep_lt(data)
613
+
614
+ if self.local_mask.device != x.device:
615
+ self.local_mask = self.local_mask.to(device=x.device)
616
+ if self.global_mask.device != x.device:
617
+ self.global_mask = self.global_mask.to(device=x.device)
618
+
619
+ x_grid = self._to_grid_local(x)
620
+ x_shifted = self._shift(x_grid)
621
+ x_patched = self._from_grid_local(x_shifted)
622
+
623
+ # Mask has to be repeated based on batch size
624
+ n_batch = x_grid.shape[0]
625
+ local_rep = [n_batch] + [1] * (self.local_mask.ndim - 1)
626
+ global_rep = [n_batch] + [1] * (self.global_mask.ndim - 1)
627
+
628
+ if self._shifted:
629
+ attn_mask = {
630
+ True: self.local_mask.repeat(local_rep),
631
+ False: self.global_mask.repeat(global_rep),
632
+ }
633
+ else:
634
+ attn_mask = {True: None, False: None}
635
+
636
+ return torch.cat((lt, x_patched), axis=1), attn_mask
637
+
638
+ class LocalGlobalLocalBlock(nn.Module):
639
+ """
640
+ Applies alternating block and grid attention. Given a parameter n_blocks, the entire
641
+ module contains 2*n_blocks+1 transformer blocks. The first, third, ..., last apply
642
+ local (block) attention. The second, fourth, ... global (grid) attention.
643
+
644
+ This is heavily inspired by Tu et al. "MaxViT: Multi-Axis Vision Transformer"
645
+ (https://arxiv.org/abs/2204.01697).
646
+ """
647
+
648
+ def __init__(
649
+ self,
650
+ features: int,
651
+ mlp_multiplier: int,
652
+ n_heads: int,
653
+ dropout: float,
654
+ n_blocks: int,
655
+ drop_path: float,
656
+ shifter: nn.Module | None = None,
657
+ checkpoint: list[int]=[],
658
+ ) -> None:
659
+ """
660
+ Args:
661
+ features: Number of features for inputs to the layer.
662
+ mlp_multiplier: Model will use features*mlp_multiplier hidden units.
663
+ n_heads: Number of attention heads. Should be a factor of features.
664
+ (I.e. the layer uses features // n_heads.)
665
+ dropout: Dropout.
666
+ drop_path: DropPath.
667
+ n_blocks: Number of local-global transformer pairs.
668
+ """
669
+ super().__init__()
670
+
671
+ self.features = features
672
+ self.mlp_multiplier = mlp_multiplier
673
+ self.n_heads = n_heads
674
+ self.dropout = dropout
675
+ self.drop_path = drop_path
676
+ self.n_blocks = n_blocks
677
+ self._checkpoint = checkpoint
678
+
679
+ if len(checkpoint) > 0:
680
+ if min(checkpoint) < 0 or max(checkpoint) >= 2 * n_blocks + 1:
681
+ raise ValueError(f'Checkpoints should satisfy 0 <= i < 2*n_blocks+1. We have {checkpoint}.')
682
+
683
+ self.transformers = nn.ModuleList(
684
+ [
685
+ Transformer(
686
+ features=features,
687
+ mlp_multiplier=mlp_multiplier,
688
+ n_heads=n_heads,
689
+ dropout=dropout,
690
+ drop_path=drop_path,
691
+ )
692
+ for _ in range(2 * n_blocks + 1)
693
+ ]
694
+ )
695
+
696
+ self.evaluator = [
697
+ self._checkpoint_wrapper if i in checkpoint else lambda m, x : m(x)
698
+ for i, _ in enumerate(self.transformers)
699
+ ]
700
+
701
+ self.shifter = shifter or _Shift()
702
+
703
+ @staticmethod
704
+ def _checkpoint_wrapper(model, data):
705
+ return checkpoint(model, data, use_reentrant=False)
706
+
707
+ def forward(self, x: Tensor) -> Tensor:
708
+ """
709
+ Args:
710
+ x: Tensor of shape [batch, global_sequence, local_sequence, features]
711
+ Returns:
712
+ Tensor of shape [batch, global_sequence, local_sequence, features]
713
+ """
714
+ if x.shape[-1] != self.features:
715
+ raise ValueError(
716
+ f"Expecting tensor with last dimension of size {self.features}."
717
+ )
718
+ if x.ndim != 4:
719
+ raise ValueError(
720
+ f"Expecting tensor with exactly four dimensions. Input has shape {x.shape}."
721
+ )
722
+
723
+ self.shifter.reset()
724
+ local: bool = True
725
+ attn_mask = {True: None, False: None}
726
+
727
+ transformer_iter = zip(self.evaluator, self.transformers)
728
+
729
+ # First local block
730
+ evaluator, transformer = next(transformer_iter)
731
+ x = evaluator(transformer, (x, attn_mask[local]))
732
+
733
+ for evaluator, transformer in transformer_iter:
734
+ local = not local
735
+ # We are making exactly 2*n_blocks transposes.
736
+ # So the output has the same shape as input.
737
+ x = x.transpose(1, 2)
738
+
739
+ x = evaluator(transformer, (x, attn_mask[local]))
740
+
741
+ if not local:
742
+ x, attn_mask = self.shifter(x)
743
+
744
+ return x
745
+
746
+
747
+ class PatchEmbed(nn.Module):
748
+ """
749
+ Patch embedding via 2D convolution.
750
+ """
751
+
752
+ def __init__(
753
+ self, patch_size: int | tuple[int, ...], channels: int, embed_dim: int
754
+ ):
755
+ super().__init__()
756
+
757
+ self.patch_size = patch_size
758
+ self.channels = channels
759
+ self.embed_dim = embed_dim
760
+
761
+ self.proj = nn.Conv2d(
762
+ channels, embed_dim, kernel_size=patch_size, stride=patch_size, bias=True
763
+ )
764
+
765
+ def forward(self, x: Tensor) -> Tensor:
766
+ """
767
+ Args:
768
+ x: Tensor of shape [batch, channels, lat, lon].
769
+ Returns:
770
+ Tensor with shape [batch, embed_dim, lat//patch_size, lon//patch_size]
771
+ """
772
+
773
+ H, W = x.shape[-2:]
774
+
775
+ if W % self.patch_size[1] != 0:
776
+ raise ValueError(
777
+ f"Cannot do patch embedding for tensor of shape {x.size()}"
778
+ " with patch size {self.patch_size}. (Dimensions are BSCHW.)"
779
+ )
780
+ if H % self.patch_size[0] != 0:
781
+ raise ValueError(
782
+ f"Cannot do patch embedding for tensor of shape {x.size()}"
783
+ f" with patch size {self.patch_size}. (Dimensions are BSCHW.)"
784
+ )
785
+
786
+ x = self.proj(x)
787
+
788
+ return x
789
+
790
+
791
+ class PrithviWxCEncoderDecoder(nn.Module):
792
+ """
793
+ Hiera-MaxViT encoder/decoder code.
794
+ """
795
+
796
+ def __init__(
797
+ self,
798
+ embed_dim: int,
799
+ n_blocks: int,
800
+ mlp_multiplier: float,
801
+ n_heads: int,
802
+ dropout: float,
803
+ drop_path: float,
804
+ shifter: nn.Module | None = None,
805
+ transformer_cp: list[int]=[],
806
+ ) -> None:
807
+ """
808
+ Args:
809
+ embed_dim: Embedding dimension
810
+ n_blocks: Number of local-global transformer pairs.
811
+ mlp_multiplier: MLP multiplier for hidden features in feed forward
812
+ networks.
813
+ n_heads: Number of attention heads.
814
+ dropout: Dropout.
815
+ drop_path: DropPath.
816
+ """
817
+ super().__init__()
818
+
819
+ self.embed_dim = embed_dim
820
+ self.n_blocks = n_blocks
821
+ self.mlp_multiplier = mlp_multiplier
822
+ self.n_heads = n_heads
823
+ self.dropout = dropout
824
+ self._transformer_cp = transformer_cp
825
+
826
+ self.lgl_block = LocalGlobalLocalBlock(
827
+ features=embed_dim,
828
+ mlp_multiplier=mlp_multiplier,
829
+ n_heads=n_heads,
830
+ dropout=dropout,
831
+ drop_path=drop_path,
832
+ n_blocks=n_blocks,
833
+ shifter=shifter,
834
+ checkpoint=transformer_cp,
835
+ )
836
+
837
+ def forward(
838
+ self,
839
+ x: torch.Tensor
840
+ ) -> torch.Tensor:
841
+ """
842
+ Args:
843
+ x: Tensor of shape [batch, global sequence, local sequence, embed_dim]
844
+ Returns:
845
+ Tensor of shape [batch, mask_unit_sequence, local_sequence, embed_dim].
846
+ Identical in shape to the input x.
847
+ """
848
+
849
+ x = self.lgl_block(x)
850
+
851
+ return x
852
+
853
+
854
+ class PrithviWxC(nn.Module):
855
+ """
856
+ Encoder-decoder fusing Hiera with MaxViT. See
857
+ - Ryali et al. "Hiera: A Hierarchical Vision Transformer without the
858
+ Bells-and-Whistles" (https://arxiv.org/abs/2306.00989)
859
+ - Tu et al. "MaxViT: Multi-Axis Vision Transformer"
860
+ (https://arxiv.org/abs/2204.01697)
861
+ """
862
+
863
+ def __init__(
864
+ self,
865
+ in_channels: int,
866
+ input_size_time: int,
867
+ in_channels_static: int,
868
+ input_scalers_mu: Tensor,
869
+ input_scalers_sigma: Tensor,
870
+ input_scalers_epsilon: float,
871
+ static_input_scalers_mu: Tensor,
872
+ static_input_scalers_sigma: Tensor,
873
+ static_input_scalers_epsilon: float,
874
+ output_scalers: Tensor,
875
+ n_lats_px: int,
876
+ n_lons_px: int,
877
+ patch_size_px: tuple[int],
878
+ mask_unit_size_px: tuple[int],
879
+ mask_ratio_inputs: float,
880
+ mask_ratio_targets: float,
881
+ embed_dim: int,
882
+ n_blocks_encoder: int,
883
+ n_blocks_decoder: int,
884
+ mlp_multiplier: float,
885
+ n_heads: int,
886
+ dropout: float,
887
+ drop_path: float,
888
+ parameter_dropout: float,
889
+ residual: str,
890
+ masking_mode: str,
891
+ positional_encoding: str,
892
+ encoder_shifting: bool = False,
893
+ decoder_shifting: bool = False,
894
+ checkpoint_encoder: list[int]=[],
895
+ checkpoint_decoder: list[int]=[],
896
+ ) -> None:
897
+ """
898
+ Args:
899
+ in_channels: Number of input channels.
900
+ input_size_time: Number of timestamps in input.
901
+ in_channels_static: Number of input channels for static data.
902
+ input_scalers_mu: Tensor of size (in_channels,). Used to rescale
903
+ input.
904
+ input_scalers_sigma: Tensor of size (in_channels,). Used to rescale
905
+ input.
906
+ input_scalers_epsilon: Float. Used to rescale input.
907
+ static_input_scalers_mu: Tensor of size (in_channels_static). Used
908
+ to rescale static inputs.
909
+ static_input_scalers_sigma: Tensor of size (in_channels_static).
910
+ Used to rescale static inputs.
911
+ static_input_scalers_epsilon: Float. Used to rescale static inputs.
912
+ output_scalers: Tensor of shape (in_channels,). Used to rescale
913
+ output.
914
+ n_lats_px: Total latitudes in data. In pixels.
915
+ n_lons_px: Total longitudes in data. In pixels.
916
+ patch_size_px: Patch size for tokenization. In pixels lat/lon.
917
+ mask_unit_size_px: Size of each mask unit. In pixels lat/lon.
918
+ mask_ratio_inputs: Masking ratio for inputs. 0 to 1.
919
+ mask_ratio_targets: Masking ratio for targets. 0 to 1.
920
+ embed_dim: Embedding dimension
921
+ n_blocks_encoder: Number of local-global transformer pairs in
922
+ encoder.
923
+ n_blocks_decoder: Number of local-global transformer pairs in
924
+ decoder.
925
+ mlp_multiplier: MLP multiplier for hidden features in feed forward
926
+ networks.
927
+ n_heads: Number of attention heads.
928
+ dropout: Dropout.
929
+ drop_path: DropPath.
930
+ parameter_dropout: Dropout applied to parameters.
931
+ residual: Indicates whether and how model should work as residual
932
+ model. Accepted values are 'climate', 'temporal' and 'none'
933
+ positional_encoding: possible values are ['absolute' (default), 'fourier'].
934
+ 'absolute' lat lon encoded in 3 dimensions using sine and cosine
935
+ 'fourier' lat/lon to be encoded using various frequencies
936
+ masking_mode: String ['local', 'global', 'both'] that controls the
937
+ type of masking used.
938
+ checkpoint_encoder: List of integers controlling if gradient checkpointing is used on encoder.
939
+ Format: [] for no gradient checkpointing. [3, 7] for checkpointing after 4th and 8th layer etc.
940
+ checkpoint_decoder: List of integers controlling if gradient checkpointing is used on decoder.
941
+ Format: See `checkpoint_encoder`.
942
+ masking_mode: The type of masking to use {'global', 'local', 'both'}
943
+ encoder_shifting: Whether to use swin shifting in the encoder.
944
+ decoder_shifting: Whether to use swin shifting in the decoder.
945
+ """
946
+ super().__init__()
947
+
948
+ if mask_ratio_targets > 0.0:
949
+ raise NotImplementedError("Target masking is not implemented.")
950
+
951
+ self.in_channels = in_channels
952
+ self.input_size_time = input_size_time
953
+ self.in_channels_static = in_channels_static
954
+ self.n_lats_px = n_lats_px
955
+ self.n_lons_px = n_lons_px
956
+ self.patch_size_px = patch_size_px
957
+ self.mask_unit_size_px = mask_unit_size_px
958
+ self.mask_ratio_inputs = mask_ratio_inputs
959
+ self.mask_ratio_targets = mask_ratio_targets
960
+ self.embed_dim = embed_dim
961
+ self.n_blocks_encoder = n_blocks_encoder
962
+ self.n_blocks_decoder = n_blocks_decoder
963
+ self.mlp_multiplier = mlp_multiplier
964
+ self.n_heads = n_heads
965
+ self.dropout = dropout
966
+ self.drop_path = drop_path
967
+ self.residual = residual
968
+ self._encoder_shift = encoder_shifting
969
+ self._decoder_shift = decoder_shifting
970
+ self.positional_encoding = positional_encoding
971
+ self._checkpoint_encoder = checkpoint_encoder
972
+ self._checkpoint_decoder = checkpoint_decoder
973
+
974
+ assert self.n_lats_px % self.mask_unit_size_px[0] == 0
975
+ assert self.n_lons_px % self.mask_unit_size_px[1] == 0
976
+ assert self.mask_unit_size_px[0] % self.patch_size_px[0] == 0
977
+ assert self.mask_unit_size_px[1] % self.patch_size_px[1] == 0
978
+
979
+ if self.patch_size_px[0] != self.patch_size_px[1]:
980
+ raise NotImplementedError(
981
+ "Current pixel shuffle implementation assumes same patch size along both dimensions."
982
+ )
983
+
984
+ self.local_shape_mu = (
985
+ self.mask_unit_size_px[0] // self.patch_size_px[0],
986
+ self.mask_unit_size_px[1] // self.patch_size_px[1],
987
+ )
988
+ self.global_shape_mu = (
989
+ self.n_lats_px // self.mask_unit_size_px[0],
990
+ self.n_lons_px // self.mask_unit_size_px[1],
991
+ )
992
+
993
+ assert input_scalers_mu.shape == (in_channels,)
994
+ assert input_scalers_sigma.shape == (in_channels,)
995
+ assert output_scalers.shape == (in_channels,)
996
+
997
+ if self.positional_encoding != 'fourier':
998
+ assert static_input_scalers_mu.shape == (in_channels_static,)
999
+ assert static_input_scalers_sigma.shape == (in_channels_static,)
1000
+
1001
+ # Input shape [batch, time, parameter, lat, lon]
1002
+ self.input_scalers_epsilon = input_scalers_epsilon
1003
+ self.register_buffer('input_scalers_mu', input_scalers_mu.reshape(1, 1, -1, 1, 1))
1004
+ self.register_buffer('input_scalers_sigma', input_scalers_sigma.reshape(1, 1, -1, 1, 1))
1005
+
1006
+ # Static inputs shape [batch, parameter, lat, lon]
1007
+ self.static_input_scalers_epsilon = static_input_scalers_epsilon
1008
+ self.register_buffer('static_input_scalers_mu', static_input_scalers_mu.reshape(1, -1, 1, 1))
1009
+ self.register_buffer('static_input_scalers_sigma', static_input_scalers_sigma.reshape(1, -1, 1, 1))
1010
+
1011
+ # Output shape [batch, parameter, lat, lon]
1012
+ self.register_buffer('output_scalers', output_scalers.reshape(1, -1, 1, 1))
1013
+
1014
+ self.parameter_dropout = nn.Dropout2d(p=parameter_dropout)
1015
+
1016
+ self.patch_embedding = PatchEmbed(
1017
+ patch_size=patch_size_px,
1018
+ channels=in_channels * input_size_time,
1019
+ embed_dim=embed_dim,
1020
+ )
1021
+
1022
+ if self.residual == "climate":
1023
+ self.patch_embedding_static = PatchEmbed(
1024
+ patch_size=patch_size_px,
1025
+ channels=in_channels + in_channels_static,
1026
+ embed_dim=embed_dim,
1027
+ )
1028
+ else:
1029
+ self.patch_embedding_static = PatchEmbed(
1030
+ patch_size=patch_size_px,
1031
+ channels=in_channels_static,
1032
+ embed_dim=embed_dim,
1033
+ )
1034
+
1035
+ self.input_time_embedding = nn.Linear(1, embed_dim//4, bias=True)
1036
+ self.lead_time_embedding = nn.Linear(1, embed_dim//4, bias=True)
1037
+
1038
+ self.mask_token = nn.Parameter(torch.randn(1, 1, 1, self.embed_dim))
1039
+ self._nglobal_mu = np.prod(self.global_shape_mu)
1040
+ self._global_idx = torch.arange(self._nglobal_mu)
1041
+
1042
+ self._nlocal_mu = np.prod(self.local_shape_mu)
1043
+ self._local_idx = torch.arange(self._nlocal_mu)
1044
+
1045
+ if self._encoder_shift:
1046
+ self.encoder_shifter = e_shifter = SWINShiftNoBuffer(
1047
+ self.mask_unit_size_px,
1048
+ self.global_shape_mu,
1049
+ self.local_shape_mu,
1050
+ self.patch_size_px,
1051
+ n_context_tokens=0,
1052
+ )
1053
+ else:
1054
+ self.encoder_shifter = e_shifter = None
1055
+ self.encoder = PrithviWxCEncoderDecoder(
1056
+ embed_dim=embed_dim,
1057
+ n_blocks=n_blocks_encoder,
1058
+ mlp_multiplier=mlp_multiplier,
1059
+ n_heads=n_heads,
1060
+ dropout=dropout,
1061
+ drop_path=drop_path,
1062
+ shifter=e_shifter,
1063
+ transformer_cp=checkpoint_encoder,
1064
+ )
1065
+
1066
+ if n_blocks_decoder != 0:
1067
+ if self._decoder_shift:
1068
+ self.decoder_shifter = d_shifter = SWINShift(
1069
+ self.mask_unit_size_px,
1070
+ self.global_shape_mu,
1071
+ self.local_shape_mu,
1072
+ self.patch_size_px,
1073
+ n_context_tokens=0,
1074
+ )
1075
+ else:
1076
+ self.decoder_shifter = d_shifter = None
1077
+
1078
+ self.decoder = PrithviWxCEncoderDecoder(
1079
+ embed_dim=embed_dim,
1080
+ n_blocks=n_blocks_decoder,
1081
+ mlp_multiplier=mlp_multiplier,
1082
+ n_heads=n_heads,
1083
+ dropout=dropout,
1084
+ drop_path=0.,
1085
+ shifter=d_shifter,
1086
+ transformer_cp=checkpoint_decoder,
1087
+ )
1088
+
1089
+ self.unembed = nn.Linear(
1090
+ self.embed_dim,
1091
+ self.in_channels * self.patch_size_px[0] * self.patch_size_px[1],
1092
+ bias=True,
1093
+ )
1094
+
1095
+ self.masking_mode = masking_mode.lower()
1096
+ match self.masking_mode:
1097
+ case "local":
1098
+ self.generate_mask = self._gen_mask_local
1099
+ case "global":
1100
+ self.generate_mask = self._gen_mask_global
1101
+ case "both":
1102
+ self._mask_both_local: bool = True
1103
+ self.generate_mask = self._gen_mask_both
1104
+ case _:
1105
+ raise ValueError(f"Masking mode '{masking_mode}' not supported")
1106
+
1107
+ def swap_masking(self) -> None:
1108
+ if hasattr(self, '_mask_both_local'):
1109
+ self._mask_both_local = not self._mask_both_local
1110
+
1111
+ @cached_property
1112
+ def n_masked_global(self):
1113
+ return int(self.mask_ratio_inputs * np.prod(self.global_shape_mu))
1114
+
1115
+ @cached_property
1116
+ def n_masked_local(self):
1117
+ return int(self.mask_ratio_inputs * np.prod(self.local_shape_mu))
1118
+
1119
+ @staticmethod
1120
+ def _shuffle_along_axis(a, axis):
1121
+ # https://stackoverflow.com/questions/5040797/shuffling-numpy-array-along-a-given-axis
1122
+ idx = torch.argsort(input=torch.rand(*a.shape), dim=axis)
1123
+ return torch.gather(a, dim=axis, index=idx)
1124
+
1125
+ def _gen_mask_local(self, sizes: tuple[int]) -> tuple[Tensor]:
1126
+ """
1127
+ Args:
1128
+ batch_size: Number of elements in batch
1129
+ Returns:
1130
+ Tuple of torch tensors. [indices masked, indices unmasked].
1131
+ Each of these is a tensor of shape (batch, global sequene)
1132
+ """
1133
+ # We identifies which indices (values) should be masked
1134
+
1135
+ maskable_indices = self._local_idx.view(1, -1).expand(*sizes[:2], -1)
1136
+
1137
+ maskable_indices = self._shuffle_along_axis(maskable_indices, 2)
1138
+
1139
+ # `...` cannot be jit'd :-(
1140
+ indices_masked = maskable_indices[:, :, : self.n_masked_local]
1141
+ indices_unmasked = maskable_indices[:, :, self.n_masked_local :]
1142
+
1143
+ return indices_masked, indices_unmasked
1144
+
1145
+ def _gen_mask_global(self, sizes: tuple[int]) -> tuple[Tensor]:
1146
+ """
1147
+ Args:
1148
+ batch_size: Number of elements in batch
1149
+ Returns:
1150
+ Tuple of torch tensors. [indices masked, indices unmasked].
1151
+ Each of these is a tensor of shape (batch, global sequene)
1152
+ """
1153
+ # We identifies which indices (values) should be masked
1154
+
1155
+ maskable_indices = self._global_idx.view(1, -1).expand(*sizes[:1], -1)
1156
+
1157
+ maskable_indices = self._shuffle_along_axis(maskable_indices, 1)
1158
+
1159
+ indices_masked = maskable_indices[:, : self.n_masked_global]
1160
+ indices_unmasked = maskable_indices[:, self.n_masked_global :]
1161
+
1162
+ return indices_masked, indices_unmasked
1163
+
1164
+ def _gen_mask_both(self, sizes: tuple[int]) -> tuple[Tensor]:
1165
+ if self._mask_both_local:
1166
+ return self._gen_mask_local(sizes)
1167
+ else:
1168
+ return self._gen_mask_global(sizes)
1169
+
1170
+ @staticmethod
1171
+ def reconstruct_batch(
1172
+ idx_masked: Tensor,
1173
+ idx_unmasked: Tensor,
1174
+ data_masked: Tensor,
1175
+ data_unmasked: Tensor,
1176
+ ) -> Tensor:
1177
+ """
1178
+ Reconstructs a tensor along the mask unit dimension. Batched version.
1179
+
1180
+ Args:
1181
+ idx_masked: Tensor of shape `batch, mask unit sequence`.
1182
+ idx_unmasked: Tensor of shape `batch, mask unit sequence`.
1183
+ data_masked: Tensor of shape `batch, mask unit sequence, ...`.
1184
+ Should have same size along mask unit sequence dimension as
1185
+ idx_masked. Dimensions beyond the first two, marked here as ...
1186
+ will typically be `local_sequence, channel` or `channel, lat, lon`.
1187
+ These dimensions should agree with data_unmasked.
1188
+ data_unmasked: Tensor of shape `batch, mask unit sequence, ...`.
1189
+ Should have same size along mask unit sequence dimension as
1190
+ idx_unmasked. Dimensions beyond the first two, marked here as
1191
+ ... will typically be `local_sequence, channel` or `channel,
1192
+ lat, lon`. These dimensions should agree with data_masked.
1193
+ Returns:
1194
+ Tensor of same shape as inputs data_masked and data_unmasked. I.e.
1195
+ `batch, mask unit sequence, ...`. Index for the total data composed
1196
+ of the masked and the unmasked part
1197
+ """
1198
+ dim: int = idx_masked.ndim
1199
+
1200
+ idx_total = torch.argsort(torch.cat([idx_masked, idx_unmasked], dim=-1), dim=-1)
1201
+ idx_total = idx_total.view(*idx_total.shape, *[1] * (data_unmasked.ndim - dim))
1202
+ idx_total = idx_total.expand(*idx_total.shape[:dim], *data_unmasked.shape[dim:])
1203
+
1204
+ data = torch.cat([data_masked, data_unmasked], dim=dim - 1)
1205
+ data = torch.gather(data, dim=dim - 1, index=idx_total)
1206
+
1207
+ return data, idx_total
1208
+
1209
+ def fourier_pos_encoding(self, x_static):
1210
+ """
1211
+ Args
1212
+ x_static: B x C x H x W. first two channels are lat, and lon respectively
1213
+ Returns
1214
+ Tensor of shape B x E x H x W where E is the embedding dimension.
1215
+ """
1216
+
1217
+ # B x C x H x W -> B x 1 x H/P x W/P
1218
+ latitudes_patch = F.avg_pool2d(x_static[:, [0]], kernel_size=self.patch_size_px, stride=self.patch_size_px)
1219
+ longitudes_patch = F.avg_pool2d(x_static[:, [1]], kernel_size=self.patch_size_px, stride=self.patch_size_px)
1220
+
1221
+ modes = torch.arange(self.embed_dim//4, device=x_static.device).view(1, -1, 1, 1) + 1.
1222
+ pos_encoding = torch.cat(
1223
+ (
1224
+ torch.sin(latitudes_patch*modes),
1225
+ torch.sin(longitudes_patch*modes),
1226
+ torch.cos(latitudes_patch*modes),
1227
+ torch.cos(longitudes_patch*modes),
1228
+ ),
1229
+ axis=1
1230
+ )
1231
+
1232
+ return pos_encoding # B x E x H/P x W/P
1233
+
1234
+ def time_encoding(self, input_time, lead_time):
1235
+ '''
1236
+ Args:
1237
+ input_time: Tensor of shape [batch].
1238
+ lead_time: Tensor of shape [batch].
1239
+ Returns:
1240
+ Tensor of shape [batch, embed_dim, 1, 1]
1241
+ '''
1242
+ input_time = self.input_time_embedding(input_time.view(-1, 1, 1, 1))
1243
+ lead_time = self.lead_time_embedding(lead_time.view(-1, 1, 1, 1))
1244
+
1245
+ time_encoding = torch.cat(
1246
+ (
1247
+ torch.cos(input_time),
1248
+ torch.cos(lead_time),
1249
+ torch.sin(input_time),
1250
+ torch.sin(lead_time),
1251
+ ),
1252
+ axis=3
1253
+ )
1254
+ return time_encoding
1255
+
1256
+ def to_patching(self, x: Tensor) -> Tensor:
1257
+ """Transform data from lat/lon space to two axis patching
1258
+
1259
+ Args: ->
1260
+ x: Tesnor in lat/lon space (N, C, Nlat//P_0, Nlon//P_1)
1261
+
1262
+ Returns:
1263
+ Tensor in patch space (N, G, L, C)
1264
+ """
1265
+ n_batch = x.shape[0]
1266
+
1267
+ x = x.view(
1268
+ n_batch,
1269
+ self.embed_dim,
1270
+ self.global_shape_mu[0],
1271
+ self.local_shape_mu[0],
1272
+ self.global_shape_mu[1],
1273
+ self.local_shape_mu[1],
1274
+ )
1275
+ x = x.permute(0, 2, 4, 3, 5, 1).contiguous()
1276
+
1277
+ s = x.shape
1278
+ return x.view(n_batch, s[1] * s[2], s[3] * s[4], -1)
1279
+
1280
+ def from_patching(self, x: Tensor) -> Tensor:
1281
+ """Transform data from two axis patching to lat/lon space
1282
+
1283
+ Args:
1284
+ x: Tensor in patch space with shape (N, G, L, C*P_0*P_1)
1285
+
1286
+ Returns:
1287
+ Tensor in lat/lon space (N, C*P_0*P_1, Nlat//P_0, Nlon // P_1)
1288
+ """
1289
+ n_batch = x.shape[0]
1290
+
1291
+ x = x.view(
1292
+ n_batch,
1293
+ self.global_shape_mu[0],
1294
+ self.global_shape_mu[1],
1295
+ self.local_shape_mu[0],
1296
+ self.local_shape_mu[1],
1297
+ -1,
1298
+ )
1299
+ x = x.permute(0, 5, 1, 3, 2, 4).contiguous()
1300
+
1301
+ s = x.shape
1302
+ return x.view(n_batch, -1, s[2]*s[3], s[4]*s[5])
1303
+
1304
+ def forward(self, batch: dict[str, torch.Tensor]) -> torch.Tensor:
1305
+ """
1306
+ Args:
1307
+ batch: Dictionary containing the keys 'x', 'y', 'input_time',
1308
+ 'lead_time' and 'static'. The associated torch tensors have the
1309
+ following shapes:
1310
+ x: Tensor of shape [batch, time, parameter, lat, lon]
1311
+ y: Tensor of shape [batch, parameter, lat, lon]
1312
+ static: Tensor of shape [batch, channel_static, lat, lon]
1313
+ climate: Optional tensor of shape [batch, parameter, lat, lon]
1314
+ input_time: Tensor of shape [batch]. Or none.
1315
+ lead_time: Tensor of shape [batch]. Or none.
1316
+ Returns:
1317
+ Tensor of shape [batch, parameter, lat, lon].
1318
+ """
1319
+ assert batch["x"].shape[2] == self.in_channels
1320
+ assert batch["x"].shape[3] == self.n_lats_px
1321
+ assert batch["x"].shape[4] == self.n_lons_px
1322
+ assert batch["y"].shape[1] == self.in_channels
1323
+ assert batch["y"].shape[2] == self.n_lats_px
1324
+ assert batch["y"].shape[3] == self.n_lons_px
1325
+ if self.positional_encoding == 'fourier':
1326
+ # the first two features (lat, lon) are encoded separately
1327
+ assert batch['static'].shape[1] - 2 == self.in_channels_static, "When setting self.positional_encoding to fourier, the number of static params change in the dataset. So, in the config, reduce num_static_channels (e.g., 4 instead of 7)."
1328
+ else:
1329
+ assert batch['static'].shape[1] == self.in_channels_static
1330
+ assert batch["static"].shape[2] == self.n_lats_px
1331
+ assert batch["static"].shape[3] == self.n_lons_px
1332
+
1333
+ x_rescaled = (batch["x"] - self.input_scalers_mu) / (
1334
+ self.input_scalers_sigma + self.input_scalers_epsilon
1335
+ )
1336
+ batch_size = x_rescaled.shape[0]
1337
+
1338
+ if self.positional_encoding == 'fourier':
1339
+ x_static_pos = self.fourier_pos_encoding(batch['static']) # B, embed_dim, lat / patch_size, lon / patch_size
1340
+ x_static = (batch['static'][:, 2:] - self.static_input_scalers_mu[:, 3:]) / ( # The first two channels in batch['static'] are used in positional encoding
1341
+ self.static_input_scalers_sigma[:, 3:] + self.static_input_scalers_epsilon # This translates to the first three channels in 'static_input_scalers_mu'
1342
+ )
1343
+ else:
1344
+ x_static = (batch["static"] - self.static_input_scalers_mu) / (
1345
+ self.static_input_scalers_sigma + self.static_input_scalers_epsilon
1346
+ )
1347
+
1348
+ if self.residual == "temporal":
1349
+ # We create a residual of same shape as y
1350
+ index = torch.where(batch["lead_time"] > 0, batch["x"].shape[1] - 1, 0)
1351
+ index = index.view(-1, 1, 1, 1, 1)
1352
+ index = index.expand(batch_size, 1, *batch["x"].shape[2:])
1353
+ x_hat = torch.gather(batch["x"], dim=1, index=index)
1354
+ x_hat = x_hat.squeeze(1)
1355
+ assert (
1356
+ batch["y"].shape == x_hat.shape
1357
+ ), f'Shapes {batch["y"].shape} and {x_hat.shape} do not agree.'
1358
+ elif self.residual == "climate":
1359
+ climate_scaled = (
1360
+ batch["climate"] - self.input_scalers_mu.view(1, -1, 1, 1)
1361
+ ) / (
1362
+ self.input_scalers_sigma.view(1, -1, 1, 1) + self.input_scalers_epsilon
1363
+ )
1364
+
1365
+ # [batch, time, parameter, lat, lon] -> [batch, time x parameter, lat, lon]
1366
+ x_rescaled = x_rescaled.flatten(1, 2)
1367
+ # Parameter dropout
1368
+ x_rescaled = self.parameter_dropout(x_rescaled)
1369
+
1370
+ x_embedded = self.patch_embedding(x_rescaled)
1371
+ assert x_embedded.shape[1] == self.embed_dim
1372
+
1373
+ if self.residual == "climate":
1374
+ static_embedded = self.patch_embedding_static(
1375
+ torch.cat((x_static, climate_scaled), dim=1)
1376
+ )
1377
+ else:
1378
+ static_embedded = self.patch_embedding_static(x_static)
1379
+ assert static_embedded.shape[1] == self.embed_dim
1380
+
1381
+ if self.positional_encoding == 'fourier':
1382
+ static_embedded += x_static_pos
1383
+
1384
+ x_embedded = self.to_patching(x_embedded)
1385
+ static_embedded = self.to_patching(static_embedded)
1386
+
1387
+ time_encoding = self.time_encoding(batch['input_time'], batch['lead_time'])
1388
+
1389
+ tokens = x_embedded + static_embedded + time_encoding
1390
+
1391
+ # Now we generate masks based on masking_mode
1392
+ indices_masked, indices_unmasked = self.generate_mask(
1393
+ (batch_size, self._nglobal_mu)
1394
+ )
1395
+ indices_masked = indices_masked.to(device=tokens.device)
1396
+ indices_unmasked = indices_unmasked.to(device=tokens.device)
1397
+ maskdim: int = indices_masked.ndim
1398
+
1399
+ # Unmasking
1400
+ unmask_view = (*indices_unmasked.shape, *[1] * (tokens.ndim - maskdim))
1401
+ unmasked = torch.gather(
1402
+ tokens,
1403
+ dim=maskdim - 1,
1404
+ index=indices_unmasked.view(*unmask_view).expand(
1405
+ *indices_unmasked.shape, *tokens.shape[maskdim:]
1406
+ ),
1407
+ )
1408
+
1409
+ # Encoder
1410
+ x_encoded = self.encoder(unmasked)
1411
+
1412
+ # Generate and position encode the mask tokens
1413
+ # (1, 1, 1, embed_dim) -> (batch, global_seq_masked, local seq, embed_dim)
1414
+ mask_view = (*indices_masked.shape, *[1] * (tokens.ndim - maskdim))
1415
+ masking = self.mask_token.repeat(*static_embedded.shape[:3], 1)
1416
+ masked = masking + static_embedded
1417
+ masked = torch.gather(
1418
+ masked,
1419
+ dim=maskdim - 1,
1420
+ index=indices_masked.view(*mask_view).expand(
1421
+ *indices_masked.shape, *tokens.shape[maskdim:]
1422
+ ),
1423
+ )
1424
+
1425
+ recon, _ = self.reconstruct_batch(
1426
+ indices_masked, indices_unmasked, masked, x_encoded
1427
+ )
1428
+
1429
+ x_decoded = self.decoder(recon)
1430
+
1431
+ # Output: (batch, global sequence, local sequence, in_channels * patch_size[0] * patch_size[1])
1432
+ x_unembed = self.unembed(x_decoded)
1433
+
1434
+ # Reshape to (batch, global_lat, global_lon, local_lat, local_lon, in_channels * patch_size[0] * patch_size[1])
1435
+ assert x_unembed.shape[0] == batch_size
1436
+ assert x_unembed.shape[1] == self.global_shape_mu[0] * self.global_shape_mu[1]
1437
+ assert x_unembed.shape[2] == self.local_shape_mu[0] * self.local_shape_mu[1]
1438
+ assert (
1439
+ x_unembed.shape[3]
1440
+ == self.in_channels * self.patch_size_px[0] * self.patch_size_px[1]
1441
+ )
1442
+
1443
+ x_out = self.from_patching(x_unembed)
1444
+
1445
+ # Pixel shuffle to (batch, in_channels, lat, lon)
1446
+ x_out = F.pixel_shuffle(x_out, self.patch_size_px[0])
1447
+
1448
+ if self.residual == "temporal":
1449
+ x_out = self.output_scalers * x_out + x_hat
1450
+ elif self.residual == "climate":
1451
+ x_out = self.output_scalers * x_out + batch["climate"]
1452
+ elif self.residual == "none":
1453
+ x_out = self.output_scalers * x_out + self.input_scalers_mu.reshape(
1454
+ 1, -1, 1, 1
1455
+ )
1456
+
1457
+ return x_out
1458
+
1459
+
1460
+ def masked_inference(
1461
+ model,
1462
+ batch: dict[str, torch.Tensor],
1463
+ mask_global: Optional[torch.Tensor] = None,
1464
+ mask_local: Optional[torch.Tensor] = None,
1465
+ ) -> torch.Tensor:
1466
+ """
1467
+ Perform masked inference with Prithvi-WxC model
1468
+
1469
+ Args:
1470
+ model: The PrithviWxC model to perform the inference with.
1471
+ batch: Dictionary containing the keys 'x', 'y', 'input_time',
1472
+ 'lead_time' and 'static'. The associated torch tensors have the
1473
+ following shapes:
1474
+ x: Tensor of shape [batch, time, parameter, lat, lon]
1475
+ y: Tensor of shape [batch, parameter, lat, lon]
1476
+ static: Tensor of shape [batch, channel_static, lat, lon]
1477
+ climate: Optional tensor of shape [batch, parameter, lat, lon]
1478
+ input_time: Tensor of shape [batch]. Or none.
1479
+ lead_time: Tensor of shape [batch]. Or none.
1480
+ mask_global: A 2D tensor of shape [n_lats_px / mask_unit_size_px[0], n_lons_px / patch_size_px[1]]
1481
+ identifying the global patches to mask.
1482
+ mask_local: A 2D tensor of shape [mask_unit_size_px[0] / patch_size_px[0], mask_unit_size_px[1] / patch_size_px[1]]
1483
+ identifying the global patches to mask.
1484
+ Returns:
1485
+ Tensor of shape [batch, parameter, lat, lon].
1486
+ """
1487
+ assert batch["x"].shape[2] == model.in_channels
1488
+ assert batch["x"].shape[3] == model.n_lats_px
1489
+ assert batch["x"].shape[4] == model.n_lons_px
1490
+ assert batch["y"].shape[1] == model.in_channels
1491
+ assert batch["y"].shape[2] == model.n_lats_px
1492
+ assert batch["y"].shape[3] == model.n_lons_px
1493
+ if model.positional_encoding == 'fourier':
1494
+ # the first two features (lat, lon) are encoded separately
1495
+ assert batch['static'].shape[1] - 2 == model.in_channels_static, "When setting model.positional_encoding to fourier, the number of static params change in the dataset. So, in the config, reduce num_static_channels (e.g., 4 instead of 7)."
1496
+ else:
1497
+ assert batch['static'].shape[1] == model.in_channels_static
1498
+ assert batch["static"].shape[2] == model.n_lats_px
1499
+ assert batch["static"].shape[3] == model.n_lons_px
1500
+
1501
+ x_rescaled = (batch["x"] - model.input_scalers_mu) / (
1502
+ model.input_scalers_sigma + model.input_scalers_epsilon
1503
+ )
1504
+ batch_size = x_rescaled.shape[0]
1505
+
1506
+ if model.positional_encoding == 'fourier':
1507
+ x_static_pos = model.fourier_pos_encoding(batch['static']) # B, embed_dim, lat / patch_size, lon / patch_size
1508
+ x_static = (batch['static'][:, 2:] - model.static_input_scalers_mu[:, 3:]) / ( # The first two channels in batch['static'] are used in positional encoding
1509
+ model.static_input_scalers_sigma[:, 3:] + model.static_input_scalers_epsilon # This translates to the first three channels in 'static_input_scalers_mu'
1510
+ )
1511
+ else:
1512
+ x_static = (batch["static"] - model.static_input_scalers_mu) / (
1513
+ model.static_input_scalers_sigma + model.static_input_scalers_epsilon
1514
+ )
1515
+
1516
+ if model.residual == "temporal":
1517
+ # We create a residual of same shape as y
1518
+ index = torch.where(batch["lead_time"] > 0, batch["x"].shape[1] - 1, 0)
1519
+ index = index.view(-1, 1, 1, 1, 1)
1520
+ index = index.expand(batch_size, 1, *batch["x"].shape[2:])
1521
+ x_hat = torch.gather(batch["x"], dim=1, index=index)
1522
+ x_hat = x_hat.squeeze(1)
1523
+ assert (
1524
+ batch["y"].shape == x_hat.shape
1525
+ ), f'Shapes {batch["y"].shape} and {x_hat.shape} do not agree.'
1526
+ elif model.residual == "climate":
1527
+ climate_scaled = (
1528
+ batch["climate"] - model.input_scalers_mu.view(1, -1, 1, 1)
1529
+ ) / (
1530
+ model.input_scalers_sigma.view(1, -1, 1, 1) + model.input_scalers_epsilon
1531
+ )
1532
+
1533
+ # [batch, time, parameter, lat, lon] -> [batch, time x parameter, lat, lon]
1534
+ x_rescaled = x_rescaled.flatten(1, 2)
1535
+ # Parameter dropout
1536
+ x_rescaled = model.parameter_dropout(x_rescaled)
1537
+
1538
+ x_embedded = model.patch_embedding(x_rescaled)
1539
+ assert x_embedded.shape[1] == model.embed_dim
1540
+
1541
+ if model.residual == "climate":
1542
+ static_embedded = model.patch_embedding_static(
1543
+ torch.cat((x_static, climate_scaled), dim=1)
1544
+ )
1545
+ else:
1546
+ static_embedded = model.patch_embedding_static(x_static)
1547
+ assert static_embedded.shape[1] == model.embed_dim
1548
+
1549
+ if model.positional_encoding == 'fourier':
1550
+ static_embedded += x_static_pos
1551
+
1552
+ x_embedded = model.to_patching(x_embedded)
1553
+ static_embedded = model.to_patching(static_embedded)
1554
+
1555
+ time_encoding = model.time_encoding(batch['input_time'], batch['lead_time'])
1556
+
1557
+ tokens = x_embedded + static_embedded + time_encoding
1558
+
1559
+ if mask_global is not None:
1560
+ indices_masked = torch.where(mask_global.flatten())[0]
1561
+ indices_masked = torch.repeat_interleave(indices_masked[None], tokens.shape[0], dim=0)
1562
+ indices_unmasked = torch.where(~mask_global.flatten())[0]
1563
+ indices_unmasked = torch.repeat_interleave(indices_unmasked[None], tokens.shape[0], dim=0)
1564
+ elif mask_local is not None:
1565
+ indices_masked = torch.where(mask_local.flatten())[0]
1566
+ indices_masked = indices_masked[None, None].expand(tokens.shape[0], tokens.shape[1], -1)
1567
+ indices_unmasked = torch.where(~mask_local.flatten())[0]
1568
+ indices_unmasked = indices_unmasked[None, None].expand(tokens.shape[0], tokens.shape[1], -1)
1569
+ print(indices_unmasked.shape)
1570
+ else:
1571
+ mask_global = torch.zeros(model.global_shape_mu, dtype=torch.bool)
1572
+ indices_masked = torch.where(mask_global.flatten())[0]
1573
+ indices_masked = torch.repeat_interleave(indices_masked[None], tokens.shape[0], dim=0)
1574
+ indices_unmasked = torch.where(~mask_global.flatten())[0]
1575
+ indices_unmasked = torch.repeat_interleave(indices_unmasked[None], tokens.shape[0], dim=0)
1576
+
1577
+ maskdim: int = indices_masked.ndim
1578
+
1579
+ # Unmasking
1580
+ unmask_view = (*indices_unmasked.shape, *[1] * (tokens.ndim - maskdim))
1581
+ unmasked = torch.gather(
1582
+ tokens,
1583
+ dim=maskdim - 1,
1584
+ index=indices_unmasked.view(*unmask_view).expand(
1585
+ *indices_unmasked.shape, *tokens.shape[maskdim:]
1586
+ ),
1587
+ )
1588
+
1589
+ # Encoder
1590
+ x_encoded = model.encoder(unmasked)
1591
+
1592
+ # Generate and position encode the mask tokens
1593
+ # (1, 1, 1, embed_dim) -> (batch, global_seq_masked, local seq, embed_dim)
1594
+ mask_view = (*indices_masked.shape, *[1] * (tokens.ndim - maskdim))
1595
+ masking = model.mask_token.repeat(*static_embedded.shape[:3], 1)
1596
+ masked = masking + static_embedded
1597
+ masked = torch.gather(
1598
+ masked,
1599
+ dim=maskdim - 1,
1600
+ index=indices_masked.view(*mask_view).expand(
1601
+ *indices_masked.shape, *tokens.shape[maskdim:]
1602
+ ),
1603
+ )
1604
+
1605
+ recon, _ = model.reconstruct_batch(
1606
+ indices_masked, indices_unmasked, masked, x_encoded
1607
+ )
1608
+
1609
+ x_decoded = model.decoder(recon)
1610
+
1611
+ # Output: (batch, global sequence, local sequence, in_channels * patch_size[0] * patch_size[1])
1612
+ x_unembed = model.unembed(x_decoded)
1613
+
1614
+ # Reshape to (batch, global_lat, global_lon, local_lat, local_lon, in_channels * patch_size[0] * patch_size[1])
1615
+ assert x_unembed.shape[0] == batch_size
1616
+ assert x_unembed.shape[1] == model.global_shape_mu[0] * model.global_shape_mu[1]
1617
+ assert x_unembed.shape[2] == model.local_shape_mu[0] * model.local_shape_mu[1]
1618
+ assert (
1619
+ x_unembed.shape[3]
1620
+ == model.in_channels * model.patch_size_px[0] * model.patch_size_px[1]
1621
+ )
1622
+
1623
+ x_out = model.from_patching(x_unembed)
1624
+
1625
+ # Pixel shuffle to (batch, in_channels, lat, lon)
1626
+ x_out = F.pixel_shuffle(x_out, model.patch_size_px[0])
1627
+
1628
+ if model.residual == "temporal":
1629
+ x_out = model.output_scalers * x_out + x_hat
1630
+ elif model.residual == "climate":
1631
+ x_out = model.output_scalers * x_out + batch["climate"]
1632
+ elif model.residual == "none":
1633
+ x_out = model.output_scalers * x_out + model.input_scalers_mu.reshape(
1634
+ 1, -1, 1, 1
1635
+ )
1636
+
1637
+ return x_out
scripts/fake_data.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import h5py
3
+ import numpy as np
4
+ import xarray as xr
5
+ from onescience.utils.YParams import YParams
6
+
7
+
8
+ # Prithvi WxC 输入两个时刻、预测一个时刻,并额外接收 4 通道静态场。
9
+ def get_dims(cfg_model, cfg_data):
10
+ H, W = int(cfg_model.n_lats_px), int(cfg_model.n_lons_px)
11
+ if tuple(map(int, cfg_data.dataset.img_size)) != (H, W):
12
+ raise ValueError("model grid and datapipe.dataset.img_size must match")
13
+ input_steps, output_steps = int(cfg_model.input_size_time), 1
14
+ samples = max(int(cfg_data.dataloader.batch_size), 2)
15
+ T = input_steps + output_steps + samples - 1
16
+ return {
17
+ "T": T, "H": H, "W": W, "time_step": 6,
18
+ "input_steps": input_steps, "output_steps": output_steps,
19
+ "static_channels": int(cfg_model.in_channels_static),
20
+ }
21
+
22
+
23
+ def generate_fake_h5(data_dir, var_names, years, dims):
24
+ """
25
+ 为每个年份生成一个空 h5 文件。
26
+ 利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
27
+ 文件实际只含元数据,极小,但 shape 与真实数据完全一致。
28
+ 均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
29
+ """
30
+ os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
31
+ T, C = dims["T"], len(var_names)
32
+ H, W = dims["H"], dims["W"]
33
+
34
+ means = np.zeros((1, C, 1, 1), dtype=np.float32)
35
+ stds = np.ones((1, C, 1, 1), dtype=np.float32)
36
+
37
+ for year in years:
38
+ path = os.path.join(data_dir, "data", f"{year}.h5")
39
+ with h5py.File(path, "w") as f:
40
+ ds = f.create_dataset(
41
+ "fields",
42
+ shape=(T, C, H, W),
43
+ dtype="float32",
44
+ chunks=(1, C, H, W),
45
+ fillvalue=0.0,
46
+ )
47
+ ds.attrs["variables"] = var_names
48
+ ds.attrs["time_step"] = dims["time_step"]
49
+ f.create_dataset("global_means", data=means)
50
+ f.create_dataset("global_stds", data=stds)
51
+
52
+ size_kb = os.path.getsize(path) / 1024
53
+ print(f" {year}.h5 shape=({T},{C},{H},{W}) "
54
+ f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
55
+
56
+
57
+ def get_static(data_dir, H, W, channels):
58
+ os.makedirs(data_dir, exist_ok=True)
59
+ lat = np.linspace(90, -90, H, dtype=np.float32)
60
+ lon = np.linspace(0, 360 - 360 / W, W, dtype=np.float32)
61
+ lat_grid = np.broadcast_to(lat[:, None], (H, W)) / 90.0
62
+ lon_grid = np.broadcast_to(lon[None, :], (H, W)) / 180.0 - 1.0
63
+ land_mask = (np.sin(np.deg2rad(lat_grid * 90)) > 0).astype(np.float32)
64
+ topography = np.cos(np.deg2rad(lat_grid * 90)).astype(np.float32)
65
+ base = [lat_grid, lon_grid, land_mask, topography]
66
+ static = np.stack((base * ((channels + 3) // 4))[:channels]).astype(np.float32)
67
+
68
+ ds = xr.Dataset(
69
+ data_vars={
70
+ "z": (("valid_time", "latitude", "longitude"), static[-1:]),
71
+ "lsm": (("valid_time", "latitude", "longitude"), static[min(2, channels - 1):min(2, channels - 1) + 1]),
72
+ },
73
+ coords={
74
+ "valid_time": ["2015-12-31"],
75
+ "latitude": lat.astype(np.float64),
76
+ "longitude": lon.astype(np.float64),
77
+ "number": 0,
78
+ "expver": "",
79
+ },
80
+ attrs={
81
+ "GRIB_centre": "ecmf",
82
+ "GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
83
+ "GRIB_subCentre": "0",
84
+ "Conventions": "CF-1.7",
85
+ "institution": "European Centre for Medium-Range Weather Forecasts",
86
+ "history": "Generated manually",
87
+ }
88
+ )
89
+
90
+ ds[["z"]].to_netcdf(f"{data_dir}/geopotential.nc")
91
+ ds[["lsm"]].to_netcdf(f"{data_dir}/land_sea_mask.nc")
92
+ np.save(f'{data_dir}/static.npy', static)
93
+ np.save(f'{data_dir}/land_mask.npy', land_mask)
94
+ np.save(f'{data_dir}/soil_type.npy', np.zeros((H, W), dtype=np.float32))
95
+ np.save(f'{data_dir}/topography.npy', topography)
96
+ print(f"✅ Static data: {static.shape}, dtype: {static.dtype}, save to {data_dir}")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ cfg_model = YParams("conf/config.yaml", "model")
101
+ cfg_datapipe = YParams("conf/config.yaml", "datapipe")
102
+
103
+ if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
104
+ print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
105
+ exit()
106
+
107
+ years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
108
+ atm_vars = cfg_datapipe.dataset.channels
109
+ if len(atm_vars) != int(cfg_model.in_channels):
110
+ raise ValueError("channel count must match model.in_channels")
111
+
112
+ dims = get_dims(cfg_model, cfg_datapipe)
113
+ generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, dims)
114
+
115
+ static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
116
+ get_static(static_dir, dims["H"], dims["W"], dims["static_channels"])
117
+
118
+ print("\n✅ Fake datasets generated.")
scripts/inference.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ # 获取项目根目录(train.py上级的上级)
5
+ root_path = Path(__file__).parent.parent
6
+ sys.path.append(str(root_path))
7
+ import torch
8
+ import os
9
+ import glob
10
+ import numpy as np
11
+ import h5py
12
+ from tqdm import tqdm
13
+ from model.prithvi_wxc import PrithviWxC
14
+ from onescience.utils.YParams import YParams
15
+ from onescience.datapipes.climate import ERA5Datapipe
16
+
17
+
18
+ def get_stats(data_dir, channels):
19
+ """从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
20
+ h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
21
+ with h5py.File(h5_files[0], "r") as f:
22
+ ds = f["fields"]
23
+ all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
24
+ mu = f["global_means"][:] # [1, C, 1, 1]
25
+ std = f["global_stds"][:]
26
+
27
+ channel_indices = [all_variables.index(v) for v in channels]
28
+ means = mu[:, channel_indices, :, :]
29
+ stds = std[:, channel_indices, :, :]
30
+ return means, stds
31
+
32
+
33
+ if __name__ == "__main__":
34
+ current_path = os.getcwd()
35
+ sys.path.append(current_path)
36
+
37
+ ## Model config init
38
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
39
+ cfg = YParams(config_file_path, "model")
40
+
41
+ ## DataLoader init
42
+ cfg_data = YParams(config_file_path, "datapipe")
43
+ means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
44
+
45
+ cfg['N_in_channels'] = len(cfg_data.dataset.channels)
46
+ cfg['N_out_channels'] = len(cfg_data.dataset.channels)
47
+
48
+ datapipe = ERA5Datapipe(
49
+ dataset_dir=cfg_data.dataset.data_dir,
50
+ used_variables=cfg_data.dataset.channels,
51
+ used_years=cfg_data.dataset.test_time,
52
+ distributed=False,
53
+ input_steps=2,
54
+ batch_size=1,
55
+ num_workers=4,
56
+ )
57
+ test_dataloader, _ = datapipe.get_dataloader("test")
58
+
59
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
60
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
61
+ model = PrithviWxC(
62
+ in_channels=cfg['N_in_channels'],
63
+ input_size_time=cfg.input_size_time,
64
+ in_channels_static=cfg.in_channels_static,
65
+ n_lats_px=cfg.n_lats_px,
66
+ n_lons_px=cfg.n_lons_px,
67
+ patch_size_px=cfg.patch_size_px,
68
+ mask_unit_size_px=cfg.mask_unit_size_px,
69
+ mask_ratio_inputs=0.0,
70
+ embed_dim=cfg.embed_dim,
71
+ n_blocks_encoder=cfg.n_blocks_encoder,
72
+ n_blocks_decoder=cfg.n_blocks_decoder,
73
+ mlp_multiplier=cfg.mlp_multiplier,
74
+ n_heads=cfg.n_heads,
75
+ dropout=cfg.dropout,
76
+ drop_path=cfg.drop_path,
77
+ parameter_dropout=cfg.parameter_dropout,
78
+ residual=cfg.residual,
79
+ masking_mode=cfg.masking_mode,
80
+ positional_encoding=cfg.positional_encoding,
81
+ encoder_shifting=cfg.encoder_shifting,
82
+ decoder_shifting=cfg.decoder_shifting,
83
+ ).to(device)
84
+ model.load_state_dict(ckpt["model_state_dict"])
85
+
86
+ model.eval()
87
+ os.makedirs('result/output/', exist_ok=True)
88
+ print(f"📂 infer results will be generated to './result/output/'")
89
+ H, W = int(cfg.n_lats_px), int(cfg.n_lons_px)
90
+ static_path = os.path.join(cfg_data.dataset.data_dir, "static", "static.npy")
91
+ static_base = torch.from_numpy(np.load(static_path)).to(device=device, dtype=torch.float32).unsqueeze(0)
92
+ expected_static = (1, int(cfg.in_channels_static), H, W)
93
+ if tuple(static_base.shape) != expected_static:
94
+ raise ValueError(f"static data shape {tuple(static_base.shape)} != expected {expected_static}")
95
+ with torch.no_grad():
96
+ for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
97
+ invar = data[0].to(device, dtype=torch.float32) # [1, 2, C, H, W]
98
+ filename = data[4][-1][0]
99
+ B = invar.shape[0]
100
+ static = static_base.expand(B, -1, -1, -1)
101
+ lead_time = torch.full((B,), 6.0, device=device)
102
+ pred_var = model(invar, static, lead_time=lead_time).cpu().numpy()
103
+ pred_var = pred_var * stds + means
104
+ np.save(f"result/output/{filename}.npy", pred_var)
scripts/result.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib
3
+ matplotlib.use('Agg')
4
+ import matplotlib.pyplot as plt
5
+ import os
6
+ import sys
7
+ import glob
8
+ import h5py
9
+ from datetime import datetime
10
+ from tqdm import tqdm
11
+ from onescience.utils.YParams import YParams
12
+ from matplotlib import rcParams
13
+
14
+ # rcParams['font.family'] = 'serif'
15
+ # rcParams['font.serif'] = ['DejaVu Serif']
16
+ rcParams['mathtext.fontset'] = 'stix'
17
+ rcParams['axes.linewidth'] = 0.9
18
+ rcParams['xtick.major.width'] = 0.9
19
+ rcParams['ytick.major.width'] = 0.9
20
+
21
+
22
+ def get_metadata(data_dir, channels):
23
+ """从新版 h5 attrs 中读取变量列表和 time_step"""
24
+ h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
25
+ with h5py.File(h5_files[0], "r") as f:
26
+ ds = f["fields"]
27
+ all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
28
+ time_step = int(ds.attrs["time_step"])
29
+
30
+ channel_indices = [all_variables.index(v) for v in channels]
31
+
32
+ total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
33
+ total_files.sort()
34
+ return total_files, channel_indices, time_step
35
+
36
+
37
+ def filename_to_index(filename, time_step):
38
+ """将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
39
+ dt = datetime.strptime(filename, "%Y%m%d%H")
40
+ year_start = datetime(dt.year, 1, 1)
41
+ hours = (dt - year_start).total_seconds() / 3600
42
+ return int(hours / time_step)
43
+
44
+
45
+ def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
46
+ channel_rmse = np.zeros(len(channel_indices))
47
+ channel_acc = np.zeros(len(channel_indices))
48
+ clim_mean = clim_mean[0, :, :, :]
49
+ if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
50
+ numerator = np.zeros(len(channel_indices))
51
+ pred_sq_sum = np.zeros(len(channel_indices))
52
+ label_sq_sum = np.zeros(len(channel_indices))
53
+ for file in tqdm(total_files, unit="files"):
54
+ fname = file[:-4] # 去掉 .npy
55
+ year = fname[:4]
56
+ t_idx = filename_to_index(fname, time_step)
57
+ with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
58
+ label = f["fields"][t_idx] # [C, H, W]
59
+ label = label[channel_indices]
60
+ pred = np.load(f'result/output/{file}').squeeze()
61
+
62
+ label_anom = label - clim_mean
63
+ pred_anom = pred - clim_mean
64
+ # 累加
65
+ numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
66
+ pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
67
+ label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
68
+
69
+ channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
70
+ channel_rmse /= len(total_files)
71
+ channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
72
+ np.save('./result/acc.npy', channel_acc)
73
+ np.save('./result/rmse.npy', channel_rmse)
74
+
75
+
76
+ def show_result():
77
+ channel_rmse = np.load('./result/rmse.npy')
78
+ channel_acc = np.load('./result/acc.npy')
79
+
80
+ channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
81
+ w = 24 # 最长 channel 名宽度
82
+
83
+ # 表头
84
+ print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
85
+ print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
86
+ print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
87
+ # 数据行
88
+ for i, ch in enumerate(channels):
89
+ print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
90
+ print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
91
+ print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
92
+ print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
93
+
94
+
95
+ def plot(label, pred, var, filename):
96
+ fig, axes = plt.subplots(1, 3, figsize=(15, 4))
97
+
98
+ xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
99
+ ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
100
+ xticks = np.linspace(0, label.shape[-1] - 1, 5)
101
+ yticks = np.linspace(0, label.shape[-2] - 1, 5)
102
+
103
+ vmin = min(label.min(), pred.min())
104
+ vmax = max(label.max(), pred.max())
105
+
106
+ diff = label - pred
107
+ rmse = np.sqrt(np.mean(diff ** 2))
108
+ diff_abs_max = np.abs(diff).max()
109
+
110
+ plot_configs = [
111
+ {'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
112
+ {'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
113
+ {'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
114
+ ]
115
+
116
+ for ax, cfg in zip(axes, plot_configs):
117
+ im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
118
+ ax.set_title(cfg['title'], fontsize=12, pad=4)
119
+ ax.set_xlabel('Longitude')
120
+ ax.set_ylabel('Latitude')
121
+ ax.set_xticks(xticks)
122
+ ax.set_xticklabels(xtick_labels)
123
+ ax.set_yticks(yticks)
124
+ ax.set_yticklabels(ytick_labels)
125
+ plt.colorbar(im, ax=ax, orientation='horizontal')
126
+
127
+ fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
128
+ plt.savefig(filename, dpi=300, bbox_inches='tight')
129
+ plt.close()
130
+
131
+
132
+ def plot_loss(train_loss, valid_loss):
133
+ mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
134
+ train_loss = train_loss[mask]
135
+ valid_loss = valid_loss[mask]
136
+
137
+ fig, ax = plt.subplots(figsize=(5, 3.5))
138
+ colors = {'train': '#2563EB', 'valid': '#EA580C'}
139
+ epochs = np.arange(1, len(train_loss) + 1)
140
+
141
+ ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
142
+ ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
143
+ min_idx = np.argmin(valid_loss)
144
+ ax.scatter(epochs[min_idx], valid_loss[min_idx],
145
+ color=colors['valid'], s=40, zorder=5, edgecolors='white')
146
+ ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
147
+ xy=(epochs[min_idx], valid_loss[min_idx]),
148
+ xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
149
+ arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
150
+
151
+ ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
152
+ ax.legend(frameon=False, loc='upper right')
153
+ ax.grid(True, linestyle='--', alpha=0.3)
154
+ ax.spines[['top', 'right']].set_visible(False)
155
+
156
+ plt.tight_layout()
157
+ plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
158
+ plt.close()
159
+
160
+
161
+ if __name__ == "__main__":
162
+ current_path = os.getcwd()
163
+ sys.path.append(current_path)
164
+ config_file_path = os.path.join(current_path, 'conf/config.yaml')
165
+ cfg = YParams(config_file_path, 'model')
166
+ cfg_data = YParams(config_file_path, "datapipe")
167
+
168
+ train_loss = np.load('./data/checkpoints/trloss.npy')
169
+ valid_loss = np.load('./data/checkpoints/valoss.npy')
170
+ plot_loss(train_loss, valid_loss)
171
+
172
+ data_dir = cfg_data.dataset.data_dir
173
+ total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
174
+
175
+ # Load data & Compute RMSE/ACC per channel
176
+ h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
177
+ with h5py.File(h5_files[0], "r") as f:
178
+ mu = f["global_means"][:]
179
+ clim_mean = mu[:, channel_indices, :, :]
180
+ get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
181
+ show_result()
182
+
183
+ ##### 默认绘制 result/output 中第一个预测结果,用户可自行指定日期和变量 #####
184
+ eg_files = [total_files[0][:-4]]
185
+ channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_850']]
186
+
187
+ selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
188
+ print(f"seleted date: {eg_files}")
189
+ print(f"selected channels: {selected_var}")
190
+ for file in eg_files:
191
+ year = file[:4]
192
+ t_idx = filename_to_index(file, time_step)
193
+ with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
194
+ label = f["fields"][t_idx] # [C, H, W]
195
+ label = label[channel_indices]
196
+ pred = np.load(f'result/output/{file}.npy').squeeze()
197
+ for i in range(len(selected_var)):
198
+ filename = f'./result/{file}_{selected_var[i]}.png'
199
+ plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
200
+ print(f'✅plot {filename}')
scripts/train.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ # 获取项目根目录(train.py上级的上级)
5
+ root_path = Path(__file__).parent.parent
6
+ sys.path.append(str(root_path))
7
+ import torch
8
+ import os
9
+ import numpy as np
10
+ import torch.distributed as dist
11
+ import logging
12
+ import time
13
+
14
+ from model.prithvi_wxc import PrithviWxC
15
+ from onescience.datapipes.climate import ERA5Datapipe
16
+ from onescience.utils.YParams import YParams
17
+ from onescience.utils.fcn.darcy_loss import LpLoss
18
+
19
+ try:
20
+ from apex import optimizers
21
+ _FUSED_ADAM = True
22
+ except Exception:
23
+ _FUSED_ADAM = False
24
+
25
+
26
+ def main():
27
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
28
+ logger = logging.getLogger()
29
+
30
+ ## Model config init
31
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
32
+ cfg = YParams(config_file_path, "model")
33
+
34
+ ## Distributed config init
35
+ cfg.world_size = 1
36
+ if "WORLD_SIZE" in os.environ:
37
+ cfg.world_size = int(os.environ["WORLD_SIZE"])
38
+ world_rank = 0
39
+ local_rank = 0
40
+ if cfg.world_size > 1 and torch.cuda.is_available():
41
+ dist.init_process_group(backend="nccl", init_method="env://")
42
+ local_rank = int(os.environ["LOCAL_RANK"])
43
+ world_rank = dist.get_rank()
44
+ device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu"
45
+
46
+ ## DataLoader init
47
+ cfg_data = YParams(config_file_path, "datapipe")
48
+ cfg['N_in_channels'] = len(cfg_data.dataset.channels)
49
+ cfg['N_out_channels'] = len(cfg_data.dataset.channels)
50
+ datapipe = ERA5Datapipe(
51
+ dataset_dir=cfg_data.dataset.data_dir,
52
+ used_variables=cfg_data.dataset.channels,
53
+ used_years=cfg_data.dataset.train_time,
54
+ distributed=dist.is_initialized(),
55
+ input_steps=2,
56
+ batch_size=cfg_data.dataloader.batch_size,
57
+ num_workers=cfg_data.dataloader.num_workers,
58
+ )
59
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
60
+ datapipe = ERA5Datapipe(
61
+ dataset_dir=cfg_data.dataset.data_dir,
62
+ used_variables=cfg_data.dataset.channels,
63
+ used_years=cfg_data.dataset.val_time,
64
+ distributed=dist.is_initialized(),
65
+ input_steps=2,
66
+ batch_size=cfg_data.dataloader.batch_size,
67
+ num_workers=cfg_data.dataloader.num_workers,
68
+ )
69
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
70
+
71
+ # Model init
72
+ model = PrithviWxC(
73
+ in_channels=cfg['N_in_channels'],
74
+ input_size_time=cfg.input_size_time,
75
+ in_channels_static=cfg.in_channels_static,
76
+ n_lats_px=cfg.n_lats_px,
77
+ n_lons_px=cfg.n_lons_px,
78
+ patch_size_px=cfg.patch_size_px,
79
+ mask_unit_size_px=cfg.mask_unit_size_px,
80
+ mask_ratio_inputs=cfg.mask_ratio_inputs,
81
+ embed_dim=cfg.embed_dim,
82
+ n_blocks_encoder=cfg.n_blocks_encoder,
83
+ n_blocks_decoder=cfg.n_blocks_decoder,
84
+ mlp_multiplier=cfg.mlp_multiplier,
85
+ n_heads=cfg.n_heads,
86
+ dropout=cfg.dropout,
87
+ drop_path=cfg.drop_path,
88
+ parameter_dropout=cfg.parameter_dropout,
89
+ residual=cfg.residual,
90
+ masking_mode=cfg.masking_mode,
91
+ positional_encoding=cfg.positional_encoding,
92
+ encoder_shifting=cfg.encoder_shifting,
93
+ decoder_shifting=cfg.decoder_shifting,
94
+ ).to(device)
95
+
96
+ if _FUSED_ADAM:
97
+ optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
98
+ else:
99
+ optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr)
100
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode='min')
101
+ loss_obj = LpLoss()
102
+
103
+ ## Train process init
104
+ os.makedirs(cfg.checkpoint_dir, exist_ok=True)
105
+ train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
106
+ valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
107
+ best_valid_loss = 1.0e6
108
+ best_loss_epoch = 0
109
+ train_losses = np.empty((0,), dtype=np.float32)
110
+ valid_losses = np.empty((0,), dtype=np.float32)
111
+
112
+ ## Get model params count
113
+ if cfg.world_size == 1:
114
+ total_params = sum(p.numel() for p in model.parameters())
115
+ print("\n\n")
116
+ print("-" * 50)
117
+ print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
118
+ print("-" * 50, "\n")
119
+
120
+ ## Load model weight if there exist well-trained model
121
+ if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
122
+ if world_rank == 0:
123
+ print("\n\n")
124
+ print("-" * 50)
125
+ print(f"✅ There has a model weight, load and continue training...")
126
+ print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
127
+ print("-" * 50, "\n")
128
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
129
+ model.load_state_dict(ckpt["model_state_dict"])
130
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
131
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
132
+ best_valid_loss = ckpt["best_valid_loss"]
133
+ best_loss_epoch = ckpt["best_loss_epoch"]
134
+ train_losses = np.load(train_loss_file)
135
+ valid_losses = np.load(valid_loss_file)
136
+
137
+ ## Distributed model
138
+ if dist.is_initialized():
139
+ model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
140
+ world_rank == 0 and logger.info(f"start training ...")
141
+
142
+ H, W = int(cfg.n_lats_px), int(cfg.n_lons_px)
143
+ static_path = os.path.join(cfg_data.dataset.data_dir, "static", "static.npy")
144
+ static_base = torch.from_numpy(np.load(static_path)).to(device=device, dtype=torch.float32).unsqueeze(0)
145
+ expected_static = (1, int(cfg.in_channels_static), H, W)
146
+ if tuple(static_base.shape) != expected_static:
147
+ raise ValueError(f"static data shape {tuple(static_base.shape)} != expected {expected_static}")
148
+ for epoch in range(cfg.max_epoch):
149
+ if dist.is_initialized():
150
+ train_sampler.set_epoch(epoch)
151
+ val_sampler.set_epoch(epoch)
152
+ model.train()
153
+ train_loss = 0
154
+ start_time = time.time()
155
+ for j, data in enumerate(train_dataloader):
156
+ invar = data[0].to(device, dtype=torch.float32) # [B, 2, C, H, W]
157
+ outvar = data[1].to(device, dtype=torch.float32) # [B, C, H, W]
158
+ B = invar.shape[0]
159
+ static = static_base.expand(B, -1, -1, -1)
160
+ lead_time = torch.full((B,), 6.0, device=device)
161
+ outvar_pred = model(invar, static, lead_time=lead_time)
162
+ loss = loss_obj(outvar, outvar_pred)
163
+ optimizer.zero_grad()
164
+ loss.backward()
165
+ optimizer.step()
166
+ train_loss += loss.item()
167
+ if world_rank == 0:
168
+ logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
169
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
170
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
171
+ f'loss:{train_loss / (j+1): .04f}')
172
+
173
+ train_loss /= len(train_dataloader)
174
+
175
+ model.eval()
176
+ valid_loss = 0
177
+ with torch.no_grad():
178
+ start_time = time.time()
179
+ for j, data in enumerate(val_dataloader):
180
+ invar = data[0].to(device, dtype=torch.float32)
181
+ outvar = data[1].to(device, dtype=torch.float32)
182
+ B = invar.shape[0]
183
+ static = static_base.expand(B, -1, -1, -1)
184
+ lead_time = torch.full((B,), 6.0, device=device)
185
+ outvar_pred = model(invar, static, lead_time=lead_time)
186
+ loss = loss_obj(outvar, outvar_pred)
187
+
188
+ if dist.is_initialized():
189
+ loss_tensor = loss.detach().to(device)
190
+ dist.all_reduce(loss_tensor)
191
+ loss = loss_tensor.item() / cfg.world_size
192
+ valid_loss += loss
193
+ else:
194
+ valid_loss += loss.item()
195
+ if world_rank == 0:
196
+ logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
197
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
198
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
199
+ f'loss:{valid_loss / (j+1): .04f}')
200
+
201
+ valid_loss /= len(val_dataloader)
202
+ is_save_ckp = False
203
+ if valid_loss < best_valid_loss:
204
+ best_valid_loss = valid_loss
205
+ best_loss_epoch = epoch
206
+ world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
207
+ is_save_ckp = True
208
+ scheduler.step(valid_loss)
209
+
210
+ if world_rank == 0:
211
+ logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
212
+ f"Train Loss: {train_loss:.4f}, "
213
+ f"Valid Loss: {valid_loss:.4f}, "
214
+ f"Best loss at Epoch: {best_loss_epoch + 1}"
215
+ + (", saving checkpoint" if is_save_ckp else "")
216
+ )
217
+ train_losses = np.append(train_losses, train_loss)
218
+ valid_losses = np.append(valid_losses, valid_loss)
219
+ np.save(train_loss_file, train_losses)
220
+ np.save(valid_loss_file, valid_losses)
221
+
222
+ if epoch - best_loss_epoch > cfg.patience:
223
+ print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
224
+ exit()
225
+
226
+
227
+ def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
228
+ model_to_save = model.module if hasattr(model, "module") else model
229
+ state = {"model_state_dict": model_to_save.state_dict(),
230
+ "optimizer_state_dict": optimizer.state_dict(),
231
+ "scheduler_state_dict": scheduler.state_dict(),
232
+ "best_valid_loss": best_valid_loss,
233
+ "best_loss_epoch": best_loss_epoch,
234
+ }
235
+ torch.save(state, f"{model_path}/model.pth")
236
+ ### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
237
+ os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
238
+
239
+
240
+ if __name__ == "__main__":
241
+ current_path = os.getcwd()
242
+ sys.path.append(current_path)
243
+ main()
weight/.gitkeep ADDED
File without changes