Upload folder using huggingface_hub
Browse files- README.md +138 -0
- conf/config.yaml +41 -0
- config.json +40 -0
- configuration.json +1 -0
- model/evolution_module.py +91 -0
- model/evolution_network.py +48 -0
- model/generation_module.py +94 -0
- model/generative_network.py +61 -0
- model/layers_utils.py +85 -0
- model/model_factory.py +32 -0
- model/noise_projector.py +39 -0
- model/nowcastnet.py +74 -0
- scripts/data_loader.py +101 -0
- scripts/fake_data.py +48 -0
- scripts/inference.py +49 -0
- scripts/result.py +160 -0
- scripts/train.py +150 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: PyTorch
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Precipitation Nowcasting
|
| 10 |
+
- Weather Forecast
|
| 11 |
+
- MRMS
|
| 12 |
+
tasks: []
|
| 13 |
+
datasets:
|
| 14 |
+
- OneScience/MRMS
|
| 15 |
+
---
|
| 16 |
+
<p align="center">
|
| 17 |
+
<strong>
|
| 18 |
+
<span style="font-size: 30px;">NowcastNet_Earth</span>
|
| 19 |
+
</strong>
|
| 20 |
+
</p>
|
| 21 |
+
|
| 22 |
+
# Model Introduction
|
| 23 |
+
|
| 24 |
+
NowcastNet is a large model for extreme-precipitation nowcasting proposed by a team from Tsinghua University. The research was published in the main edition of *Nature*.
|
| 25 |
+
|
| 26 |
+
Paper: Skilful nowcasting of extreme precipitation with NowcastNet
|
| 27 |
+
|
| 28 |
+
https://www.nature.com/articles/s41586-023-06184-4
|
| 29 |
+
|
| 30 |
+
# Model Description
|
| 31 |
+
|
| 32 |
+
NowcastNet combines data-driven deep learning with numerical methods based on physical equations in a unified framework. Two core networks work together to model precipitation processes at different spatial scales.
|
| 33 |
+
|
| 34 |
+
# Use Cases
|
| 35 |
+
|
| 36 |
+
| Scenario | Description |
|
| 37 |
+
| :---: | :--- |
|
| 38 |
+
| Short-term precipitation nowcasting training | Train NowcastNet with MRMS data. |
|
| 39 |
+
| Local quick validation | Use synthetic data to check data loading, model training and inference, and visualization of inference results. |
|
| 40 |
+
| ModelScope / OneCode execution | Download the standalone model package, install dependencies, and run the scripts directly. |
|
| 41 |
+
| Multi-GPU training | Use `torchrun` for data-parallel training across multiple GPUs or accelerators on one host. |
|
| 42 |
+
|
| 43 |
+
# Usage Guide
|
| 44 |
+
|
| 45 |
+
## 1. OneCode Usage
|
| 46 |
+
|
| 47 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 48 |
+
|
| 49 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 50 |
+
|
| 51 |
+
## 2. Manual Installation and Usage
|
| 52 |
+
|
| 53 |
+
**Hardware Requirements**
|
| 54 |
+
|
| 55 |
+
- A GPU or DCU is recommended.
|
| 56 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 57 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching your cluster, is recommended.
|
| 58 |
+
|
| 59 |
+
### Download the Model Package
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
hf download OneScience-Group/NowcastNet --local-dir ./NowcastNet
|
| 63 |
+
cd NowcastNet
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Install the Runtime Environment
|
| 67 |
+
|
| 68 |
+
**DCU Environment**
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
# Please activate DTK and CONDA first
|
| 72 |
+
conda create -n onescience311 python=3.11 -y
|
| 73 |
+
conda activate onescience311
|
| 74 |
+
# uv installation is supported
|
| 75 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
**GPU Environment**
|
| 79 |
+
```bash
|
| 80 |
+
# Please activate CONDA first
|
| 81 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 82 |
+
conda activate onescience311
|
| 83 |
+
# uv installation is supported
|
| 84 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Generate Synthetic Data
|
| 88 |
+
|
| 89 |
+
Synthetic data is only used to check the data protocol and program flow; it does not represent real MRMS data or forecast quality:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python scripts/fake_data.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Training
|
| 96 |
+
|
| 97 |
+
Single GPU:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
python scripts/train.py
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Multi-GPU:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
torchrun --nproc_per_node=8 scripts/train.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Training weights are saved to `data/checkpoints/` by default.
|
| 110 |
+
|
| 111 |
+
### Training Weights
|
| 112 |
+
|
| 113 |
+
This repository provides weights trained on MRMS data in the `weight/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
|
| 114 |
+
|
| 115 |
+
### Inference
|
| 116 |
+
|
| 117 |
+
Inference reads the training weights from `data/checkpoints/` by default:
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
python scripts/inference.py
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Evaluation and Visualization
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
python scripts/result.py
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
# Official OneScience Resources
|
| 130 |
+
|
| 131 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 132 |
+
| --- | --- | --- |
|
| 133 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 134 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 135 |
+
|
| 136 |
+
# Citation and License
|
| 137 |
+
|
| 138 |
+
- This repository is a reproduction of the original NowcastNet paper.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
input_length: 9
|
| 3 |
+
total_length: 29
|
| 4 |
+
img_height: 512
|
| 5 |
+
img_width: 512
|
| 6 |
+
img_ch: 2
|
| 7 |
+
ngf: 32
|
| 8 |
+
device: auto
|
| 9 |
+
pretrained_model: ""
|
| 10 |
+
checkpoint_dir: "./data/checkpoint"
|
| 11 |
+
checkpoint_prefix: "model_bak"
|
| 12 |
+
|
| 13 |
+
data:
|
| 14 |
+
data_dir: "./data/data"
|
| 15 |
+
split: train
|
| 16 |
+
image_height: 512
|
| 17 |
+
image_width: 512
|
| 18 |
+
total_length: 29
|
| 19 |
+
input_data_type: float32
|
| 20 |
+
scale_divisor: 10.0
|
| 21 |
+
offset: 3.0
|
| 22 |
+
clip_max: 128.0
|
| 23 |
+
batch_size: 1
|
| 24 |
+
num_workers: 0
|
| 25 |
+
|
| 26 |
+
training:
|
| 27 |
+
epochs: 10
|
| 28 |
+
lr: 0.0003
|
| 29 |
+
weight_decay: 0.0
|
| 30 |
+
seed: 42
|
| 31 |
+
mode: generator
|
| 32 |
+
output_dir: "./results/training"
|
| 33 |
+
loss_history_file: "loss_history.json"
|
| 34 |
+
|
| 35 |
+
inference:
|
| 36 |
+
output_dir: "./results/inference"
|
| 37 |
+
num_samples: 1
|
| 38 |
+
threshold: 16.0
|
| 39 |
+
|
| 40 |
+
visualization:
|
| 41 |
+
output_dir: "./results/visualization"
|
config.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "NowcastNet",
|
| 3 |
+
"model_type": "nowcastnet",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"Net"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "atmosphere",
|
| 9 |
+
"task": "extreme-precipitation-nowcasting",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/nowcastnet.py",
|
| 12 |
+
"scope": "evolution and generative networks for radar precipitation nowcasting"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "physics-guided evolution network plus conditional generative network",
|
| 16 |
+
"input_length": 9,
|
| 17 |
+
"total_length": 29,
|
| 18 |
+
"prediction_length": 20,
|
| 19 |
+
"image_shape": [
|
| 20 |
+
512,
|
| 21 |
+
512
|
| 22 |
+
],
|
| 23 |
+
"input_channels": 1,
|
| 24 |
+
"base_channels": 32,
|
| 25 |
+
"noise_channels": 32
|
| 26 |
+
},
|
| 27 |
+
"data": {
|
| 28 |
+
"dataset": "MRMS",
|
| 29 |
+
"protocol": "synthetic_radar_precipitation",
|
| 30 |
+
"scale_divisor": 10.0,
|
| 31 |
+
"offset": 3.0,
|
| 32 |
+
"clip_max": 128.0
|
| 33 |
+
},
|
| 34 |
+
"configuration_sources": [
|
| 35 |
+
"conf/config.yaml",
|
| 36 |
+
"model/nowcastnet.py",
|
| 37 |
+
"model/evolution_network.py",
|
| 38 |
+
"model/generative_network.py"
|
| 39 |
+
]
|
| 40 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Jax","task":"other"}
|
model/evolution_module.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch.nn.utils import spectral_norm
|
| 5 |
+
|
| 6 |
+
class DoubleConv(nn.Module):
|
| 7 |
+
|
| 8 |
+
def __init__(self, in_channels, out_channels, kernel=3, mid_channels=None):
|
| 9 |
+
super().__init__()
|
| 10 |
+
if not mid_channels:
|
| 11 |
+
mid_channels = out_channels
|
| 12 |
+
|
| 13 |
+
self.double_conv = nn.Sequential(
|
| 14 |
+
nn.BatchNorm2d(in_channels),
|
| 15 |
+
nn.ReLU(inplace=True),
|
| 16 |
+
spectral_norm(nn.Conv2d(in_channels, mid_channels, kernel_size=kernel, padding=kernel//2)),
|
| 17 |
+
nn.BatchNorm2d(mid_channels),
|
| 18 |
+
nn.ReLU(inplace=True),
|
| 19 |
+
spectral_norm(nn.Conv2d(mid_channels, out_channels, kernel_size=kernel, padding=kernel//2)),
|
| 20 |
+
)
|
| 21 |
+
self.single_conv = nn.Sequential(
|
| 22 |
+
nn.BatchNorm2d(in_channels),
|
| 23 |
+
spectral_norm(nn.Conv2d(in_channels, out_channels, kernel_size=kernel, padding=kernel // 2))
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def forward(self, x):
|
| 27 |
+
shortcut = self.single_conv(x)
|
| 28 |
+
x = self.double_conv(x)
|
| 29 |
+
x = x + shortcut
|
| 30 |
+
return x
|
| 31 |
+
|
| 32 |
+
class Down(nn.Module):
|
| 33 |
+
|
| 34 |
+
def __init__(self, in_channels, out_channels, kernel=3):
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.maxpool_conv = nn.Sequential(
|
| 37 |
+
nn.MaxPool2d(2),
|
| 38 |
+
DoubleConv(in_channels, out_channels, kernel)
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
def forward(self, x):
|
| 42 |
+
x = self.maxpool_conv(x)
|
| 43 |
+
return x
|
| 44 |
+
|
| 45 |
+
class Up(nn.Module):
|
| 46 |
+
|
| 47 |
+
def __init__(self, in_channels, out_channels, bilinear=True, kernel=3):
|
| 48 |
+
super().__init__()
|
| 49 |
+
if bilinear:
|
| 50 |
+
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
|
| 51 |
+
self.conv = DoubleConv(in_channels, out_channels, kernel=kernel, mid_channels=in_channels // 2)
|
| 52 |
+
else:
|
| 53 |
+
self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
|
| 54 |
+
self.conv = DoubleConv(in_channels, out_channels, kernel)
|
| 55 |
+
|
| 56 |
+
def forward(self, x1, x2):
|
| 57 |
+
x1 = self.up(x1)
|
| 58 |
+
# input is CHW
|
| 59 |
+
diffY = x2.size()[2] - x1.size()[2]
|
| 60 |
+
diffX = x2.size()[3] - x1.size()[3]
|
| 61 |
+
|
| 62 |
+
x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2,
|
| 63 |
+
diffY // 2, diffY - diffY // 2])
|
| 64 |
+
x = torch.cat([x2, x1], dim=1)
|
| 65 |
+
return self.conv(x)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class Up_S(nn.Module):
|
| 69 |
+
|
| 70 |
+
def __init__(self, in_channels, out_channels, bilinear=True, kernel=3):
|
| 71 |
+
super().__init__()
|
| 72 |
+
if bilinear:
|
| 73 |
+
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
|
| 74 |
+
self.conv = DoubleConv(in_channels, out_channels, kernel=kernel, mid_channels=in_channels)
|
| 75 |
+
else:
|
| 76 |
+
self.up = nn.ConvTranspose2d(in_channels, in_channels, kernel_size=2, stride=2)
|
| 77 |
+
self.conv = DoubleConv(in_channels, out_channels, kernel)
|
| 78 |
+
|
| 79 |
+
def forward(self, x):
|
| 80 |
+
x = self.up(x)
|
| 81 |
+
return self.conv(x)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class OutConv(nn.Module):
|
| 85 |
+
def __init__(self, in_channels, out_channels):
|
| 86 |
+
super(OutConv, self).__init__()
|
| 87 |
+
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1)
|
| 88 |
+
|
| 89 |
+
def forward(self, x):
|
| 90 |
+
return self.conv(x)
|
| 91 |
+
|
model/evolution_network.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn.functional as F
|
| 2 |
+
from .evolution_module import *
|
| 3 |
+
|
| 4 |
+
class Evolution_Network(nn.Module):
|
| 5 |
+
def __init__(self, n_channels, n_classes, base_c=64, bilinear=True):
|
| 6 |
+
super(Evolution_Network, self).__init__()
|
| 7 |
+
self.n_channels = n_channels
|
| 8 |
+
self.n_classes = n_classes
|
| 9 |
+
self.bilinear = bilinear
|
| 10 |
+
base_c = base_c
|
| 11 |
+
self.inc = DoubleConv(n_channels, base_c)
|
| 12 |
+
self.down1 = Down(base_c * 1, base_c * 2)
|
| 13 |
+
self.down2 = Down(base_c * 2, base_c * 4)
|
| 14 |
+
self.down3 = Down(base_c * 4, base_c * 8)
|
| 15 |
+
factor = 2 if bilinear else 1
|
| 16 |
+
self.down4 = Down(base_c * 8, base_c * 16 // factor)
|
| 17 |
+
|
| 18 |
+
self.up1 = Up(base_c * 16, base_c * 8 // factor, bilinear)
|
| 19 |
+
self.up2 = Up(base_c * 8, base_c * 4 // factor, bilinear)
|
| 20 |
+
self.up3 = Up(base_c * 4, base_c * 2 // factor, bilinear)
|
| 21 |
+
self.up4 = Up(base_c * 2, base_c * 1, bilinear)
|
| 22 |
+
self.outc = OutConv(base_c * 1, n_classes)
|
| 23 |
+
self.gamma = nn.Parameter(torch.zeros(1, n_classes, 1, 1), requires_grad=True)
|
| 24 |
+
|
| 25 |
+
self.up1_v = Up(base_c * 16, base_c * 8 // factor, bilinear)
|
| 26 |
+
self.up2_v = Up(base_c * 8, base_c * 4 // factor, bilinear)
|
| 27 |
+
self.up3_v = Up(base_c * 4, base_c * 2 // factor, bilinear)
|
| 28 |
+
self.up4_v = Up(base_c * 2, base_c * 1, bilinear)
|
| 29 |
+
self.outc_v = OutConv(base_c * 1, n_classes * 2)
|
| 30 |
+
|
| 31 |
+
def forward(self, x):
|
| 32 |
+
x1 = self.inc(x)
|
| 33 |
+
x2 = self.down1(x1)
|
| 34 |
+
x3 = self.down2(x2)
|
| 35 |
+
x4 = self.down3(x3)
|
| 36 |
+
x5 = self.down4(x4)
|
| 37 |
+
x = self.up1(x5, x4)
|
| 38 |
+
x = self.up2(x, x3)
|
| 39 |
+
x = self.up3(x, x2)
|
| 40 |
+
x = self.up4(x, x1)
|
| 41 |
+
x = self.outc(x) * self.gamma
|
| 42 |
+
|
| 43 |
+
v = self.up1_v(x5, x4)
|
| 44 |
+
v = self.up2_v(v, x3)
|
| 45 |
+
v = self.up3_v(v, x2)
|
| 46 |
+
v = self.up4_v(v, x1)
|
| 47 |
+
v = self.outc_v(v)
|
| 48 |
+
return x, v
|
model/generation_module.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPADE Module and Block are adapted from Nvidia SPADE project (https://github.com/NVlabs/SPADE).
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
import sys
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
import torch.nn.utils.spectral_norm as spectral_norm
|
| 10 |
+
|
| 11 |
+
class GenBlock(nn.Module):
|
| 12 |
+
def __init__(self, fin, fout, opt, use_se=False, dilation=1, double_conv=False):
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.learned_shortcut = (fin != fout)
|
| 15 |
+
fmiddle = min(fin, fout)
|
| 16 |
+
self.opt = opt
|
| 17 |
+
self.double_conv = double_conv
|
| 18 |
+
|
| 19 |
+
self.pad = nn.ReflectionPad2d(dilation)
|
| 20 |
+
self.conv_0 = nn.Conv2d(fin, fmiddle, kernel_size=3, padding=0, dilation=dilation)
|
| 21 |
+
self.conv_1 = nn.Conv2d(fmiddle, fout, kernel_size=3, padding=0, dilation=dilation)
|
| 22 |
+
|
| 23 |
+
if self.learned_shortcut:
|
| 24 |
+
self.conv_s = nn.Conv2d(fin, fout, kernel_size=1, bias=False)
|
| 25 |
+
|
| 26 |
+
self.conv_0 = spectral_norm(self.conv_0)
|
| 27 |
+
self.conv_1 = spectral_norm(self.conv_1)
|
| 28 |
+
if self.learned_shortcut:
|
| 29 |
+
self.conv_s = spectral_norm(self.conv_s)
|
| 30 |
+
|
| 31 |
+
ic = opt.evo_ic
|
| 32 |
+
|
| 33 |
+
self.norm_0 = SPADE(fin, ic)
|
| 34 |
+
self.norm_1 = SPADE(fmiddle, ic)
|
| 35 |
+
if self.learned_shortcut:
|
| 36 |
+
self.norm_s = SPADE(fin, ic)
|
| 37 |
+
|
| 38 |
+
def forward(self, x, evo):
|
| 39 |
+
x_s = self.shortcut(x, evo)
|
| 40 |
+
dx = self.conv_0(self.pad(self.actvn(self.norm_0(x, evo))))
|
| 41 |
+
if self.double_conv:
|
| 42 |
+
dx = self.conv_1(self.pad(self.actvn(self.norm_1(dx, evo))))
|
| 43 |
+
|
| 44 |
+
out = x_s + dx
|
| 45 |
+
|
| 46 |
+
return out
|
| 47 |
+
|
| 48 |
+
def shortcut(self, x, evo):
|
| 49 |
+
if self.learned_shortcut:
|
| 50 |
+
x_s = self.conv_s(self.norm_s(x, evo))
|
| 51 |
+
else:
|
| 52 |
+
x_s = x
|
| 53 |
+
return x_s
|
| 54 |
+
|
| 55 |
+
def actvn(self, x):
|
| 56 |
+
return F.leaky_relu(x, 2e-1)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class SPADE(nn.Module):
|
| 60 |
+
def __init__(self, norm_nc, label_nc):
|
| 61 |
+
super().__init__()
|
| 62 |
+
|
| 63 |
+
ks = 3
|
| 64 |
+
|
| 65 |
+
self.param_free_norm = nn.InstanceNorm2d(norm_nc, affine=False)
|
| 66 |
+
nhidden = 64
|
| 67 |
+
ks = 3
|
| 68 |
+
pw = ks // 2
|
| 69 |
+
self.mlp_shared = nn.Sequential(
|
| 70 |
+
nn.ReflectionPad2d(pw),
|
| 71 |
+
nn.Conv2d(label_nc, nhidden, kernel_size=ks, padding=0),
|
| 72 |
+
nn.ReLU()
|
| 73 |
+
)
|
| 74 |
+
self.pad = nn.ReflectionPad2d(pw)
|
| 75 |
+
self.mlp_gamma = nn.Conv2d(nhidden, norm_nc, kernel_size=ks, padding=0)
|
| 76 |
+
self.mlp_beta = nn.Conv2d(nhidden, norm_nc, kernel_size=ks, padding=0)
|
| 77 |
+
|
| 78 |
+
def forward(self, x, evo):
|
| 79 |
+
|
| 80 |
+
normalized = self.param_free_norm(x)
|
| 81 |
+
evo = F.adaptive_avg_pool2d(evo, output_size=x.size()[2:])
|
| 82 |
+
|
| 83 |
+
actv = self.mlp_shared(evo)
|
| 84 |
+
|
| 85 |
+
gamma = self.mlp_gamma(self.pad(actv))
|
| 86 |
+
beta = self.mlp_beta(self.pad(actv))
|
| 87 |
+
|
| 88 |
+
out = normalized * (1 + gamma) + beta
|
| 89 |
+
|
| 90 |
+
return out
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
|
model/generative_network.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch.autograd import Function
|
| 6 |
+
from .generation_module import GenBlock
|
| 7 |
+
from .evolution_module import *
|
| 8 |
+
|
| 9 |
+
class Generative_Encoder(nn.Module):
|
| 10 |
+
def __init__(self, n_channels, base_c=64):
|
| 11 |
+
super(Generative_Encoder, self).__init__()
|
| 12 |
+
base_c = base_c
|
| 13 |
+
self.inc = DoubleConv(n_channels, base_c, kernel=3)
|
| 14 |
+
self.down1 = Down(base_c * 1, base_c * 2, 3)
|
| 15 |
+
self.down2 = Down(base_c * 2, base_c * 4, 3)
|
| 16 |
+
self.down3 = Down(base_c * 4, base_c * 8, 3)
|
| 17 |
+
|
| 18 |
+
def forward(self, x):
|
| 19 |
+
x = self.inc(x)
|
| 20 |
+
x = self.down1(x)
|
| 21 |
+
x = self.down2(x)
|
| 22 |
+
x = self.down3(x)
|
| 23 |
+
return x
|
| 24 |
+
|
| 25 |
+
class Generative_Decoder(nn.Module):
|
| 26 |
+
def __init__(self, opt):
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.opt = opt
|
| 29 |
+
nf = opt.ngf
|
| 30 |
+
|
| 31 |
+
ic = opt.ic_feature
|
| 32 |
+
self.fc = nn.Conv2d(ic, 8 * nf, 3, padding=1)
|
| 33 |
+
|
| 34 |
+
self.head_0 = GenBlock(8 * nf, 8 * nf, opt)
|
| 35 |
+
|
| 36 |
+
self.G_middle_0 = GenBlock(8 * nf, 4 * nf, opt, double_conv=True)
|
| 37 |
+
self.G_middle_1 = GenBlock(4 * nf, 4 * nf, opt, double_conv=True)
|
| 38 |
+
|
| 39 |
+
self.up_0 = GenBlock(4 * nf, 2 * nf, opt)
|
| 40 |
+
|
| 41 |
+
self.up_1 = GenBlock(2 * nf, 1 * nf, opt, double_conv=True)
|
| 42 |
+
self.up_2 = GenBlock(1 * nf, 1 * nf, opt, double_conv=True)
|
| 43 |
+
|
| 44 |
+
final_nc = nf * 1
|
| 45 |
+
|
| 46 |
+
self.conv_img = nn.Conv2d(final_nc, self.opt.gen_oc, 3, padding=1)
|
| 47 |
+
self.up = nn.Upsample(scale_factor=2)
|
| 48 |
+
|
| 49 |
+
def forward(self, x, evo):
|
| 50 |
+
x = self.fc(x)
|
| 51 |
+
x = self.head_0(x, evo)
|
| 52 |
+
x = self.up(x)
|
| 53 |
+
x = self.G_middle_0(x, evo)
|
| 54 |
+
x = self.G_middle_1(x, evo)
|
| 55 |
+
x = self.up(x)
|
| 56 |
+
x = self.up_0(x, evo)
|
| 57 |
+
x = self.up(x)
|
| 58 |
+
x = self.up_1(x, evo)
|
| 59 |
+
x = self.up_2(x, evo)
|
| 60 |
+
x = self.conv_img(F.leaky_relu(x, 2e-1))
|
| 61 |
+
return x
|
model/layers_utils.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn.functional as F
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def make_grid(input):
|
| 7 |
+
B, C, H, W = input.size()
|
| 8 |
+
# mesh grid
|
| 9 |
+
device = input.device
|
| 10 |
+
xx = torch.arange(0, W, device=device).view(1, -1).repeat(H, 1)
|
| 11 |
+
yy = torch.arange(0, H, device=device).view(-1, 1).repeat(1, W)
|
| 12 |
+
xx = xx.view(1, 1, H, W).repeat(B, 1, 1, 1)
|
| 13 |
+
yy = yy.view(1, 1, H, W).repeat(B, 1, 1, 1)
|
| 14 |
+
grid = torch.cat((xx, yy), 1).float()
|
| 15 |
+
|
| 16 |
+
return grid
|
| 17 |
+
|
| 18 |
+
def warp(input, flow, grid, mode="bilinear", padding_mode="zeros"):
|
| 19 |
+
|
| 20 |
+
B, C, H, W = input.size()
|
| 21 |
+
vgrid = grid + flow
|
| 22 |
+
|
| 23 |
+
vgrid[:, 0, :, :] = 2.0 * vgrid[:, 0, :, :].clone() / max(W - 1, 1) - 1.0
|
| 24 |
+
vgrid[:, 1, :, :] = 2.0 * vgrid[:, 1, :, :].clone() / max(H - 1, 1) - 1.0
|
| 25 |
+
vgrid = vgrid.permute(0, 2, 3, 1)
|
| 26 |
+
output = torch.nn.functional.grid_sample(input, vgrid, padding_mode=padding_mode, mode=mode, align_corners=True)
|
| 27 |
+
return output
|
| 28 |
+
|
| 29 |
+
def l2normalize(v, eps=1e-12):
|
| 30 |
+
return v / (v.norm() + eps)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class spectral_norm(nn.Module):
|
| 34 |
+
def __init__(self, module, name='weight', power_iterations=1):
|
| 35 |
+
super(spectral_norm, self).__init__()
|
| 36 |
+
self.module = module
|
| 37 |
+
self.name = name
|
| 38 |
+
self.power_iterations = power_iterations
|
| 39 |
+
if not self._made_params():
|
| 40 |
+
self._make_params()
|
| 41 |
+
|
| 42 |
+
def _update_u_v(self):
|
| 43 |
+
u = getattr(self.module, self.name + "_u")
|
| 44 |
+
v = getattr(self.module, self.name + "_v")
|
| 45 |
+
w = getattr(self.module, self.name + "_bar")
|
| 46 |
+
|
| 47 |
+
height = w.data.shape[0]
|
| 48 |
+
for _ in range(self.power_iterations):
|
| 49 |
+
v.data = l2normalize(torch.mv(torch.t(w.view(height,-1).data), u.data))
|
| 50 |
+
u.data = l2normalize(torch.mv(w.view(height,-1).data, v.data))
|
| 51 |
+
|
| 52 |
+
sigma = u.dot(w.view(height, -1).mv(v))
|
| 53 |
+
setattr(self.module, self.name, w / sigma.expand_as(w))
|
| 54 |
+
|
| 55 |
+
def _made_params(self):
|
| 56 |
+
try:
|
| 57 |
+
u = getattr(self.module, self.name + "_u")
|
| 58 |
+
v = getattr(self.module, self.name + "_v")
|
| 59 |
+
w = getattr(self.module, self.name + "_bar")
|
| 60 |
+
return True
|
| 61 |
+
except AttributeError:
|
| 62 |
+
return False
|
| 63 |
+
|
| 64 |
+
def _make_params(self):
|
| 65 |
+
w = getattr(self.module, self.name)
|
| 66 |
+
|
| 67 |
+
height = w.data.shape[0]
|
| 68 |
+
width = w.view(height, -1).data.shape[1]
|
| 69 |
+
|
| 70 |
+
u = nn.Parameter(w.data.new(height).normal_(0, 1), requires_grad=False)
|
| 71 |
+
v = nn.Parameter(w.data.new(width).normal_(0, 1), requires_grad=False)
|
| 72 |
+
u.data = l2normalize(u.data)
|
| 73 |
+
v.data = l2normalize(v.data)
|
| 74 |
+
w_bar = nn.Parameter(w.data)
|
| 75 |
+
|
| 76 |
+
del self.module._parameters[self.name]
|
| 77 |
+
|
| 78 |
+
self.module.register_parameter(self.name + "_u", u)
|
| 79 |
+
self.module.register_parameter(self.name + "_v", v)
|
| 80 |
+
self.module.register_parameter(self.name + "_bar", w_bar)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def forward(self, *args):
|
| 84 |
+
self._update_u_v()
|
| 85 |
+
return self.module.forward(*args)
|
model/model_factory.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Factory and checkpoint helpers for the project-local NowcastNet model."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from types import SimpleNamespace
|
| 6 |
+
from typing import Mapping
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from .nowcastnet import Net
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def build_model(config, device: torch.device | str = "cpu") -> Net:
|
| 14 |
+
"""Build a model from a mapping or namespace without requiring OneScience."""
|
| 15 |
+
if isinstance(config, Mapping):
|
| 16 |
+
config = SimpleNamespace(**config)
|
| 17 |
+
config.device = torch.device(device)
|
| 18 |
+
config.evo_ic = config.total_length - config.input_length
|
| 19 |
+
config.gen_oc = config.total_length - config.input_length
|
| 20 |
+
config.ic_feature = config.ngf * 10
|
| 21 |
+
return Net(config).to(config.device)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_checkpoint(model: torch.nn.Module, path: str, device: torch.device | str = "cpu") -> Mapping:
|
| 25 |
+
try:
|
| 26 |
+
state = torch.load(path, map_location=device, weights_only=True)
|
| 27 |
+
except TypeError: # torch < 2.0
|
| 28 |
+
state = torch.load(path, map_location=device)
|
| 29 |
+
checkpoint = state if isinstance(state, Mapping) else {}
|
| 30 |
+
model_state = checkpoint["state_dict"] if "state_dict" in checkpoint else state
|
| 31 |
+
model.load_state_dict(model_state, strict=True)
|
| 32 |
+
return checkpoint
|
model/noise_projector.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from .layers_utils import spectral_norm
|
| 4 |
+
|
| 5 |
+
class Noise_Projector(nn.Module):
|
| 6 |
+
def __init__(self, input_length, configs):
|
| 7 |
+
super(Noise_Projector, self).__init__()
|
| 8 |
+
self.input_length = input_length
|
| 9 |
+
self.conv_first = spectral_norm(nn.Conv2d(self.input_length, self.input_length * 2, kernel_size=3, padding=1))
|
| 10 |
+
self.L1 = ProjBlock(self.input_length * 2, self.input_length * 4)
|
| 11 |
+
self.L2 = ProjBlock(self.input_length * 4, self.input_length * 8)
|
| 12 |
+
self.L3 = ProjBlock(self.input_length * 8, self.input_length * 16)
|
| 13 |
+
self.L4 = ProjBlock(self.input_length * 16, self.input_length * 32)
|
| 14 |
+
|
| 15 |
+
def forward(self, x):
|
| 16 |
+
x = self.conv_first(x)
|
| 17 |
+
x = self.L1(x)
|
| 18 |
+
x = self.L2(x)
|
| 19 |
+
x = self.L3(x)
|
| 20 |
+
x = self.L4(x)
|
| 21 |
+
|
| 22 |
+
return x
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ProjBlock(nn.Module):
|
| 26 |
+
def __init__(self, in_channel, out_channel):
|
| 27 |
+
super(ProjBlock, self).__init__()
|
| 28 |
+
self.one_conv = spectral_norm(nn.Conv2d(in_channel, out_channel-in_channel, kernel_size=1, padding=0))
|
| 29 |
+
self.double_conv = nn.Sequential(
|
| 30 |
+
spectral_norm(nn.Conv2d(in_channel, out_channel, kernel_size=3, padding=1)),
|
| 31 |
+
nn.ReLU(),
|
| 32 |
+
spectral_norm(nn.Conv2d(out_channel, out_channel, kernel_size=3, padding=1))
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def forward(self, x):
|
| 36 |
+
x1 = torch.cat([x, self.one_conv(x)], dim=1)
|
| 37 |
+
x2 = self.double_conv(x)
|
| 38 |
+
output = x1 + x2
|
| 39 |
+
return output
|
model/nowcastnet.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
import numpy as np
|
| 5 |
+
from .layers_utils import warp, make_grid
|
| 6 |
+
from .generative_network import Generative_Encoder, Generative_Decoder
|
| 7 |
+
from .evolution_network import Evolution_Network
|
| 8 |
+
from .noise_projector import Noise_Projector
|
| 9 |
+
|
| 10 |
+
class Net(nn.Module):
|
| 11 |
+
def __init__(self, configs):
|
| 12 |
+
super(Net, self).__init__()
|
| 13 |
+
self.configs = configs
|
| 14 |
+
self.pred_length = self.configs.total_length - self.configs.input_length
|
| 15 |
+
|
| 16 |
+
self.evo_net = Evolution_Network(self.configs.input_length, self.pred_length, base_c=32)
|
| 17 |
+
self.gen_enc = Generative_Encoder(self.configs.total_length, base_c=self.configs.ngf)
|
| 18 |
+
self.gen_dec = Generative_Decoder(self.configs)
|
| 19 |
+
self.proj = Noise_Projector(self.configs.ngf, configs)
|
| 20 |
+
|
| 21 |
+
sample_tensor = torch.zeros(1, 1, self.configs.img_height, self.configs.img_width)
|
| 22 |
+
self.register_buffer("grid", make_grid(sample_tensor), persistent=False)
|
| 23 |
+
|
| 24 |
+
def forward(self, all_frames):
|
| 25 |
+
if all_frames.ndim == 4:
|
| 26 |
+
all_frames = all_frames.unsqueeze(-1)
|
| 27 |
+
if all_frames.ndim != 5 or all_frames.shape[-1] < 1:
|
| 28 |
+
raise ValueError("Expected frames with shape [B,T,H,W,C], C>=1")
|
| 29 |
+
all_frames = all_frames[:, :, :, :, :1]
|
| 30 |
+
|
| 31 |
+
frames = all_frames.permute(0, 1, 4, 2, 3)
|
| 32 |
+
batch = frames.shape[0]
|
| 33 |
+
height = frames.shape[3]
|
| 34 |
+
width = frames.shape[4]
|
| 35 |
+
|
| 36 |
+
# Input Frames
|
| 37 |
+
input_frames = frames[:, :self.configs.input_length]
|
| 38 |
+
input_frames = input_frames.reshape(batch, self.configs.input_length, height, width)
|
| 39 |
+
|
| 40 |
+
# Evolution Network
|
| 41 |
+
intensity, motion = self.evo_net(input_frames)
|
| 42 |
+
motion_ = motion.reshape(batch, self.pred_length, 2, height, width)
|
| 43 |
+
intensity_ = intensity.reshape(batch, self.pred_length, 1, height, width)
|
| 44 |
+
series = []
|
| 45 |
+
last_frames = all_frames[:, (self.configs.input_length - 1):self.configs.input_length, :, :, 0]
|
| 46 |
+
grid = self.grid.to(frames.device).repeat(batch, 1, 1, 1)
|
| 47 |
+
for i in range(self.pred_length):
|
| 48 |
+
last_frames = warp(last_frames, motion_[:, i], grid, mode="nearest", padding_mode="border")
|
| 49 |
+
last_frames = last_frames + intensity_[:, i]
|
| 50 |
+
series.append(last_frames)
|
| 51 |
+
evo_result = torch.cat(series, dim=1)
|
| 52 |
+
|
| 53 |
+
evo_result = evo_result/128
|
| 54 |
+
|
| 55 |
+
# Generative Network
|
| 56 |
+
evo_feature = self.gen_enc(torch.cat([input_frames, evo_result], dim=1))
|
| 57 |
+
|
| 58 |
+
noise = torch.randn(batch, self.configs.ngf, max(1, height // 32), max(1, width // 32), device=frames.device)
|
| 59 |
+
projected = self.proj(noise)
|
| 60 |
+
# The official reshape/permute is a 4x pixel shuffle: 32*ngf
|
| 61 |
+
# projected channels become 2*ngf channels at H/8 x W/8. Expressing
|
| 62 |
+
# it directly preserves the checkpoint layout and also works at the
|
| 63 |
+
# official 512x512 resolution (where the released literal reshape is
|
| 64 |
+
# dimensionally inconsistent).
|
| 65 |
+
if projected.shape[2] * 4 == height // 8 and projected.shape[3] * 4 == width // 8:
|
| 66 |
+
noise_feature = F.pixel_shuffle(projected, 4)
|
| 67 |
+
else:
|
| 68 |
+
target_hw = (max(1, height // 8), max(1, width // 8))
|
| 69 |
+
noise_feature = F.interpolate(projected[:, : 2 * self.configs.ngf], size=target_hw, mode="nearest")
|
| 70 |
+
|
| 71 |
+
feature = torch.cat([evo_feature, noise_feature], dim=1)
|
| 72 |
+
gen_result = self.gen_dec(feature, evo_result)
|
| 73 |
+
|
| 74 |
+
return gen_result.unsqueeze(-1)
|
scripts/data_loader.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MRMS data loading utilities matching the official NowcastNet contract."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import struct
|
| 6 |
+
import zlib
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from torch.utils.data import Dataset
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _read_png_gray16(path: Path) -> np.ndarray:
|
| 15 |
+
"""Read the 16-bit grayscale PNG subset used by MRMS."""
|
| 16 |
+
raw = path.read_bytes()
|
| 17 |
+
if raw[:8] != b"\x89PNG\r\n\x1a\n":
|
| 18 |
+
raise ValueError(f"Not a PNG file: {path}")
|
| 19 |
+
pos = 8
|
| 20 |
+
idat: list[bytes] = []
|
| 21 |
+
width = height = bit_depth = color_type = None
|
| 22 |
+
while pos < len(raw):
|
| 23 |
+
length = struct.unpack(">I", raw[pos:pos + 4])[0]
|
| 24 |
+
kind = raw[pos + 4:pos + 8]
|
| 25 |
+
payload = raw[pos + 8:pos + 8 + length]
|
| 26 |
+
pos += length + 12
|
| 27 |
+
if kind == b"IHDR":
|
| 28 |
+
width, height, bit_depth, color_type = struct.unpack(">IIBB", payload[:10])
|
| 29 |
+
elif kind == b"IDAT":
|
| 30 |
+
idat.append(payload)
|
| 31 |
+
elif kind == b"IEND":
|
| 32 |
+
break
|
| 33 |
+
if width is None or height is None or bit_depth != 16 or color_type != 0:
|
| 34 |
+
raise ValueError(f"Expected 16-bit grayscale PNG: {path}")
|
| 35 |
+
|
| 36 |
+
decoded = zlib.decompress(b"".join(idat))
|
| 37 |
+
row_bytes = width * 2
|
| 38 |
+
previous = np.zeros(row_bytes, dtype=np.uint8)
|
| 39 |
+
rows: list[np.ndarray] = []
|
| 40 |
+
cursor = 0
|
| 41 |
+
for _ in range(height):
|
| 42 |
+
filter_type = decoded[cursor]
|
| 43 |
+
row = np.frombuffer(decoded[cursor + 1:cursor + 1 + row_bytes], dtype=np.uint8).copy()
|
| 44 |
+
cursor += row_bytes + 1
|
| 45 |
+
for index in range(row_bytes):
|
| 46 |
+
left = row[index - 2] if index >= 2 else 0
|
| 47 |
+
up = previous[index]
|
| 48 |
+
upper_left = previous[index - 2] if index >= 2 else 0
|
| 49 |
+
if filter_type == 1:
|
| 50 |
+
value = left
|
| 51 |
+
elif filter_type == 2:
|
| 52 |
+
value = up
|
| 53 |
+
elif filter_type == 3:
|
| 54 |
+
value = (int(left) + int(up)) // 2
|
| 55 |
+
elif filter_type == 4:
|
| 56 |
+
predictor = int(left) + int(up) - int(upper_left)
|
| 57 |
+
distances = (abs(predictor - int(left)), abs(predictor - int(up)), abs(predictor - int(upper_left)))
|
| 58 |
+
value = (left, up, upper_left)[int(np.argmin(distances))]
|
| 59 |
+
elif filter_type == 0:
|
| 60 |
+
value = 0
|
| 61 |
+
else:
|
| 62 |
+
raise ValueError(f"Unsupported PNG filter {filter_type}")
|
| 63 |
+
row[index] = (int(row[index]) + int(value)) & 255
|
| 64 |
+
rows.append(row)
|
| 65 |
+
previous = row
|
| 66 |
+
return np.frombuffer(b"".join(row.tobytes() for row in rows), dtype=">u2").astype(np.uint16).reshape(height, width)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def decode_mrms_event(event_dir: str | Path, image_height: int = 512, image_width: int = 512, total_length: int = 29) -> np.ndarray:
|
| 70 |
+
event_dir = Path(event_dir)
|
| 71 |
+
event_name = event_dir.name
|
| 72 |
+
frames = [_read_png_gray16(event_dir / f"{event_name}-{index:02d}.png") for index in range(total_length)]
|
| 73 |
+
data = np.stack(frames).astype(np.float32) / 10.0 - 3.0
|
| 74 |
+
if data.shape[1:] != (image_height, image_width):
|
| 75 |
+
raise ValueError(f"MRMS frame shape {data.shape[1:]} != {(image_height, image_width)}")
|
| 76 |
+
mask = np.ones_like(data, dtype=np.float32)
|
| 77 |
+
mask[data < 0] = 0
|
| 78 |
+
data[data < 0] = 0
|
| 79 |
+
data = np.clip(data, 0, 128)
|
| 80 |
+
return np.stack([data, mask], axis=-1)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class MRMSDataset(Dataset):
|
| 84 |
+
def __init__(self, data_dir: str | Path, image_height: int = 512, image_width: int = 512, total_length: int = 29, split: str = "train"):
|
| 85 |
+
self.data_dir = Path(data_dir)
|
| 86 |
+
self.events = sorted(path for path in self.data_dir.iterdir() if path.is_dir())
|
| 87 |
+
if split == "train":
|
| 88 |
+
self.events = [path for path in self.events if len(list(path.glob("*.png"))) == total_length]
|
| 89 |
+
elif split != "test":
|
| 90 |
+
raise ValueError("split must be train or test")
|
| 91 |
+
self.image_height = image_height
|
| 92 |
+
self.image_width = image_width
|
| 93 |
+
self.total_length = total_length
|
| 94 |
+
|
| 95 |
+
def __len__(self) -> int:
|
| 96 |
+
return len(self.events)
|
| 97 |
+
|
| 98 |
+
def __getitem__(self, index: int) -> dict[str, torch.Tensor | str]:
|
| 99 |
+
event = self.events[index]
|
| 100 |
+
frames = decode_mrms_event(event, self.image_height, self.image_width, self.total_length)
|
| 101 |
+
return {"radar_frames": torch.from_numpy(frames), "event": event.name}
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate MRMS-shaped synthetic events for contract and smoke checks."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import struct
|
| 7 |
+
import zlib
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def write_png_gray16(path: Path, array: np.ndarray) -> None:
|
| 14 |
+
array = np.asarray(array, dtype=">u2")
|
| 15 |
+
raw = b"".join(b"\x00" + row.tobytes() for row in array)
|
| 16 |
+
def chunk(kind: bytes, payload: bytes) -> bytes:
|
| 17 |
+
return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload) & 0xffffffff)
|
| 18 |
+
header = struct.pack(">IIBBBBB", array.shape[1], array.shape[0], 16, 0, 0, 0, 0)
|
| 19 |
+
path.write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", header) + chunk(b"IDAT", zlib.compress(raw, 1)) + chunk(b"IEND", b""))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def generate(output_dir: str | Path, events: int = 2, height: int = 512, width: int = 512, total_length: int = 29, seed: int = 42) -> None:
|
| 23 |
+
root = Path(output_dir)
|
| 24 |
+
rng = np.random.default_rng(seed)
|
| 25 |
+
yy, xx = np.mgrid[:height, :width]
|
| 26 |
+
for event_index in range(events):
|
| 27 |
+
event = root / f"synthetic_{event_index:04d}"
|
| 28 |
+
event.mkdir(parents=True, exist_ok=True)
|
| 29 |
+
cx, cy = width * (0.25 + 0.2 * event_index), height * 0.45
|
| 30 |
+
for frame in range(total_length):
|
| 31 |
+
center_x = cx + frame * 1.5
|
| 32 |
+
rain = 12.0 * np.exp(-((xx - center_x) ** 2 + (yy - cy) ** 2) / (2 * (max(height, width) * 0.12) ** 2))
|
| 33 |
+
rain += rng.normal(0, 0.08, size=(height, width))
|
| 34 |
+
# Inverse official encoding: decoded = uint16 / 10 - 3.
|
| 35 |
+
encoded = np.clip(np.rint((np.maximum(rain, 0) + 3.0) * 10), 0, 65535).astype(np.uint16)
|
| 36 |
+
write_png_gray16(event / f"{event.name}-{frame:02d}.png", encoded)
|
| 37 |
+
print(f"generated {events} MRMS events at {root} with shape ({total_length},{height},{width})")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
parser = argparse.ArgumentParser()
|
| 42 |
+
parser.add_argument("--output-dir", type=str, default="data/data")
|
| 43 |
+
parser.add_argument("--events", type=int, default=2)
|
| 44 |
+
parser.add_argument("--height", type=int, default=512)
|
| 45 |
+
parser.add_argument("--width", type=int, default=512)
|
| 46 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 47 |
+
args = parser.parse_args()
|
| 48 |
+
generate(args.output_dir, args.events, args.height, args.width, seed=args.seed)
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import sys
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import yaml
|
| 9 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 11 |
+
from scripts.data_loader import MRMSDataset
|
| 12 |
+
from model.model_factory import build_model, load_checkpoint
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
parser = argparse.ArgumentParser(description="NowcastNet inference")
|
| 16 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 17 |
+
parser.add_argument("--data-dir", help="MRMS event directory; defaults to data.data_dir in config.yaml")
|
| 18 |
+
parser.add_argument("--checkpoint")
|
| 19 |
+
parser.add_argument("--output-dir")
|
| 20 |
+
parser.add_argument("--device", default="auto")
|
| 21 |
+
parser.add_argument("--height", type=int)
|
| 22 |
+
parser.add_argument("--width", type=int)
|
| 23 |
+
parser.add_argument("--ngf", type=int)
|
| 24 |
+
args = parser.parse_args()
|
| 25 |
+
cfg = yaml.safe_load(Path(args.config).read_text())
|
| 26 |
+
mc, dc, ic = cfg["model"], cfg["data"], cfg["inference"]
|
| 27 |
+
height, width = args.height or dc["image_height"], args.width or dc["image_width"]
|
| 28 |
+
if args.ngf: mc["ngf"] = args.ngf
|
| 29 |
+
mc["img_height"], mc["img_width"] = height, width
|
| 30 |
+
device = torch.device("cuda" if args.device == "auto" and torch.cuda.is_available() else ("cpu" if args.device == "auto" else args.device))
|
| 31 |
+
checkpoint = Path(args.checkpoint) if args.checkpoint else PROJECT_ROOT / mc["checkpoint_dir"] / f"{mc.get('checkpoint_prefix', 'model_bak')}.pth"
|
| 32 |
+
if not checkpoint.is_file():
|
| 33 |
+
raise FileNotFoundError(f"Checkpoint not found: {checkpoint}. Run scripts/train.py first or pass --checkpoint.")
|
| 34 |
+
model = build_model(mc, device); load_checkpoint(model, checkpoint, device); model.eval()
|
| 35 |
+
data_dir = Path(args.data_dir) if args.data_dir else PROJECT_ROOT / dc["data_dir"]
|
| 36 |
+
ds = MRMSDataset(data_dir, height, width, dc["total_length"], "test")
|
| 37 |
+
out = Path(args.output_dir) if args.output_dir else PROJECT_ROOT / ic["output_dir"]
|
| 38 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
with torch.no_grad():
|
| 40 |
+
for item in ds:
|
| 41 |
+
frames = item["radar_frames"].unsqueeze(0).to(device=device, dtype=torch.float32)
|
| 42 |
+
pred = model(frames).squeeze(0).squeeze(-1).cpu().numpy()
|
| 43 |
+
target = frames[0, mc["input_length"]:, :, :, 0].cpu().numpy()
|
| 44 |
+
np.save(out / f"{item['event']}_pred.npy", pred)
|
| 45 |
+
np.save(out / f"{item['event']}_input.npy", frames[0, :mc["input_length"], :, :, 0].cpu().numpy())
|
| 46 |
+
np.save(out / f"{item['event']}_target.npy", target)
|
| 47 |
+
print(item["event"], pred.shape, float(pred.min()), float(pred.max()))
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__": main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render NowcastNet predictions and truth comparisons as RGB PNG files."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import struct
|
| 10 |
+
import zlib
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import yaml
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
|
| 18 |
+
RAIN_THRESHOLDS = np.asarray([0.1, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0], dtype=np.float32)
|
| 19 |
+
RAIN_COLORS = np.asarray(
|
| 20 |
+
[
|
| 21 |
+
[0, 0, 0],
|
| 22 |
+
[70, 70, 70],
|
| 23 |
+
[0, 110, 255],
|
| 24 |
+
[0, 205, 255],
|
| 25 |
+
[0, 190, 80],
|
| 26 |
+
[255, 230, 0],
|
| 27 |
+
[255, 145, 0],
|
| 28 |
+
[235, 35, 30],
|
| 29 |
+
[205, 0, 180],
|
| 30 |
+
],
|
| 31 |
+
dtype=np.uint8,
|
| 32 |
+
)
|
| 33 |
+
ERROR_THRESHOLDS = np.asarray([0.1, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0], dtype=np.float32)
|
| 34 |
+
ERROR_COLORS = np.asarray(
|
| 35 |
+
[
|
| 36 |
+
[0, 0, 0],
|
| 37 |
+
[40, 40, 40],
|
| 38 |
+
[35, 80, 170],
|
| 39 |
+
[30, 165, 215],
|
| 40 |
+
[80, 200, 120],
|
| 41 |
+
[245, 225, 65],
|
| 42 |
+
[245, 145, 45],
|
| 43 |
+
[220, 55, 40],
|
| 44 |
+
[245, 245, 245],
|
| 45 |
+
],
|
| 46 |
+
dtype=np.uint8,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _png_chunk(kind: bytes, payload: bytes) -> bytes:
|
| 51 |
+
checksum = zlib.crc32(kind + payload) & 0xFFFFFFFF
|
| 52 |
+
return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", checksum)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def write_png(path: Path, image: np.ndarray) -> None:
|
| 56 |
+
"""Write an H x W x 3 uint8 array as a standards-compliant RGB PNG."""
|
| 57 |
+
image = np.asarray(image, dtype=np.uint8)
|
| 58 |
+
if image.ndim != 3 or image.shape[2] != 3:
|
| 59 |
+
raise ValueError(f"Expected RGB image [H,W,3], got {image.shape}")
|
| 60 |
+
raw = b"".join(b"\x00" + row.tobytes() for row in image)
|
| 61 |
+
header = struct.pack(">IIBBBBB", image.shape[1], image.shape[0], 8, 2, 0, 0, 0)
|
| 62 |
+
path.write_bytes(
|
| 63 |
+
b"\x89PNG\r\n\x1a\n"
|
| 64 |
+
+ _png_chunk(b"IHDR", header)
|
| 65 |
+
+ _png_chunk(b"IDAT", zlib.compress(raw, 1))
|
| 66 |
+
+ _png_chunk(b"IEND", b"")
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def colorize(image: np.ndarray, thresholds: np.ndarray, colors: np.ndarray) -> np.ndarray:
|
| 71 |
+
values = np.nan_to_num(np.asarray(image, dtype=np.float32), nan=0.0, posinf=128.0, neginf=0.0)
|
| 72 |
+
return colors[np.searchsorted(thresholds, np.maximum(values, 0.0), side="right")]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def comparison_image(truth: np.ndarray, prediction: np.ndarray) -> np.ndarray:
|
| 76 |
+
truth_rgb = colorize(truth, RAIN_THRESHOLDS, RAIN_COLORS)
|
| 77 |
+
prediction_rgb = colorize(prediction, RAIN_THRESHOLDS, RAIN_COLORS)
|
| 78 |
+
error_rgb = colorize(np.abs(prediction - truth), ERROR_THRESHOLDS, ERROR_COLORS)
|
| 79 |
+
separator = np.full((truth.shape[0], 4, 3), 255, dtype=np.uint8)
|
| 80 |
+
return np.concatenate([truth_rgb, separator, prediction_rgb, separator, error_rgb], axis=1)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def main() -> None:
|
| 84 |
+
parser = argparse.ArgumentParser(description="Render NowcastNet inference results as PNG images")
|
| 85 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 86 |
+
parser.add_argument("--input-dir", help="directory containing *_pred.npy and *_target.npy")
|
| 87 |
+
parser.add_argument("--output-dir")
|
| 88 |
+
parser.add_argument("--threshold", type=float)
|
| 89 |
+
args = parser.parse_args()
|
| 90 |
+
|
| 91 |
+
cfg = yaml.safe_load(Path(args.config).read_text())
|
| 92 |
+
src = Path(args.input_dir) if args.input_dir else PROJECT_ROOT / cfg["inference"]["output_dir"]
|
| 93 |
+
out = Path(args.output_dir) if args.output_dir else PROJECT_ROOT / cfg["visualization"]["output_dir"]
|
| 94 |
+
threshold = args.threshold if args.threshold is not None else float(cfg["inference"]["threshold"])
|
| 95 |
+
expected_frames = int(cfg["model"]["total_length"]) - int(cfg["model"]["input_length"])
|
| 96 |
+
prediction_dir = out / "predictions"
|
| 97 |
+
comparison_dir = out / "comparison"
|
| 98 |
+
prediction_dir.mkdir(parents=True, exist_ok=True)
|
| 99 |
+
comparison_dir.mkdir(parents=True, exist_ok=True)
|
| 100 |
+
|
| 101 |
+
summary: dict[str, dict[str, object]] = {}
|
| 102 |
+
pred_paths = sorted(src.glob("*_pred.npy"))
|
| 103 |
+
if not pred_paths:
|
| 104 |
+
raise FileNotFoundError(f"No *_pred.npy inference results found under {src}")
|
| 105 |
+
|
| 106 |
+
for pred_path in pred_paths:
|
| 107 |
+
event = pred_path.name.removesuffix("_pred.npy")
|
| 108 |
+
target_path = src / f"{event}_target.npy"
|
| 109 |
+
if not target_path.is_file():
|
| 110 |
+
raise FileNotFoundError(
|
| 111 |
+
f"Truth file not found: {target_path}. Rerun scripts/inference.py to export targets."
|
| 112 |
+
)
|
| 113 |
+
prediction = np.load(pred_path)
|
| 114 |
+
truth = np.load(target_path)
|
| 115 |
+
if prediction.shape != truth.shape:
|
| 116 |
+
raise ValueError(f"Prediction shape {prediction.shape} != truth shape {truth.shape} for {event}")
|
| 117 |
+
if prediction.ndim != 3 or prediction.shape[0] != expected_frames:
|
| 118 |
+
raise ValueError(
|
| 119 |
+
f"Expected {expected_frames} frames [T,H,W] for {event}, got {prediction.shape}"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
absolute_error = np.abs(prediction - truth)
|
| 123 |
+
mae_by_lead = absolute_error.mean(axis=(1, 2))
|
| 124 |
+
rmse_by_lead = np.sqrt(np.square(prediction - truth).mean(axis=(1, 2)))
|
| 125 |
+
for index in range(expected_frames):
|
| 126 |
+
filename = f"{event}_t{index + 1:02d}.png"
|
| 127 |
+
write_png(
|
| 128 |
+
prediction_dir / filename,
|
| 129 |
+
colorize(prediction[index], RAIN_THRESHOLDS, RAIN_COLORS),
|
| 130 |
+
)
|
| 131 |
+
write_png(
|
| 132 |
+
comparison_dir / filename,
|
| 133 |
+
comparison_image(truth[index], prediction[index]),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
summary[event] = {
|
| 137 |
+
"shape": list(prediction.shape),
|
| 138 |
+
"prediction_png_count": expected_frames,
|
| 139 |
+
"comparison_png_count": expected_frames,
|
| 140 |
+
"comparison_layout": ["truth", "prediction", "absolute_error"],
|
| 141 |
+
"prediction_min": float(prediction.min()),
|
| 142 |
+
"prediction_max": float(prediction.max()),
|
| 143 |
+
"prediction_mean": float(prediction.mean()),
|
| 144 |
+
"threshold": threshold,
|
| 145 |
+
"threshold_fraction": float((prediction >= threshold).mean()),
|
| 146 |
+
"mae": float(absolute_error.mean()),
|
| 147 |
+
"rmse": float(np.sqrt(np.square(prediction - truth).mean())),
|
| 148 |
+
"mae_by_lead": [float(value) for value in mae_by_lead],
|
| 149 |
+
"rmse_by_lead": [float(value) for value in rmse_by_lead],
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
summary_path = out / "summary.json"
|
| 153 |
+
summary_path.write_text(json.dumps(summary, indent=2) + "\n")
|
| 154 |
+
print(f"prediction_png_dir={prediction_dir}")
|
| 155 |
+
print(f"comparison_png_dir={comparison_dir}")
|
| 156 |
+
print(f"summary={summary_path}")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import random
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
import yaml
|
| 13 |
+
from torch.utils.data import DataLoader
|
| 14 |
+
from torch.utils.data.distributed import DistributedSampler
|
| 15 |
+
|
| 16 |
+
# ``model`` and ``scripts`` are namespace packages; no __init__.py is required.
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 19 |
+
from scripts.data_loader import MRMSDataset
|
| 20 |
+
from model.model_factory import build_model, load_checkpoint
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def clone_state_dict(model: torch.nn.Module) -> dict[str, torch.Tensor]:
|
| 24 |
+
"""Copy the current parameters to CPU so later epochs cannot mutate them."""
|
| 25 |
+
network = model.module if hasattr(model, "module") else model
|
| 26 |
+
return {name: tensor.detach().cpu().clone() for name, tensor in network.state_dict().items()}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main() -> None:
|
| 30 |
+
parser = argparse.ArgumentParser(description="NowcastNet single-card training")
|
| 31 |
+
parser.add_argument("--config", default=str(Path(__file__).parents[1] / "conf/config.yaml"))
|
| 32 |
+
parser.add_argument("--data-dir", help="MRMS event directory; defaults to data.data_dir in config.yaml")
|
| 33 |
+
parser.add_argument("--device", default="auto")
|
| 34 |
+
parser.add_argument("--epochs", type=int)
|
| 35 |
+
parser.add_argument("--checkpoint")
|
| 36 |
+
parser.add_argument("--output-dir")
|
| 37 |
+
parser.add_argument("--log-dir", help="training log directory; defaults to training.output_dir in config.yaml")
|
| 38 |
+
parser.add_argument("--checkpoint-prefix")
|
| 39 |
+
parser.add_argument("--height", type=int)
|
| 40 |
+
parser.add_argument("--width", type=int)
|
| 41 |
+
parser.add_argument("--ngf", type=int)
|
| 42 |
+
parser.add_argument("--distributed", action="store_true", help="use torchrun for single-node or multi-node DDP")
|
| 43 |
+
args = parser.parse_args()
|
| 44 |
+
cfg = yaml.safe_load(Path(args.config).read_text())
|
| 45 |
+
mc, dc, tc = cfg["model"], cfg["data"], cfg["training"]
|
| 46 |
+
if args.height: mc["img_height"] = dc["image_height"] = args.height
|
| 47 |
+
if args.width: mc["img_width"] = dc["image_width"] = args.width
|
| 48 |
+
if args.ngf: mc["ngf"] = args.ngf
|
| 49 |
+
data_dir = Path(args.data_dir) if args.data_dir else PROJECT_ROOT / dc["data_dir"]
|
| 50 |
+
distributed = args.distributed or int(__import__("os").environ.get("WORLD_SIZE", "1")) > 1
|
| 51 |
+
rank = 0
|
| 52 |
+
if distributed:
|
| 53 |
+
import os, torch.distributed as dist
|
| 54 |
+
dist.init_process_group(backend="nccl" if torch.cuda.is_available() else "gloo", init_method="env://")
|
| 55 |
+
rank = dist.get_rank()
|
| 56 |
+
if args.device == "auto" and torch.cuda.is_available():
|
| 57 |
+
torch.cuda.set_device(int(os.environ.get("LOCAL_RANK", 0)))
|
| 58 |
+
if distributed and torch.cuda.is_available():
|
| 59 |
+
device = torch.device("cuda", int(__import__("os").environ.get("LOCAL_RANK", 0)))
|
| 60 |
+
else:
|
| 61 |
+
device = torch.device("cuda" if args.device == "auto" and torch.cuda.is_available() else ("cpu" if args.device == "auto" else args.device))
|
| 62 |
+
seed = int(tc["seed"]); random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
|
| 63 |
+
model = build_model(mc, device)
|
| 64 |
+
best_loss = float("inf")
|
| 65 |
+
best_epoch = 0
|
| 66 |
+
best_state = None
|
| 67 |
+
if args.checkpoint:
|
| 68 |
+
checkpoint = load_checkpoint(model, args.checkpoint, device)
|
| 69 |
+
if "best_loss" in checkpoint:
|
| 70 |
+
best_loss = float(checkpoint["best_loss"])
|
| 71 |
+
best_epoch = int(checkpoint.get("epoch", 0))
|
| 72 |
+
best_state = clone_state_dict(model)
|
| 73 |
+
dataset = MRMSDataset(data_dir, dc["image_height"], dc["image_width"], dc["total_length"], "train")
|
| 74 |
+
if not dataset:
|
| 75 |
+
raise RuntimeError(f"No complete MRMS events found under {data_dir}")
|
| 76 |
+
sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
|
| 77 |
+
loader = DataLoader(dataset, batch_size=int(dc["batch_size"]), shuffle=sampler is None, sampler=sampler, num_workers=int(dc["num_workers"]), drop_last=False)
|
| 78 |
+
if distributed:
|
| 79 |
+
from torch.nn.parallel import DistributedDataParallel as DDP
|
| 80 |
+
# The official generator keeps architectural branches whose
|
| 81 |
+
# parameters are not touched by the L1 smoke-training path.
|
| 82 |
+
model = DDP(
|
| 83 |
+
model,
|
| 84 |
+
device_ids=[device.index] if device.type == "cuda" else None,
|
| 85 |
+
find_unused_parameters=True,
|
| 86 |
+
)
|
| 87 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=float(tc["lr"]), weight_decay=float(tc["weight_decay"]))
|
| 88 |
+
epochs = args.epochs or int(tc["epochs"])
|
| 89 |
+
checkpoint_dir = Path(args.output_dir) if args.output_dir else PROJECT_ROOT / mc["checkpoint_dir"]
|
| 90 |
+
checkpoint_prefix = args.checkpoint_prefix or mc.get("checkpoint_prefix", "model_bak")
|
| 91 |
+
log_dir = Path(args.log_dir) if args.log_dir else PROJECT_ROOT / tc["output_dir"]
|
| 92 |
+
loss_history_path = log_dir / tc.get("loss_history_file", "loss_history.json")
|
| 93 |
+
loss_history: list[dict[str, float | int]] = []
|
| 94 |
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
| 95 |
+
if rank == 0:
|
| 96 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
for epoch in range(epochs):
|
| 98 |
+
if sampler is not None: sampler.set_epoch(epoch)
|
| 99 |
+
model.train()
|
| 100 |
+
loss_sum = torch.zeros(1, device=device)
|
| 101 |
+
sample_count = torch.zeros(1, device=device)
|
| 102 |
+
for batch in loader:
|
| 103 |
+
frames = batch["radar_frames"].to(device=device, dtype=torch.float32)
|
| 104 |
+
pred = model(frames)
|
| 105 |
+
target = frames[:, int(mc["input_length"]):, :, :, :1].squeeze(-1)
|
| 106 |
+
loss = torch.mean(torch.abs(pred.squeeze(-1) - target))
|
| 107 |
+
optimizer.zero_grad(set_to_none=True)
|
| 108 |
+
loss.backward()
|
| 109 |
+
optimizer.step()
|
| 110 |
+
loss_sum += loss.detach() * frames.shape[0]
|
| 111 |
+
sample_count += frames.shape[0]
|
| 112 |
+
if distributed:
|
| 113 |
+
torch.distributed.all_reduce(loss_sum)
|
| 114 |
+
torch.distributed.all_reduce(sample_count)
|
| 115 |
+
epoch_loss = (loss_sum / sample_count.clamp_min(1)).item()
|
| 116 |
+
if rank == 0:
|
| 117 |
+
print(f"epoch={epoch + 1}/{epochs} loss={epoch_loss:.6f}")
|
| 118 |
+
if epoch_loss < best_loss:
|
| 119 |
+
best_loss = epoch_loss
|
| 120 |
+
best_epoch = epoch + 1
|
| 121 |
+
best_state = clone_state_dict(model)
|
| 122 |
+
loss_history.append({"epoch": epoch + 1, "loss": epoch_loss})
|
| 123 |
+
loss_history_path.write_text(
|
| 124 |
+
json.dumps(
|
| 125 |
+
{
|
| 126 |
+
"epochs": loss_history,
|
| 127 |
+
"best_epoch": best_epoch,
|
| 128 |
+
"best_loss": best_loss,
|
| 129 |
+
},
|
| 130 |
+
indent=2,
|
| 131 |
+
)
|
| 132 |
+
+ "\n"
|
| 133 |
+
)
|
| 134 |
+
if rank == 0:
|
| 135 |
+
if best_state is None:
|
| 136 |
+
raise RuntimeError("Training finished without producing a model state")
|
| 137 |
+
checkpoint_path = checkpoint_dir / f"{checkpoint_prefix}.pth"
|
| 138 |
+
torch.save(
|
| 139 |
+
{"state_dict": best_state, "epoch": best_epoch, "best_loss": best_loss},
|
| 140 |
+
checkpoint_path,
|
| 141 |
+
)
|
| 142 |
+
print(f"checkpoint={checkpoint_path}")
|
| 143 |
+
print(f"loss_history={loss_history_path}")
|
| 144 |
+
if distributed:
|
| 145 |
+
torch.distributed.barrier()
|
| 146 |
+
torch.distributed.destroy_process_group()
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
if __name__ == "__main__":
|
| 150 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|