Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .ms_upload_cache +1 -0
- README.md +179 -0
- conf/config.yaml +65 -0
- config.json +92 -0
- configuration.json +1 -0
- model/__pycache__/sfno.cpython-311.pyc +0 -0
- model/sfno.py +100 -0
- scripts/fake_data.py +108 -0
- scripts/inference.py +89 -0
- scripts/result.py +200 -0
- scripts/train.py +224 -0
- weight/.gitkeep +0 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.ms_upload_cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version": 3, "repo_id": "OneScience/Spherical-Fourier-Neural-Operators", "files": {"README.md|1787118190.0|5887": {"hash": "bec28fe4ac1b73be089ebdb23b26f499685ec2fc1e8929dd5d9468381fce8149", "size": 5887, "status": "c"}, "conf/config.yaml|1787189043.0|1979": {"hash": "f6a18e6a5e34e1144994e28c17753ea7e4ef4e2c50377bf500f94f20059b482e", "size": 1979, "status": "c"}, "model/__pycache__/sfno.cpython-311.pyc|1787193344.0|3023": {"hash": "866a2151bf8c896e448c3fcedcd6d5af2ff0e61f84c5a99f526b5f71a056a3ad", "size": 3023, "status": "c"}, "scripts/fake_data.py|1787118238.0|4258": {"hash": "6c7667f81925261a97bd297eade50bfd7afad4c7311e7f48535cbae7975cedb9", "size": 4258, "status": "c"}, "model/sfno.py|1787189031.0|4142": {"hash": "b105e978205217f5b5535fd832c802f1ac12e1f1bbde4da16448a0e0d9021252", "size": 4142, "status": "c"}, "scripts/inference.py|1787037053.0|3214": {"hash": "ee20e9825d9533b521c1e5b955abc29ebc8d636f9339d28dfbbcc972411e809e", "size": 3214, "status": "c"}, "scripts/result.py|1787129353.0|8391": {"hash": "9a5b66ed2b41bbb766b3ce26a286be904d6d2483d8fde2ada6ea6c6044e6aed0", "size": 8391, "status": "c"}, "configuration.json|1787036618.0|38": {"hash": "97d4072fc4a7a3b71e3184609fc491e77a324a982d9df860d9379c009dff84de", "size": 38, "status": "c"}, "scripts/train.py|1787036703.0|9260": {"hash": "ef4f334a62fc97e80b2333e4c0e3664298778275ed3c141c161106133a8646ed", "size": 9260, "status": "c"}, "weight/.gitkeep|1787036758.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}}}
|
README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: bsd-3-clause
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth science
|
| 9 |
+
- Weather forecasting
|
| 10 |
+
- Short- to medium-range weather forecasting
|
| 11 |
+
- Spherical Fourier neural operators
|
| 12 |
+
- ERA5
|
| 13 |
+
frameworks: PyTorch
|
| 14 |
+
datasets:
|
| 15 |
+
- OneScience/ERA5
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">Spherical Fourier Neural Operators</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Model Introduction
|
| 25 |
+
|
| 26 |
+
SFNO(Spherical Fourier Neural Operator)was proposed jointly by NVIDIA, Caltech, and other institutions,replaces the planar FFT in standard FNOs with spherical harmonic transforms (SHT), making spectral convolution respect spherical geometry and mitigating polar artifacts, spectral artifacts, and instability during long autoregressive rollouts.
|
| 27 |
+
|
| 28 |
+
Paper:Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere
|
| 29 |
+
|
| 30 |
+
https://arxiv.org/abs/2306.03838
|
| 31 |
+
|
| 32 |
+
# Model Description
|
| 33 |
+
|
| 34 |
+
SFNO is a deterministic, single-state global weather dynamics model: it takes the atmospheric state at one 6-hour time step and outputs the same set of variables at the next 6-hour time step. Longer lead times are obtained through autoregressive rollout.This repository is organized from the official reference implementation in `NVIDIA/torch-harmonics` and integrated with the OneScience data loading and training workflow.
|
| 35 |
+
|
| 36 |
+
# Use Cases
|
| 37 |
+
|
| 38 |
+
| Scenario | Description |
|
| 39 |
+
| :---: | :--- |
|
| 40 |
+
| Global weather dynamics research | Train a spherical-equivariant neural operator forecasting model on ERA5 data. |
|
| 41 |
+
| Long-horizon autoregressive stability research | Evaluate polar artifacts and dissipation during multi-step rollout. |
|
| 42 |
+
| Local quick validation | Use synthetic data to check data loading, training, inference, and result scripts. |
|
| 43 |
+
| ModelScope/OneCode execution | Download the model package, install dependencies, and run the scripts directly. |
|
| 44 |
+
| Multi-card training | Launch multi-process training with `torchrun`. |
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# Usage
|
| 48 |
+
|
| 49 |
+
## 1. OneCode Usage
|
| 50 |
+
|
| 51 |
+
Use the OneCode online environment for intelligent one-click AI4S programming:
|
| 52 |
+
|
| 53 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 54 |
+
|
| 55 |
+
## 2. Manual Installation and Usage
|
| 56 |
+
|
| 57 |
+
**Hardware Requirements**
|
| 58 |
+
|
| 59 |
+
- GPU or DCU is recommended.
|
| 60 |
+
- CPU can be used for imports and small-configuration connectivity validation, but full training and inference are slower.
|
| 61 |
+
- DCU users must install DTK beforehand. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 62 |
+
- The model depends on `torch-harmonics`(spherical harmonic transforms),Confirm that its version matches PyTorch/CUDA before installation.
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
### Download the Model Package
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
hf download OneScience-Group/SphericalFourierNeuralOperators --local-dir ./SphericalFourierNeuralOperators
|
| 69 |
+
cd SphericalFourierNeuralOperators
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Install the Runtime Environment
|
| 73 |
+
|
| 74 |
+
**DCU Environment**
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Activate DTK and CONDA first
|
| 78 |
+
conda create -n onescience311 python=3.11 -y
|
| 79 |
+
conda activate onescience311
|
| 80 |
+
# uv installation is supported
|
| 81 |
+
pip install onescience[earth-dcu] torch-harmonics -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
**GPU Environment**
|
| 85 |
+
```bash
|
| 86 |
+
# Activate CONDA first
|
| 87 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 88 |
+
conda activate onescience311
|
| 89 |
+
# uv installation is supported
|
| 90 |
+
pip install onescience[earth-gpu] torch-harmonics -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### Training Data
|
| 94 |
+
|
| 95 |
+
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:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
For a quick workflow validation, run the synthetic data script first:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
python scripts/fake_data.py
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
> Note: `scripts/fake_data.py` generates `[T, C, H, W]` HDF5 data from the model configuration. The current small configuration uses 6 channels and a 32×64 grid, and automatically calculates a time length sufficient for the batch.
|
| 108 |
+
|
| 109 |
+
### Training
|
| 110 |
+
|
| 111 |
+
Single card:
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python scripts/train.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Multiple cards:
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
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
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
Training outputs:
|
| 124 |
+
|
| 125 |
+
```text
|
| 126 |
+
data/checkpoints/model_bak.pth
|
| 127 |
+
data/checkpoints/trloss.npy
|
| 128 |
+
data/checkpoints/valoss.npy
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Training Weights
|
| 132 |
+
The `weight/` folder is reserved for model weights. The paper does not clearly release weights for the 26/73-channel weather model; this repository does not currently provide official weights, and users may train the model using the paper configuration.
|
| 133 |
+
|
| 134 |
+
### Inference
|
| 135 |
+
|
| 136 |
+
Inference reads `data/checkpoints/model_bak.pth`:
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
python scripts/inference.py
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
Prediction results are written to:
|
| 143 |
+
|
| 144 |
+
```text
|
| 145 |
+
result/output/
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Evaluation and Visualization
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
python scripts/result.py
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
Outputs include:
|
| 155 |
+
|
| 156 |
+
- `result/rmse.npy`
|
| 157 |
+
- `result/acc.npy`
|
| 158 |
+
- `result/loss.png`
|
| 159 |
+
- Forecast comparison plots for the specified date and variables
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# Official Source and Reproduction Notes
|
| 163 |
+
|
| 164 |
+
- The model implementation comes from the SFNO reference implementation in the official `NVIDIA/torch-harmonics` (BSD-3-Clause).
|
| 165 |
+
- Commit fetched for the current case directory: `49bac755cd8306fbd27a3604acafa65adf7ca202` (2026-08-14).
|
| 166 |
+
- `conf/config.yaml` uses a small configuration (`img_size=[32, 64]`, `embed_dim=16`, `num_layers=2`) for connectivity validation by default; paper-level reproduction requires a 0.25° (721×1440) grid, 26/73 channels, and a larger network as specified in the paper.
|
| 167 |
+
- The following details are not disclosed in the paper and are assumptions in this reproduction:the internal spectral downsampling factor of the weather model, the positional embedding form, per-variable normalization statistics, and training batch size.
|
| 168 |
+
|
| 169 |
+
# Official OneScience Information
|
| 170 |
+
|
| 171 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 172 |
+
| --- | --- | --- |
|
| 173 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 174 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 175 |
+
|
| 176 |
+
# Citation and License
|
| 177 |
+
|
| 178 |
+
- This repository is an independent organization and adaptation of SFNO. The model source is based on the official `torch-harmonics` implementation by Bonev et al. (2023) and follows BSD-3-Clause.
|
| 179 |
+
- Please cite:Bonev, Kurth, Hundt, Pathak, Baust, Kashinath, Anandkumar. Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere. ICML 2023.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SFNO 训练配置示例
|
| 2 |
+
# 论文默认配置为 0.25° 全球网格(721×1440)与 26/73 通道;
|
| 3 |
+
# 当前 img_size/embed_dim 等为连通性验证用的小配置,完整论文复现需按论文调整。
|
| 4 |
+
model:
|
| 5 |
+
start_epoch: 0
|
| 6 |
+
max_epoch: 100
|
| 7 |
+
lr: 1E-3
|
| 8 |
+
patience: 50
|
| 9 |
+
checkpoint_dir: "./data/checkpoints"
|
| 10 |
+
|
| 11 |
+
# SFNO 网络参数(模型名对应 Bonev et al. 2023 Spherical Fourier Neural Operator)
|
| 12 |
+
img_size: [32, 64] # 论文为 [721, 1440](0.25° ERA5)
|
| 13 |
+
scale_factor: 2 # 潜在空间频谱降采样倍数(论文天气模型未公开精确值)
|
| 14 |
+
embed_dim: 16 # 嵌入维度(论文为 256,需大显存)
|
| 15 |
+
num_layers: 2 # SFNO block 层数(论文约 8 层)
|
| 16 |
+
activation_function: "gelu"
|
| 17 |
+
use_mlp: true
|
| 18 |
+
mlp_ratio: 2.0
|
| 19 |
+
drop_rate: 0.0
|
| 20 |
+
drop_path_rate: 0.0
|
| 21 |
+
normalization_layer: "instance_norm"
|
| 22 |
+
hard_thresholding_fraction: 1.0
|
| 23 |
+
residual_prediction: false
|
| 24 |
+
pos_embed: "none" # 可选: none / sequence / spectral / learnable lat / learnable latlon
|
| 25 |
+
bias: false
|
| 26 |
+
|
| 27 |
+
# 整个数据读取流程
|
| 28 |
+
datapipe:
|
| 29 |
+
name: "ERA5"
|
| 30 |
+
task: "weather_forecasting"
|
| 31 |
+
|
| 32 |
+
dataset:
|
| 33 |
+
type: "hdf5"
|
| 34 |
+
data_dir: './data/'
|
| 35 |
+
train_time: [1951, 1952]
|
| 36 |
+
val_time: [1953]
|
| 37 |
+
test_time: [1954]
|
| 38 |
+
img_size: [32, 64]
|
| 39 |
+
verbose: true
|
| 40 |
+
cache: false
|
| 41 |
+
|
| 42 |
+
# 气象变量(此处为论文 26/73 变量配置的一个小子集,用于连通性验证)
|
| 43 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature',
|
| 44 |
+
'mean_sea_level_pressure', 'geopotential_500', 'temperature_850']
|
| 45 |
+
|
| 46 |
+
# DataLoader 配置
|
| 47 |
+
dataloader:
|
| 48 |
+
mask_dtype: "float32"
|
| 49 |
+
batch_size: 2
|
| 50 |
+
num_workers: 1
|
| 51 |
+
pin_memory: true
|
| 52 |
+
drop_last: true
|
| 53 |
+
shuffle: false
|
| 54 |
+
prefetch_factor: 2
|
| 55 |
+
persistent_workers: true
|
| 56 |
+
|
| 57 |
+
# 分布式配置
|
| 58 |
+
distributed:
|
| 59 |
+
enabled: true
|
| 60 |
+
sampler: "DistributedSampler"
|
| 61 |
+
rank: 0
|
| 62 |
+
world_size: 2
|
| 63 |
+
shuffle: true
|
| 64 |
+
seed: 42
|
| 65 |
+
drop_last: true
|
config.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "Spherical Fourier Neural Operators",
|
| 3 |
+
"model_type": "sfno",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"SFNO"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "climate-and-atmosphere",
|
| 9 |
+
"task": "global-weather-forecasting",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/sfno.py",
|
| 12 |
+
"scope": "configurable wrapper around torch_harmonics.examples.models.sfno.SphericalFourierNeuralOperator for deterministic single-state 6-hour forecasting"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "Spherical Fourier Neural Operator",
|
| 16 |
+
"spectral_operator": "spherical harmonic transform",
|
| 17 |
+
"input_format": "B C H W",
|
| 18 |
+
"prediction": "one atmospheric state to the next 6-hour state; longer forecasts use autoregressive rollout",
|
| 19 |
+
"activation": "GELU",
|
| 20 |
+
"normalization": "InstanceNorm",
|
| 21 |
+
"repository_default_config": {
|
| 22 |
+
"purpose": "connectivity validation with synthetic ERA5 data",
|
| 23 |
+
"img_size": [
|
| 24 |
+
32,
|
| 25 |
+
64
|
| 26 |
+
],
|
| 27 |
+
"scale_factor": 2,
|
| 28 |
+
"in_channels": 6,
|
| 29 |
+
"out_channels": 6,
|
| 30 |
+
"embed_dim": 16,
|
| 31 |
+
"num_layers": 2,
|
| 32 |
+
"use_mlp": true,
|
| 33 |
+
"mlp_ratio": 2.0,
|
| 34 |
+
"drop_rate": 0.0,
|
| 35 |
+
"drop_path_rate": 0.0,
|
| 36 |
+
"hard_thresholding_fraction": 1.0,
|
| 37 |
+
"residual_prediction": false,
|
| 38 |
+
"positional_embedding": "none",
|
| 39 |
+
"bias": false
|
| 40 |
+
},
|
| 41 |
+
"paper_reference_config": {
|
| 42 |
+
"grid_resolution_degrees": 0.25,
|
| 43 |
+
"grid_size": [
|
| 44 |
+
721,
|
| 45 |
+
1440
|
| 46 |
+
],
|
| 47 |
+
"channels": "26 or 73",
|
| 48 |
+
"embed_dim": 256,
|
| 49 |
+
"num_layers": "approximately 8",
|
| 50 |
+
"note": "the weather model's exact internal spectral downsampling factor is not disclosed"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"data": {
|
| 54 |
+
"dataset": "ERA5",
|
| 55 |
+
"variables": [
|
| 56 |
+
"10m_u_component_of_wind",
|
| 57 |
+
"10m_v_component_of_wind",
|
| 58 |
+
"2m_temperature",
|
| 59 |
+
"mean_sea_level_pressure",
|
| 60 |
+
"geopotential_500",
|
| 61 |
+
"temperature_850"
|
| 62 |
+
],
|
| 63 |
+
"time_step_hours": 6,
|
| 64 |
+
"input_length": 1,
|
| 65 |
+
"output_length": 1,
|
| 66 |
+
"channels": 6,
|
| 67 |
+
"spatial_size": [
|
| 68 |
+
32,
|
| 69 |
+
64
|
| 70 |
+
],
|
| 71 |
+
"storage_format": "HDF5 fields with T C H W layout",
|
| 72 |
+
"train_years": [
|
| 73 |
+
1951,
|
| 74 |
+
1952
|
| 75 |
+
],
|
| 76 |
+
"validation_years": [
|
| 77 |
+
1953
|
| 78 |
+
],
|
| 79 |
+
"test_years": [
|
| 80 |
+
1954
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
"configuration_sources": [
|
| 84 |
+
"README.md",
|
| 85 |
+
"conf/config.yaml",
|
| 86 |
+
"model/sfno.py",
|
| 87 |
+
"scripts/train.py",
|
| 88 |
+
"scripts/inference.py",
|
| 89 |
+
"scripts/fake_data.py",
|
| 90 |
+
"configuration.json"
|
| 91 |
+
]
|
| 92 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/__pycache__/sfno.cpython-311.pyc
ADDED
|
Binary file (3.02 kB). View file
|
|
|
model/sfno.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
#
|
| 3 |
+
# SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
|
| 4 |
+
# SPDX-License-Identifier: BSD-3-Clause
|
| 5 |
+
#
|
| 6 |
+
# This module is adapted from the official Spherical Fourier Neural Operator
|
| 7 |
+
# reference implementation of Boris Bonev et al. (ICML 2023), published in the
|
| 8 |
+
# NVIDIA/torch-harmonics repository (BSD-3-Clause). Only a thin configurable
|
| 9 |
+
# wrapper is added so that a single YAML config can drive the model.
|
| 10 |
+
#
|
| 11 |
+
# Redistribution and use in source and binary forms, with or without
|
| 12 |
+
# modification, are permitted provided that the following conditions are met:
|
| 13 |
+
#
|
| 14 |
+
# 1. Redistributions of source code must retain the above copyright notice,
|
| 15 |
+
# this list of conditions and the following disclaimer.
|
| 16 |
+
#
|
| 17 |
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
| 18 |
+
# this list of conditions and the following disclaimer in the documentation
|
| 19 |
+
# and/or other materials provided with the distribution.
|
| 20 |
+
#
|
| 21 |
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
| 22 |
+
# may be used to endorse or promote products derived from this software
|
| 23 |
+
# without specific prior written permission.
|
| 24 |
+
#
|
| 25 |
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 26 |
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 27 |
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 28 |
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 29 |
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 30 |
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 31 |
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 32 |
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 33 |
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 34 |
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 35 |
+
import torch
|
| 36 |
+
import torch.nn as nn
|
| 37 |
+
|
| 38 |
+
from torch_harmonics.examples.models.sfno import SphericalFourierNeuralOperator
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SFNO(nn.Module):
|
| 42 |
+
"""
|
| 43 |
+
Configurable wrapper around the official SFNO (Spherical Fourier Neural
|
| 44 |
+
Operator, Bonev et al. 2023, ICML).
|
| 45 |
+
|
| 46 |
+
The underlying network is provided by ``torch-harmonics``
|
| 47 |
+
(``torch_harmonics.examples.models.sfno.SphericalFourierNeuralOperator``),
|
| 48 |
+
which replaces the plain FFT of a vanilla FNO by a Spherical Harmonic
|
| 49 |
+
Transform (SHT) so that the learned convolution respects the geometry of
|
| 50 |
+
the sphere.
|
| 51 |
+
|
| 52 |
+
Model inputs / outputs are deterministic global fields of shape
|
| 53 |
+
``(Batch, C, H, W)``: a single 6-hour state ``u_t`` is mapped to the next
|
| 54 |
+
state ``u_{t+1}`` (trained with weighted L2 losses and 1-2 step rollout).
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(
|
| 58 |
+
self,
|
| 59 |
+
img_size=(32, 64),
|
| 60 |
+
scale_factor=2,
|
| 61 |
+
in_chans=4,
|
| 62 |
+
out_chans=4,
|
| 63 |
+
embed_dim=16,
|
| 64 |
+
num_layers=2,
|
| 65 |
+
activation_function="gelu",
|
| 66 |
+
use_mlp=True,
|
| 67 |
+
mlp_ratio=2.0,
|
| 68 |
+
drop_rate=0.0,
|
| 69 |
+
drop_path_rate=0.0,
|
| 70 |
+
normalization_layer="instance_norm",
|
| 71 |
+
hard_thresholding_fraction=1.0,
|
| 72 |
+
residual_prediction=False,
|
| 73 |
+
pos_embed="none",
|
| 74 |
+
bias=False,
|
| 75 |
+
):
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.img_size = tuple(img_size)
|
| 78 |
+
self.in_chans = int(in_chans)
|
| 79 |
+
self.out_chans = int(out_chans)
|
| 80 |
+
self.model = SphericalFourierNeuralOperator(
|
| 81 |
+
img_size=self.img_size,
|
| 82 |
+
scale_factor=int(scale_factor),
|
| 83 |
+
in_chans=self.in_chans,
|
| 84 |
+
out_chans=self.out_chans,
|
| 85 |
+
embed_dim=int(embed_dim),
|
| 86 |
+
num_layers=int(num_layers),
|
| 87 |
+
activation_function=activation_function,
|
| 88 |
+
use_mlp=use_mlp,
|
| 89 |
+
mlp_ratio=mlp_ratio,
|
| 90 |
+
drop_rate=drop_rate,
|
| 91 |
+
drop_path_rate=drop_path_rate,
|
| 92 |
+
normalization_layer=normalization_layer,
|
| 93 |
+
hard_thresholding_fraction=hard_thresholding_fraction,
|
| 94 |
+
residual_prediction=residual_prediction,
|
| 95 |
+
pos_embed=pos_embed,
|
| 96 |
+
bias=bias,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
def forward(self, x):
|
| 100 |
+
return self.model(x)
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# SFNO 单步输入、单步输出;T 保证每个年份至少产生一个完整 batch。
|
| 9 |
+
def get_dims(cfg_model, cfg_data):
|
| 10 |
+
H, W = map(int, cfg_model.img_size)
|
| 11 |
+
if tuple(map(int, cfg_data.dataset.img_size)) != (H, W):
|
| 12 |
+
raise ValueError("model.img_size and datapipe.dataset.img_size must match")
|
| 13 |
+
batch_size = int(cfg_data.dataloader.batch_size)
|
| 14 |
+
input_steps, output_steps = 1, 1
|
| 15 |
+
T = input_steps + output_steps + batch_size - 1
|
| 16 |
+
return {
|
| 17 |
+
"T": T, "H": H, "W": W, "time_step": 6,
|
| 18 |
+
"input_steps": input_steps, "output_steps": output_steps,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 23 |
+
"""
|
| 24 |
+
为每个年份生成一个空 h5 文件。
|
| 25 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 26 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 27 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 28 |
+
"""
|
| 29 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 30 |
+
T, C = dims["T"], len(var_names)
|
| 31 |
+
H, W = dims["H"], dims["W"]
|
| 32 |
+
|
| 33 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 34 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 35 |
+
|
| 36 |
+
for year in years:
|
| 37 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 38 |
+
with h5py.File(path, "w") as f:
|
| 39 |
+
ds = f.create_dataset(
|
| 40 |
+
"fields",
|
| 41 |
+
shape=(T, C, H, W),
|
| 42 |
+
dtype="float32",
|
| 43 |
+
chunks=(1, C, H, W),
|
| 44 |
+
fillvalue=0.0,
|
| 45 |
+
)
|
| 46 |
+
ds.attrs["variables"] = var_names
|
| 47 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 48 |
+
f.create_dataset("global_means", data=means)
|
| 49 |
+
f.create_dataset("global_stds", data=stds)
|
| 50 |
+
|
| 51 |
+
size_kb = os.path.getsize(path) / 1024
|
| 52 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 53 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def get_static(data_dir, var, name, H, W):
|
| 57 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 58 |
+
ds = xr.Dataset(
|
| 59 |
+
data_vars={
|
| 60 |
+
f"{var}": (("valid_time", "latitude", "longitude"),
|
| 61 |
+
np.random.rand(1, H, W).astype(np.float32))
|
| 62 |
+
},
|
| 63 |
+
coords={
|
| 64 |
+
"valid_time": ["2015-12-31"],
|
| 65 |
+
"latitude": np.linspace(90, -90, H, dtype=np.float64),
|
| 66 |
+
"longitude": np.linspace(0, 360 - 360 / W, W, dtype=np.float64),
|
| 67 |
+
"number": 0,
|
| 68 |
+
"expver": "",
|
| 69 |
+
},
|
| 70 |
+
attrs={
|
| 71 |
+
"GRIB_centre": "ecmf",
|
| 72 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 73 |
+
"GRIB_subCentre": "0",
|
| 74 |
+
"Conventions": "CF-1.7",
|
| 75 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 76 |
+
"history": "Generated manually",
|
| 77 |
+
}
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
ds.to_netcdf(f"{data_dir}/{name}.nc")
|
| 81 |
+
arr = np.random.randn(H, W).astype(np.float32)
|
| 82 |
+
np.save(f'{data_dir}/land_mask.npy', arr)
|
| 83 |
+
np.save(f'{data_dir}/soil_type.npy', arr)
|
| 84 |
+
np.save(f'{data_dir}/topography.npy', arr)
|
| 85 |
+
print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
cfg_model = YParams("conf/config.yaml", "model")
|
| 90 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 91 |
+
|
| 92 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 93 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 94 |
+
exit()
|
| 95 |
+
|
| 96 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 97 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 98 |
+
if len(atm_vars) == 0:
|
| 99 |
+
raise ValueError("datapipe.dataset.channels must not be empty")
|
| 100 |
+
|
| 101 |
+
dims = get_dims(cfg_model, cfg_datapipe)
|
| 102 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, dims)
|
| 103 |
+
|
| 104 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 105 |
+
get_static(static_dir, 'z', 'geopotential', dims["H"], dims["W"])
|
| 106 |
+
get_static(static_dir, 'lsm', 'land_sea_mask', dims["H"], dims["W"])
|
| 107 |
+
|
| 108 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.sfno import SFNO
|
| 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 |
+
batch_size=1,
|
| 54 |
+
num_workers=4,
|
| 55 |
+
)
|
| 56 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 57 |
+
|
| 58 |
+
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 59 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 60 |
+
model = SFNO(
|
| 61 |
+
img_size=cfg.img_size,
|
| 62 |
+
scale_factor=cfg.scale_factor,
|
| 63 |
+
in_chans=cfg['N_in_channels'],
|
| 64 |
+
out_chans=cfg['N_out_channels'],
|
| 65 |
+
embed_dim=cfg.embed_dim,
|
| 66 |
+
num_layers=cfg.num_layers,
|
| 67 |
+
activation_function=cfg.activation_function,
|
| 68 |
+
use_mlp=cfg.use_mlp,
|
| 69 |
+
mlp_ratio=cfg.mlp_ratio,
|
| 70 |
+
drop_rate=cfg.drop_rate,
|
| 71 |
+
drop_path_rate=cfg.drop_path_rate,
|
| 72 |
+
normalization_layer=cfg.normalization_layer,
|
| 73 |
+
hard_thresholding_fraction=cfg.hard_thresholding_fraction,
|
| 74 |
+
residual_prediction=cfg.residual_prediction,
|
| 75 |
+
pos_embed=cfg.pos_embed,
|
| 76 |
+
bias=cfg.bias,
|
| 77 |
+
).to(device)
|
| 78 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 79 |
+
|
| 80 |
+
model.eval()
|
| 81 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 82 |
+
print(f"📂 infer results will be generated to './result/output/'")
|
| 83 |
+
with torch.no_grad():
|
| 84 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 85 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 86 |
+
filename = data[4][-1][0]
|
| 87 |
+
pred_var = model(invar).cpu().numpy()
|
| 88 |
+
pred_var = pred_var * stds + means
|
| 89 |
+
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,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.sfno import SFNO
|
| 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 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 56 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 57 |
+
)
|
| 58 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 59 |
+
datapipe = ERA5Datapipe(
|
| 60 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 61 |
+
used_variables=cfg_data.dataset.channels,
|
| 62 |
+
used_years=cfg_data.dataset.val_time,
|
| 63 |
+
distributed=dist.is_initialized(),
|
| 64 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 65 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 66 |
+
)
|
| 67 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 68 |
+
|
| 69 |
+
# Model init
|
| 70 |
+
model = SFNO(
|
| 71 |
+
img_size=cfg.img_size,
|
| 72 |
+
scale_factor=cfg.scale_factor,
|
| 73 |
+
in_chans=cfg['N_in_channels'],
|
| 74 |
+
out_chans=cfg['N_out_channels'],
|
| 75 |
+
embed_dim=cfg.embed_dim,
|
| 76 |
+
num_layers=cfg.num_layers,
|
| 77 |
+
activation_function=cfg.activation_function,
|
| 78 |
+
use_mlp=cfg.use_mlp,
|
| 79 |
+
mlp_ratio=cfg.mlp_ratio,
|
| 80 |
+
drop_rate=cfg.drop_rate,
|
| 81 |
+
drop_path_rate=cfg.drop_path_rate,
|
| 82 |
+
normalization_layer=cfg.normalization_layer,
|
| 83 |
+
hard_thresholding_fraction=cfg.hard_thresholding_fraction,
|
| 84 |
+
residual_prediction=cfg.residual_prediction,
|
| 85 |
+
pos_embed=cfg.pos_embed,
|
| 86 |
+
bias=cfg.bias,
|
| 87 |
+
).to(device)
|
| 88 |
+
|
| 89 |
+
if _FUSED_ADAM:
|
| 90 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
|
| 91 |
+
else:
|
| 92 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr)
|
| 93 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode='min')
|
| 94 |
+
loss_obj = LpLoss()
|
| 95 |
+
|
| 96 |
+
## Train process init
|
| 97 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 98 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 99 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 100 |
+
best_valid_loss = 1.0e6
|
| 101 |
+
best_loss_epoch = 0
|
| 102 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 103 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 104 |
+
|
| 105 |
+
## Get model params count
|
| 106 |
+
if cfg.world_size == 1:
|
| 107 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 108 |
+
print("\n\n")
|
| 109 |
+
print("-" * 50)
|
| 110 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 111 |
+
print("-" * 50, "\n")
|
| 112 |
+
|
| 113 |
+
## Load model weight if there exist well-trained model
|
| 114 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 115 |
+
if world_rank == 0:
|
| 116 |
+
print("\n\n")
|
| 117 |
+
print("-" * 50)
|
| 118 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 119 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 120 |
+
print("-" * 50, "\n")
|
| 121 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 122 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 123 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 124 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 125 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 126 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 127 |
+
train_losses = np.load(train_loss_file)
|
| 128 |
+
valid_losses = np.load(valid_loss_file)
|
| 129 |
+
|
| 130 |
+
## Distributed model
|
| 131 |
+
if dist.is_initialized():
|
| 132 |
+
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 133 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 134 |
+
|
| 135 |
+
for epoch in range(cfg.max_epoch):
|
| 136 |
+
if dist.is_initialized():
|
| 137 |
+
train_sampler.set_epoch(epoch)
|
| 138 |
+
val_sampler.set_epoch(epoch)
|
| 139 |
+
model.train()
|
| 140 |
+
train_loss = 0
|
| 141 |
+
start_time = time.time()
|
| 142 |
+
for j, data in enumerate(train_dataloader):
|
| 143 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 144 |
+
outvar = data[1].to(device, dtype=torch.float32)
|
| 145 |
+
outvar_pred = model(invar)
|
| 146 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 147 |
+
optimizer.zero_grad()
|
| 148 |
+
loss.backward()
|
| 149 |
+
optimizer.step()
|
| 150 |
+
train_loss += loss.item()
|
| 151 |
+
if world_rank == 0:
|
| 152 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 153 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 154 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 155 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 156 |
+
|
| 157 |
+
train_loss /= len(train_dataloader)
|
| 158 |
+
|
| 159 |
+
model.eval()
|
| 160 |
+
valid_loss = 0
|
| 161 |
+
with torch.no_grad():
|
| 162 |
+
start_time = time.time()
|
| 163 |
+
for j, data in enumerate(val_dataloader):
|
| 164 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 165 |
+
outvar = data[1].to(device, dtype=torch.float32)
|
| 166 |
+
outvar_pred = model(invar)
|
| 167 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 168 |
+
|
| 169 |
+
if dist.is_initialized():
|
| 170 |
+
loss_tensor = loss.detach().to(device)
|
| 171 |
+
dist.all_reduce(loss_tensor)
|
| 172 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 173 |
+
valid_loss += loss
|
| 174 |
+
else:
|
| 175 |
+
valid_loss += loss.item()
|
| 176 |
+
if world_rank == 0:
|
| 177 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 178 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 179 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 180 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 181 |
+
|
| 182 |
+
valid_loss /= len(val_dataloader)
|
| 183 |
+
is_save_ckp = False
|
| 184 |
+
if valid_loss < best_valid_loss:
|
| 185 |
+
best_valid_loss = valid_loss
|
| 186 |
+
best_loss_epoch = epoch
|
| 187 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 188 |
+
is_save_ckp = True
|
| 189 |
+
scheduler.step(valid_loss)
|
| 190 |
+
|
| 191 |
+
if world_rank == 0:
|
| 192 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 193 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 194 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 195 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 196 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 197 |
+
)
|
| 198 |
+
train_losses = np.append(train_losses, train_loss)
|
| 199 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 200 |
+
np.save(train_loss_file, train_losses)
|
| 201 |
+
np.save(valid_loss_file, valid_losses)
|
| 202 |
+
|
| 203 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 204 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 205 |
+
exit()
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 209 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 210 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 211 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 212 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 213 |
+
"best_valid_loss": best_valid_loss,
|
| 214 |
+
"best_loss_epoch": best_loss_epoch,
|
| 215 |
+
}
|
| 216 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 217 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 218 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
if __name__ == "__main__":
|
| 222 |
+
current_path = os.getcwd()
|
| 223 |
+
sys.path.append(current_path)
|
| 224 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|