zhangrenchao commited on
Commit
9be39c5
·
verified ·
1 Parent(s): 60c2561

Add engineering reproduction package

Browse files
.gitattributes CHANGED
@@ -1,35 +1,6 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
  *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
1
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  *.pt filter=lfs diff=lfs merge=lfs -text
3
  *.pth filter=lfs diff=lfs merge=lfs -text
4
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
5
+ *.npy filter=lfs diff=lfs merge=lfs -text
6
+ *.npz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Precipitation Nowcasting
9
+ - Spatiotemporal Sequence Prediction
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong>
15
+ <span style="font-size: 30px;">ConvLSTM</span>
16
+ </strong>
17
+ </p>
18
+
19
+ # Model Introduction
20
+
21
+ ConvLSTM is a convolutional recurrent neural network for spatiotemporal sequence prediction. It replaces the input-to-state and state-to-state transformations of LSTM with spatial convolutions, thereby modeling local spatial correlations while preserving long-term temporal dependencies.
22
+
23
+ Paper: Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting
24
+ https://papers.nips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html
25
+
26
+ # Model Description
27
+
28
+ ConvLSTM was proposed by researchers at the Hong Kong University of Science and Technology and the Hong Kong Observatory. The model is trained and evaluated using the 97 days with the highest rainfall in the 2011 to 2013 Hong Kong weather radar data, as well as synthetic Moving-MNIST sequences. The model is suitable for precipitation nowcasting, video prediction, and general spatiotemporal sequence prediction tasks.
29
+
30
+ # Use Cases
31
+
32
+ | Scenario | Description |
33
+ | :---: | :--- |
34
+ | Precipitation nowcasting | Predict the next 15 time steps from 5 historical radar maps. |
35
+ | Spatiotemporal sequence modeling | Jointly learn spatial and temporal correlations with a convolutional gating structure. |
36
+ | Multi-step image prediction | Continuously generate future images through an Encoder-Forecaster structure. |
37
+ | Local workflow validation | Use synthetic radar sequences to check training, inference, evaluation, visualization, and checkpoint workflows. |
38
+ | Multi-GPU training | Launch distributed data-parallel training with `torchrun`. |
39
+
40
+ # Usage Instructions
41
+
42
+ ## 1. OneCode
43
+
44
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
45
+
46
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
47
+
48
+ ## 2. Download and Installation
49
+
50
+ ```bash
51
+ hf download OneScience-Group/ConvLSTM --local-dir ./ConvLSTM
52
+ cd ConvLSTM
53
+ ```
54
+
55
+ ### Environment Dependencies
56
+
57
+ **Hardware Requirements**
58
+
59
+ - A GPU or DCU is recommended.
60
+ - A CPU can be used for connectivity validation with the default small-sample configuration.
61
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
62
+
63
+ **DCU Environment**
64
+
65
+ ```bash
66
+ # Activate DTK and Conda first
67
+ conda create -n onescience311 python=3.11 -y
68
+ conda activate onescience311
69
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
70
+ ```
71
+
72
+ **GPU Environment**
73
+
74
+ ```bash
75
+ # Activate Conda first
76
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
77
+ conda activate onescience311
78
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
79
+ ```
80
+
81
+ ### Training Data
82
+
83
+ This repository uses a small number of synthetic samples to validate the engineering workflow. The synthetic data contains 20 consecutive single-channel `float32` radar echo images of `100×100` pixels at 6-minute intervals. The first 5 frames are used as input and the following 15 frames as prediction targets. This data is only used to validate ConvLSTM's spatiotemporal encoding, 15-step prediction, training, inference, and evaluation workflows; it does not represent the official radar data distribution or training scale.
84
+
85
+ ```bash
86
+ python scripts/fake_data.py
87
+ ```
88
+
89
+ ### Training
90
+
91
+ ```bash
92
+ python scripts/train.py
93
+ ```
94
+
95
+ For multi-GPU training, use:
96
+
97
+ ```bash
98
+ 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
99
+ ```
100
+
101
+ The default configuration retains two Encoder layers, two Forecaster layers, `3×3` convolutions, channel-wise peephole connections, and the full prediction length; it only reduces the number of samples, hidden channels, and training epochs.
102
+
103
+ ```text
104
+ result/checkpoints/convlstm.pt
105
+ result/training/metrics.json
106
+ ```
107
+
108
+ ### Training Weights
109
+
110
+ This repository does not include synthetic or trained weights, and the paper does not provide a directly downloadable original Theano pretrained checkpoint.
111
+
112
+ ### Inference
113
+
114
+ ```bash
115
+ python scripts/inference.py
116
+ ```
117
+
118
+ Inference loads the training checkpoint and generates 15 future radar echo predictions from 5 historical radar maps. The inference results contain the input sequence, ground-truth target, predicted sequence, and corresponding lead-time information in minutes.
119
+
120
+ ```text
121
+ result/output/predictions.npz
122
+ ```
123
+
124
+ ### Evaluation and Visualization
125
+
126
+ ```bash
127
+ python scripts/result.py
128
+ ```
129
+
130
+ Following the paper, the evaluation converts radar echoes to rainfall rates using the Z-R relationship and computes Rainfall-MSE, CSI, FAR, POD, and Correlation. The results include per-step metrics for all 15 forecast lead times and overall summary metrics, and generate comparison plots of targets, predictions, and absolute errors for selected lead times. Results on synthetic data are only for validating the engineering workflow and do not represent metrics on the paper's actual radar data.
131
+
132
+ ```text
133
+ result/evaluation/metrics.json
134
+ result/evaluation/comparison.png
135
+ ```
136
+
137
+ # Official OneScience Information
138
+
139
+ | Platform | OneScience Main Repository | Skills Repository |
140
+ | --- | --- | --- |
141
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
142
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
143
+
144
+ # Citation and License
145
+
146
+ This repository is an independent engineering reproduction of the publicly available specifications in the ConvLSTM paper.
147
+
148
+ Use of the code and data in this repository remains subject to the licenses and terms of use of their respective projects.
README_zh.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: Apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 降水临近预报
10
+ - 时空序列预测
11
+ frameworks: PyTorch
12
+ datasets:
13
+ - Hong Kong weather radar echo dataset
14
+ ---
15
+
16
+ <p align="center">
17
+ <strong>
18
+ <span style="font-size: 30px;">ConvLSTM</span>
19
+ </strong>
20
+ </p>
21
+
22
+ # 模型介绍
23
+
24
+ ConvLSTM 是面向时空序列预测的卷积循环神经网络,将 LSTM 的输入到状态和状态到状态变换替换为空间卷积,从而在保留长期时间依赖的同时建模局部空间相关性。
25
+
26
+ 论文:Convolutional LSTM Network: A Machine Learning Approach for Precipitation Nowcasting
27
+ https://papers.nips.cc/paper_files/paper/2015/hash/07563a3fe3bbe7e3ba84431ad9d055af-Abstract.html
28
+
29
+ # 模型描述
30
+
31
+ ConvLSTM 由香港科技大学与香港天文台的研究人员提出。模型使用 2011 至 2013 年香港天气雷达数据中降雨量最高的 97 天,以及 Moving-MNIST 合成序列进行训练和评估。模型适用于降水临近预报、视频预测和一般时空序列预测任务。
32
+
33
+ # 适用场景
34
+
35
+ | 场景 | 说明 |
36
+ | :---: | :--- |
37
+ | 降水临近预报 | 根据 5 张历史雷达图预测未来 15 个时间步。 |
38
+ | 时空序列建模 | 使用卷积门控结构联合学习空间和时间相关性。 |
39
+ | 多步图像预测 | 通过 Encoder-Forecaster 结构连续生成未来图像。 |
40
+ | 本地工程验证 | 使用虚拟雷达序列检查训练、推理、评估、可视化和 checkpoint 流程。 |
41
+ | 多卡训练 | 通过 `torchrun` 启动分布式数据并行训练。 |
42
+
43
+ # 使用说明
44
+
45
+ ## 1.OneCode
46
+
47
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
48
+
49
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
50
+
51
+ ## 2.下载安装
52
+
53
+ ```bash
54
+ modelscope download --model OneScience/ConvLSTM --local_dir ./ConvLSTM
55
+ cd ConvLSTM
56
+ ```
57
+
58
+ ### 环境依赖
59
+
60
+ **硬件要求**
61
+
62
+ - 推荐使用 GPU 或 DCU 运行。
63
+ - CPU 可用于默认小样本配置的连通性验证。
64
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
65
+
66
+ **DCU环境**
67
+
68
+ ```bash
69
+ # 请首先激活DTK及CONDA
70
+ conda create -n onescience311 python=3.11 -y
71
+ conda activate onescience311
72
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
73
+ ```
74
+
75
+ **GPU环境**
76
+
77
+ ```bash
78
+ # 请首先激活CONDA
79
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
80
+ conda activate onescience311
81
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
82
+ ```
83
+
84
+ ### 训练数据介绍
85
+
86
+ 本仓库使用少量虚拟样本验证工程流程,虚拟数据包含连续 20 帧、时间间隔为 6 分钟的 `100×100` 单通道 `float32` 雷达回波图,其中前 5 帧作为输入,后 15 帧作为预测目标。该数据仅用于验证 ConvLSTM 的时空编码、15 步预测、训练、推理和评估流程,不代表官方雷达数据分布与训练规模。
87
+
88
+ ```bash
89
+ python scripts/fake_data.py
90
+ ```
91
+
92
+ ### 训练
93
+
94
+ ```bash
95
+ python scripts/train.py
96
+ ```
97
+
98
+ 多卡训练可使用:
99
+
100
+ ```bash
101
+ 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
102
+ ```
103
+
104
+ 默认配置保持两层 Encoder、两层 Forecaster、`3×3` 卷积、逐通道 peephole 和完整预测长度,只缩小样本数量、隐藏通道和训练周期。
105
+
106
+ ```text
107
+ result/checkpoints/convlstm.pt
108
+ result/training/metrics.json
109
+ ```
110
+
111
+ ### 训练权重
112
+
113
+ 本仓库不内置虚拟权重或训练权重,论文未提供可直接下载的原始 Theano 预训练 checkpoint。
114
+
115
+ ### 推理
116
+
117
+ ```bash
118
+ python scripts/inference.py
119
+ ```
120
+
121
+ 推理加载训练 checkpoint,根据 5 张历史雷达图生成未来 15 张雷达回波预测。推理结果包含输入序列、真实目标、预测序列及对应的分钟时效信息。
122
+
123
+ ```text
124
+ result/output/predictions.npz
125
+ ```
126
+
127
+ ### 评估和可视化
128
+
129
+ ```bash
130
+ python scripts/result.py
131
+ ```
132
+
133
+ 评估按照论文的 Z-R 关系将雷达回波转换为降雨率,并计算 Rainfall-MSE、CSI、FAR、POD 和 Correlation。结果同时包含 15 个预测时效的分步指标及整体汇总指标,并生成部分时效的目标、预测和绝对误差对比图。虚拟数据结果仅用于验证工程流程,不代表论文真实雷达数据指标。
134
+
135
+ ```text
136
+ result/evaluation/metrics.json
137
+ result/evaluation/comparison.png
138
+ ```
139
+
140
+ # OneScience官方信息
141
+
142
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
143
+ | --- | --- | --- |
144
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
145
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
146
+
147
+ # 引用与许可证
148
+
149
+ 本仓库为 ConvLSTM 论文公开规格的独立工程复现版本。
150
+
151
+ 本仓库代码和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ data:
3
+ root: data
4
+ format_version: convlstm_hko_synthetic_engineering_v1
5
+ train_samples: 4
6
+ test_samples: 2
7
+ input_frames: 5
8
+ output_frames: 15
9
+ channels: 1
10
+ height: 100
11
+ width: 100
12
+ interval_minutes: 6
13
+ patch_size: 2
14
+ rainfall_threshold_mm_h: 0.5
15
+ zr_a: 118.239
16
+ zr_b: 1.5241
17
+ radar_db_min: 0.0
18
+ radar_db_max: 70.0
19
+ model:
20
+ input_channels: 1
21
+ patch_size: 2
22
+ hidden_channels: [16, 16]
23
+ kernel_size: 3
24
+ output_frames: 15
25
+ paper_model:
26
+ input_channels: 1
27
+ patch_size: 2
28
+ hidden_channels: [64, 64]
29
+ kernel_size: 3
30
+ output_frames: 15
31
+ train:
32
+ epochs: 1
33
+ batch_size: 1
34
+ learning_rate: 0.001
35
+ rmsprop_alpha: 0.9
36
+ weight_decay: 0.0
37
+ gradient_clip_norm: 10.0
38
+ num_workers: 0
39
+ runtime:
40
+ device: auto
41
+ paths:
42
+ checkpoint: result/checkpoints/convlstm.pt
43
+ training_metrics: result/training/metrics.json
44
+ inference_dir: result/output
45
+ evaluation_dir: result/evaluation
config.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "ConvLSTM",
3
+ "model_type": "convlstm",
4
+ "architectures": ["ConvLSTM", "ConvLSTMCell"],
5
+ "framework": "PyTorch",
6
+ "domain": "atmosphere",
7
+ "task": "precipitation-nowcasting",
8
+ "implementation": {
9
+ "entry_point": "model/convlstm.py",
10
+ "scope": "peephole ConvLSTM encoder-forecaster for fifteen-step radar echo prediction",
11
+ "train_script": "scripts/train.py",
12
+ "inference_script": "scripts/inference.py",
13
+ "evaluation_script": "scripts/result.py",
14
+ "synthetic_data_script": "scripts/fake_data.py"
15
+ },
16
+ "architecture": {
17
+ "family": "two-layer ConvLSTM encoder and two-layer unconditional forecaster",
18
+ "input_channels": 1,
19
+ "patch_size": 2,
20
+ "patched_channels": 4,
21
+ "hidden_channels": [16, 16],
22
+ "kernel_size": 3,
23
+ "peephole": true,
24
+ "output_frames": 15,
25
+ "output_head": "1x1 convolution over concatenated forecaster states"
26
+ },
27
+ "data": {
28
+ "datasets": ["Hong Kong weather radar echo dataset"],
29
+ "protocol": "convlstm_hko_synthetic_engineering_v1",
30
+ "format": "NPZ",
31
+ "train_file": "data/train.npz",
32
+ "test_file": "data/test.npz",
33
+ "input_shape": ["N", 5, 1, 100, 100],
34
+ "target_shape": ["N", 15, 1, 100, 100],
35
+ "interval_minutes": 6,
36
+ "forecast_horizon_minutes": 90,
37
+ "rainfall_threshold_mm_h": 0.5,
38
+ "zr_parameters": {"a": 118.239, "b": 1.5241},
39
+ "required_metadata": ["format_version", "data_source"]
40
+ },
41
+ "configuration_sources": ["conf/config.yaml", "model/convlstm.py", "scripts/fake_data.py", "scripts/train.py", "scripts/inference.py", "scripts/result.py"]
42
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "precipitation_nowcasting",
4
+ "model": "ConvLSTM",
5
+ "input_format": "BTCHW",
6
+ "protocol": "convlstm_encoder_forecaster",
7
+ "default_config": "conf/config.yaml",
8
+ "training": "scripts/train.py",
9
+ "inference": "scripts/inference.py",
10
+ "evaluation": "scripts/result.py",
11
+ "visualization": "scripts/result.py"
12
+ }
model/convlstm.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peephole ConvLSTM encoder-forecaster for precipitation nowcasting."""
2
+
3
+ import torch
4
+ from torch import nn
5
+ from torch.nn import functional as F
6
+
7
+
8
+ def patchify(sequence, patch_size):
9
+ batch, steps, channels, height, width = sequence.shape
10
+ flattened = sequence.flatten(0, 1)
11
+ patched = F.pixel_unshuffle(flattened, patch_size)
12
+ return patched.unflatten(0, (batch, steps))
13
+
14
+
15
+ def unpatchify(sequence, patch_size):
16
+ batch, steps = sequence.shape[:2]
17
+ images = F.pixel_shuffle(sequence.flatten(0, 1), patch_size)
18
+ return images.unflatten(0, (batch, steps))
19
+
20
+
21
+ class ConvLSTMCell(nn.Module):
22
+ def __init__(self, input_channels, hidden_channels, kernel_size):
23
+ super().__init__()
24
+ padding = kernel_size // 2
25
+ self.hidden_channels = hidden_channels
26
+ self.input_conv = None if input_channels == 0 else nn.Conv2d(
27
+ input_channels, 4 * hidden_channels, kernel_size, padding=padding
28
+ )
29
+ self.hidden_conv = nn.Conv2d(hidden_channels, 4 * hidden_channels, kernel_size,
30
+ padding=padding, bias=False)
31
+ self.peephole_input = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
32
+ self.peephole_forget = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
33
+ self.peephole_output = nn.Parameter(torch.zeros(1, hidden_channels, 1, 1))
34
+ self.bias = nn.Parameter(torch.zeros(1, 4 * hidden_channels, 1, 1))
35
+
36
+ def forward(self, values, state):
37
+ hidden, cell = state
38
+ gates = self.hidden_conv(hidden) + self.bias
39
+ if values is not None:
40
+ if self.input_conv is None:
41
+ raise ValueError("this ConvLSTM cell has no external input projection")
42
+ gates = gates + self.input_conv(values)
43
+ input_gate, forget_gate, candidate, output_gate = gates.chunk(4, dim=1)
44
+ input_gate = torch.sigmoid(input_gate + self.peephole_input * cell)
45
+ forget_gate = torch.sigmoid(forget_gate + self.peephole_forget * cell)
46
+ cell = forget_gate * cell + input_gate * torch.tanh(candidate)
47
+ output_gate = torch.sigmoid(output_gate + self.peephole_output * cell)
48
+ hidden = output_gate * torch.tanh(cell)
49
+ return hidden, cell
50
+
51
+
52
+ class ConvLSTM(nn.Module):
53
+ def __init__(self, config):
54
+ super().__init__()
55
+ self.patch_size = int(config["patch_size"])
56
+ self.output_frames = int(config["output_frames"])
57
+ patch_channels = int(config["input_channels"]) * self.patch_size ** 2
58
+ hidden = [int(value) for value in config["hidden_channels"]]
59
+ kernel = int(config["kernel_size"])
60
+ self.encoder = nn.ModuleList([
61
+ ConvLSTMCell(patch_channels, hidden[0], kernel),
62
+ ConvLSTMCell(hidden[0], hidden[1], kernel),
63
+ ])
64
+ self.forecaster = nn.ModuleList([
65
+ ConvLSTMCell(0, hidden[0], kernel),
66
+ ConvLSTMCell(hidden[0], hidden[1], kernel),
67
+ ])
68
+ self.output = nn.Conv2d(sum(hidden), patch_channels, 1)
69
+
70
+ @staticmethod
71
+ def _zero_state(batch, channels, height, width, reference):
72
+ zeros = reference.new_zeros(batch, channels, height, width)
73
+ return zeros, zeros.clone()
74
+
75
+ def forward(self, sequence, return_states=False):
76
+ patched = patchify(sequence, self.patch_size)
77
+ batch, _, _, height, width = patched.shape
78
+ states = [self._zero_state(batch, cell.hidden_channels, height, width, sequence)
79
+ for cell in self.encoder]
80
+ for step in range(patched.shape[1]):
81
+ values = patched[:, step]
82
+ for index, cell in enumerate(self.encoder):
83
+ states[index] = cell(values, states[index])
84
+ values = states[index][0]
85
+ forecast_states = [(hidden.clone(), cell.clone()) for hidden, cell in states]
86
+ predictions, traces = [], []
87
+ for _ in range(self.output_frames):
88
+ forecast_states[0] = self.forecaster[0](None, forecast_states[0])
89
+ forecast_states[1] = self.forecaster[1](forecast_states[0][0], forecast_states[1])
90
+ hidden = torch.cat((forecast_states[0][0], forecast_states[1][0]), dim=1)
91
+ predictions.append(self.output(hidden))
92
+ traces.append([state[0] for state in forecast_states])
93
+ logits = torch.stack(predictions, dim=1)
94
+ images = unpatchify(logits.sigmoid(), self.patch_size)
95
+ return (images, logits, traces) if return_states else (images, logits)
scripts/fake_data.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate 5-to-15 synthetic radar sequences at the paper's 100x100 size."""
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import yaml
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+
11
+
12
+ def make_split(path, count, config, seed):
13
+ rng = np.random.default_rng(seed)
14
+ data = config["data"]
15
+ total = int(data["input_frames"]) + int(data["output_frames"])
16
+ height, width = int(data["height"]), int(data["width"])
17
+ y, x = np.mgrid[-1:1:complex(height), -1:1:complex(width)].astype(np.float32)
18
+ sequences = np.empty((count, total, 1, height, width), np.float32)
19
+ for sample in range(count):
20
+ centers = rng.uniform(-0.55, 0.55, (3, 2))
21
+ velocities = rng.uniform(-0.035, 0.035, (3, 2))
22
+ amplitudes = rng.uniform(0.25, 0.95, 3)
23
+ scales = rng.uniform(0.10, 0.28, 3)
24
+ for step in range(total):
25
+ field = np.zeros((height, width), np.float32)
26
+ for storm in range(3):
27
+ cy, cx = centers[storm] + velocities[storm] * step
28
+ distance = ((x - cx) ** 2 + (y - cy) ** 2) / (2 * scales[storm] ** 2)
29
+ field += amplitudes[storm] * np.exp(-distance)
30
+ sequences[sample, step, 0] = np.clip(field + rng.normal(0, 0.01, field.shape), 0, 1)
31
+ split = int(data["input_frames"])
32
+ np.savez_compressed(path, format_version=np.asarray(data["format_version"]),
33
+ data_source=np.asarray("synthetic_hko_radar_like"),
34
+ inputs=sequences[:, :split], targets=sequences[:, split:])
35
+
36
+
37
+ def main():
38
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
39
+ output = ROOT / config["data"]["root"]
40
+ output.mkdir(parents=True, exist_ok=True)
41
+ for offset, (filename, count) in enumerate((("train.npz", config["data"]["train_samples"]),
42
+ ("test.npz", config["data"]["test_samples"]))):
43
+ target = output / filename
44
+ if not target.exists():
45
+ make_split(target, int(count), config, int(config["seed"]) + offset)
46
+ print(f"generated={target.relative_to(ROOT)} input=5x1x100x100 target=15x1x100x100")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Forecast fifteen 6-minute radar frames from five observations."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+ import yaml
9
+ from torch.utils.data import DataLoader
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(ROOT))
14
+ from model.convlstm import ConvLSTM
15
+ from train import RadarDataset, device_from_config
16
+
17
+
18
+ def main():
19
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
20
+ device = device_from_config(config)
21
+ checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=True)
22
+ model = ConvLSTM(checkpoint["model_config"]).to(device)
23
+ model.load_state_dict(checkpoint["model"])
24
+ model.eval()
25
+ loader = DataLoader(RadarDataset(ROOT / config["data"]["root"] / "test.npz", config), batch_size=1)
26
+ inputs_all, targets_all, predictions_all = [], [], []
27
+ with torch.no_grad():
28
+ for inputs, targets in loader:
29
+ prediction, _ = model(inputs.to(device))
30
+ inputs_all.append(inputs.numpy())
31
+ targets_all.append(targets.numpy())
32
+ predictions_all.append(prediction.cpu().numpy())
33
+ output = ROOT / config["paths"]["inference_dir"] / "predictions.npz"
34
+ output.parent.mkdir(parents=True, exist_ok=True)
35
+ np.savez_compressed(output, inputs=np.concatenate(inputs_all), targets=np.concatenate(targets_all),
36
+ predictions=np.concatenate(predictions_all),
37
+ input_lead_minutes=np.arange(-24, 1, int(config["data"]["interval_minutes"]), dtype=np.int64),
38
+ forecast_lead_minutes=np.arange(1, int(config["data"]["output_frames"]) + 1, dtype=np.int64)
39
+ * int(config["data"]["interval_minutes"]),
40
+ normalized_value_range=np.asarray([0.0, 1.0], np.float32),
41
+ data_type=np.asarray("normalized_radar_echo_grayscale"))
42
+ print(f"predictions={output.relative_to(ROOT)}")
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
scripts/result.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute the paper's rainfall metrics and visualize the 90-minute forecast."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import matplotlib
7
+ matplotlib.use("Agg")
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import yaml
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+
15
+
16
+ def to_rainfall(values, config):
17
+ data = config["data"]
18
+ radar_db = values * (float(data["radar_db_max"]) - float(data["radar_db_min"])) + float(data["radar_db_min"])
19
+ return 10 ** ((radar_db - 10 * np.log10(float(data["zr_a"]))) / (10 * float(data["zr_b"])))
20
+
21
+
22
+ def main():
23
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
24
+ data = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
25
+ prediction, target = data["predictions"], data["targets"]
26
+ predicted_rain, target_rain = to_rainfall(prediction, config), to_rainfall(target, config)
27
+ threshold = float(config["data"]["rainfall_threshold_mm_h"])
28
+ forecast, observed = predicted_rain >= threshold, target_rain >= threshold
29
+ hits, misses = np.logical_and(forecast, observed).sum(), np.logical_and(~forecast, observed).sum()
30
+ false_alarms = np.logical_and(forecast, ~observed).sum()
31
+ eps = 1e-9
32
+ frame_correlation, per_step = [], []
33
+ for step in range(prediction.shape[1]):
34
+ left, right = prediction[:, step].reshape(-1), target[:, step].reshape(-1)
35
+ correlation = float(np.dot(left, right) / (np.sqrt(np.dot(left, left) * np.dot(right, right)) + eps))
36
+ frame_correlation.append(correlation)
37
+ step_forecast, step_observed = forecast[:, step], observed[:, step]
38
+ step_hits = np.logical_and(step_forecast, step_observed).sum()
39
+ step_misses = np.logical_and(~step_forecast, step_observed).sum()
40
+ step_false_alarms = np.logical_and(step_forecast, ~step_observed).sum()
41
+ per_step.append({
42
+ "lead_minutes": int(data["forecast_lead_minutes"][step]),
43
+ "rainfall_mse": float(np.mean((predicted_rain[:, step] - target_rain[:, step]) ** 2)),
44
+ "csi": float(step_hits / (step_hits + step_misses + step_false_alarms + eps)),
45
+ "far": float(step_false_alarms / (step_hits + step_false_alarms + eps)),
46
+ "pod": float(step_hits / (step_hits + step_misses + eps)),
47
+ "correlation": correlation,
48
+ })
49
+ metrics = {
50
+ "samples": int(len(prediction)),
51
+ "binary_cross_entropy": float(-(target * np.log(prediction.clip(1e-7, 1 - 1e-7)) +
52
+ (1 - target) * np.log((1 - prediction).clip(1e-7, 1))).mean()),
53
+ "rainfall_mse": float(np.mean((predicted_rain - target_rain) ** 2)),
54
+ "csi": float(hits / (hits + misses + false_alarms + eps)),
55
+ "far": float(false_alarms / (hits + false_alarms + eps)),
56
+ "pod": float(hits / (hits + misses + eps)),
57
+ "correlation": float(np.mean(frame_correlation)),
58
+ "per_forecast_step": per_step,
59
+ }
60
+ output = ROOT / config["paths"]["evaluation_dir"]
61
+ output.mkdir(parents=True, exist_ok=True)
62
+ (output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
63
+ steps = [0, 2, 5, 8, 11, 14]
64
+ figure, axes = plt.subplots(3, len(steps), figsize=(15, 7))
65
+ for column, step in enumerate(steps):
66
+ axes[0, column].imshow(target[0, step, 0], cmap="turbo", vmin=0, vmax=1)
67
+ axes[1, column].imshow(prediction[0, step, 0], cmap="turbo", vmin=0, vmax=1)
68
+ axes[2, column].imshow(np.abs(target[0, step, 0] - prediction[0, step, 0]), cmap="magma", vmin=0, vmax=1)
69
+ axes[0, column].set_title(f"+{(step + 1) * 6} min")
70
+ for axis in axes[:, column]:
71
+ axis.axis("off")
72
+ figure.tight_layout()
73
+ figure.savefig(output / "comparison.png", dpi=150)
74
+ plt.close(figure)
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
scripts/train.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train the ConvLSTM radar encoder-forecaster with full-sequence BPTT."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import torch
10
+ import yaml
11
+ from torch.nn.parallel import DistributedDataParallel
12
+ from torch.utils.data import DataLoader, Dataset, DistributedSampler
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(ROOT))
17
+ from model.convlstm import ConvLSTM
18
+
19
+
20
+ class RadarDataset(Dataset):
21
+ def __init__(self, path, config):
22
+ self.data = np.load(path)
23
+ data = config["data"]
24
+ if str(self.data["format_version"]) != data["format_version"]:
25
+ raise ValueError("incompatible radar data format")
26
+ expected_input = (int(data["input_frames"]), int(data["channels"]), int(data["height"]), int(data["width"]))
27
+ expected_target = (int(data["output_frames"]), int(data["channels"]), int(data["height"]), int(data["width"]))
28
+ if self.data["inputs"].shape[1:] != expected_input or self.data["targets"].shape[1:] != expected_target:
29
+ raise ValueError("radar tensors do not preserve the paper dimensions")
30
+
31
+ def __len__(self):
32
+ return len(self.data["inputs"])
33
+
34
+ def __getitem__(self, index):
35
+ return torch.from_numpy(self.data["inputs"][index]).float(), torch.from_numpy(self.data["targets"][index]).float()
36
+
37
+
38
+ def device_from_config(config, rank=0):
39
+ if config["runtime"]["device"] == "auto":
40
+ return torch.device("cuda", rank) if torch.cuda.is_available() else torch.device("cpu")
41
+ return torch.device(config["runtime"]["device"])
42
+
43
+
44
+ def main():
45
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
46
+ torch.manual_seed(int(config["seed"]))
47
+ distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
48
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
49
+ if distributed:
50
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
51
+ rank = torch.distributed.get_rank() if distributed else 0
52
+ device = device_from_config(config, local_rank)
53
+ dataset = RadarDataset(ROOT / config["data"]["root"] / "train.npz", config)
54
+ sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
55
+ loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler,
56
+ shuffle=sampler is None, num_workers=int(config["train"]["num_workers"]))
57
+ model = ConvLSTM(config["model"]).to(device)
58
+ if distributed:
59
+ model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
60
+ optimizer = torch.optim.RMSprop(model.parameters(), lr=float(config["train"]["learning_rate"]),
61
+ alpha=float(config["train"]["rmsprop_alpha"]),
62
+ weight_decay=float(config["train"]["weight_decay"]))
63
+ history = []
64
+ for epoch in range(int(config["train"]["epochs"])):
65
+ model.train()
66
+ total, steps = 0.0, 0
67
+ for inputs, targets in loader:
68
+ _, logits = model(inputs.to(device))
69
+ patched_target = torch.nn.functional.pixel_unshuffle(targets.to(device).flatten(0, 1),
70
+ int(config["model"]["patch_size"])).unflatten(0, targets.shape[:2])
71
+ loss = torch.nn.functional.binary_cross_entropy_with_logits(logits, patched_target)
72
+ optimizer.zero_grad(set_to_none=True)
73
+ loss.backward()
74
+ torch.nn.utils.clip_grad_norm_(model.parameters(), float(config["train"]["gradient_clip_norm"]))
75
+ optimizer.step()
76
+ total += float(loss.detach())
77
+ steps += 1
78
+ metrics = {"epoch": epoch + 1, "binary_cross_entropy": total / max(steps, 1)}
79
+ history.append(metrics)
80
+ if rank == 0:
81
+ print(f"epoch={epoch + 1} binary_cross_entropy={metrics['binary_cross_entropy']:.6f}")
82
+ if rank == 0:
83
+ checkpoint, metrics_path = ROOT / config["paths"]["checkpoint"], ROOT / config["paths"]["training_metrics"]
84
+ checkpoint.parent.mkdir(parents=True, exist_ok=True)
85
+ metrics_path.parent.mkdir(parents=True, exist_ok=True)
86
+ state = model.module.state_dict() if distributed else model.state_dict()
87
+ torch.save({"model": state, "model_config": config["model"],
88
+ "format_version": config["data"]["format_version"]}, checkpoint)
89
+ metrics_path.write_text(json.dumps({"history": history}, indent=2) + "\n")
90
+ if distributed:
91
+ torch.distributed.destroy_process_group()
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
weight/.gitkeep ADDED
File without changes