zhangrenchao commited on
Commit
b20ca9c
·
verified ·
1 Parent(s): 36e4eb5

Publish CRAI-ClimateExtremes reproduction

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
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
  *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.npz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Climate Extremes
9
+ - Spatial Reconstruction
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong><span style="font-size: 30px;">CRAI-ClimateExtremes</span></strong>
15
+ </p>
16
+
17
+ # Model Introduction
18
+
19
+ CRAI-ClimateExtremes reconstructs missing regions from incomplete climate-extreme index fields and validity masks for historical-extreme analysis and sparse-observation reconstruction.
20
+
21
+ Paper: Artificial intelligence reveals past climate extremes by reconstructing historical records
22
+ https://doi.org/10.1038/s41467-024-53464-2
23
+
24
+ # Model Description
25
+
26
+ The method was proposed by research teams from the German Climate Computing Center, the Met Office, the University of Hamburg, and collaborating institutions. The paper uses 45 historical simulations from eight CMIP6 models, HadEX-CAM observations, and reanalysis products including ERA5. The model reconstructs monthly TX90p, TN90p, TX10p, and TN10p temperature-extreme indices.
27
+
28
+ # Use Cases
29
+
30
+ | Use Case | Description |
31
+ | :---: | :--- |
32
+ | Climate-extreme reconstruction | Reconstruct four monthly temperature-extreme indices from incomplete fields and validity masks. |
33
+ | Irregular-gap handling | Validate partial convolution, mask propagation, and missing-region loss. |
34
+ | Local engineering validation | Validate the end-to-end workflow with structured synthetic data on the complete `144×192` grid. |
35
+ | ModelScope/OneCode execution | Validate structured data, training, inference, climate-reconstruction metrics, and visualization in ModelScope or OneCode environments. |
36
+ | Multi-GPU training | Validate distributed training and the checkpoint workflow through `torchrun`. |
37
+
38
+ # Usage Instructions
39
+
40
+ ## 1.OneCode
41
+
42
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
43
+
44
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
45
+
46
+ ## 2. Download and Installation
47
+
48
+ ```bash
49
+ hf download OneScience-Group/CRAI-ClimateExtremes --local-dir ./CRAI-ClimateExtremes
50
+ cd CRAI-ClimateExtremes
51
+ ```
52
+
53
+ ### Environment Dependencies
54
+
55
+ **Hardware Requirements**
56
+
57
+ - A GPU or DCU is recommended.
58
+ - A CPU can be used for connectivity validation with the default small-sample configuration.
59
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
60
+
61
+ **DCU Environment**
62
+
63
+ ```bash
64
+ # Activate DTK and Conda first
65
+ conda create -n onescience311 python=3.11 -y
66
+ conda activate onescience311
67
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
68
+ ```
69
+
70
+ **GPU Environment**
71
+
72
+ ```bash
73
+ # Activate Conda first
74
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
75
+ conda activate onescience311
76
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
77
+ ```
78
+
79
+ ### Training Data
80
+
81
+ The paper uses monthly CMIP6 extreme indices from 1901-2014 and applies HadEX-CAM missing-data masks to construct training samples. Inputs contain an index field and validity mask `[B,2,144,192]`, while targets are complete `[B,1,144,192]` index fields. This repository uses a small structured synthetic dataset for engineering validation only and does not represent the real CMIP6 or HadEX-CAM distributions, training scale, or paper performance.
82
+
83
+ ```bash
84
+ python scripts/fake_data.py
85
+ ```
86
+
87
+ ### Training
88
+
89
+ For single-GPU training, use:
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 reduces samples, network width, epochs, and ensemble members without reducing the `144×192` grid or four-index protocol. Formal experiments require real CMIP6 and HadEX-CAM data and paper-scale training, with artifacts saved to:
102
+
103
+ ```text
104
+ result/checkpoints/crai_climateextremes.pt
105
+ result/training/metrics.json
106
+ ```
107
+
108
+ ### Trained Weights
109
+
110
+ The paper does not provide directly loadable official model weights, and this repository bundles no weights under `weight/`. The locally trained checkpoint is saved to `result/checkpoints/crai_climateextremes.pt` and must not be represented as an official pretrained weight.
111
+
112
+ ### Inference
113
+
114
+ ```bash
115
+ python scripts/inference.py
116
+ ```
117
+
118
+ Inference loads the unified ensemble checkpoint, reconstructs missing extreme indices, and saves the member mean and spread. Complete numerical outputs are saved to:
119
+
120
+ ```text
121
+ result/output/predictions.npz
122
+ result/output/metadata.json
123
+ ```
124
+
125
+ ### Evaluation and Visualization
126
+
127
+ ```bash
128
+ python scripts/result.py
129
+ ```
130
+
131
+ Evaluation computes missing-region RMSE, Spearman correlation, bias, and neighborhood spatial correlation and generates a target, input, and reconstruction comparison. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
132
+
133
+ ```text
134
+ result/evaluation/metrics.json
135
+ result/evaluation/comparison.png
136
+ ```
137
+
138
+ # Official OneScience Information
139
+
140
+ | Platform | OneScience Main Repository | Skills Repository |
141
+ | --- | --- | --- |
142
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
143
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
144
+
145
+ # Citation and License
146
+
147
+ This repository is an independent engineering reproduction of the public CRAI-ClimateExtremes specifications.
148
+
149
+ Use of this repository's code, official model weights, and data remains subject to the licenses and terms of their respective projects.
README_zh.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - zh
5
+ - en
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 气候极端
10
+ - 空间重建
11
+ frameworks: PyTorch
12
+ ---
13
+
14
+ <p align="center">
15
+ <strong><span style="font-size: 30px;">CRAI-ClimateExtremes</span></strong>
16
+ </p>
17
+
18
+ # 模型介绍
19
+
20
+ CRAI-ClimateExtremes 根据不完整的气候极端指数场及其有效掩码重建缺测区域,主要用于历史气候极端事件分析和稀疏观测资料重建。
21
+
22
+ 论文:Artificial intelligence reveals past climate extremes by reconstructing historical records
23
+ https://doi.org/10.1038/s41467-024-53464-2
24
+
25
+ # 模型描述
26
+
27
+ 该方法由德国气候计算中心、英国气象局和汉堡大学等机构的研究团队提出。论文使用 8 个 CMIP6 模式的 45 个历史模拟、HadEX-CAM 观测资料以及 ERA5 等再分析数据。模型适用于重建 TX90p、TN90p、TX10p 和 TN10p 月尺度温度极端指数。
28
+
29
+ # 适用场景
30
+
31
+ | 场景 | 说明 |
32
+ | :---: | :--- |
33
+ | 气候极端指数重建 | 从不完整指数场和有效掩码重建四类月尺度温度极端指数。 |
34
+ | 不规则缺测处理 | 验证 partial convolution、掩码传播和缺失区域损失。 |
35
+ | 本地工程验证 | 使用完整 `144×192` 网格的结构化虚拟数据验证端到端流程。 |
36
+ | ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证结构化数据、训练、推理、气候重建指标和可视化流程。 |
37
+ | 多卡训练 | 通过 `torchrun` 验证分布式训练和 checkpoint 流程。 |
38
+
39
+ # 使用说明
40
+
41
+ ## 1.OneCode
42
+
43
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
44
+
45
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
46
+
47
+ ## 2.下载安装
48
+
49
+ ```bash
50
+ modelscope download --model OneScience/CRAI-ClimateExtremes --local_dir ./CRAI-ClimateExtremes
51
+ cd CRAI-ClimateExtremes
52
+ ```
53
+
54
+ ### 环境依赖
55
+
56
+ **硬件要求**
57
+
58
+ - 推荐使用 GPU 或 DCU 运行。
59
+ - CPU 可用于默认小样本配置的连通性验证。
60
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
61
+
62
+ **DCU环境**
63
+
64
+ ```bash
65
+ # 请首先激活 DTK 及 Conda
66
+ conda create -n onescience311 python=3.11 -y
67
+ conda activate onescience311
68
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
69
+ ```
70
+
71
+ **GPU环境**
72
+
73
+ ```bash
74
+ # 请首先激活 Conda
75
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
76
+ conda activate onescience311
77
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
78
+ ```
79
+
80
+ ### 训练数据介绍
81
+
82
+ 论文使用 1901-2014 年 CMIP6 月尺度极端指数,并应用 HadEX-CAM 缺测掩码构造训练样本。输入为指数场与有效掩码 `[B,2,144,192]`,目标为 `[B,1,144,192]` 的完整指数场。本仓库使用少量结构化虚拟样本验证工程流程,不代表 CMIP6 或 HadEX-CAM 的真实数据分布、训练规模或论文性能。
83
+
84
+ ```bash
85
+ python scripts/fake_data.py
86
+ ```
87
+
88
+ ### 训练
89
+
90
+ 单卡训练可使用:
91
+
92
+ ```bash
93
+ python scripts/train.py
94
+ ```
95
+
96
+ 多卡训练可使用:
97
+
98
+ ```bash
99
+ 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
100
+ ```
101
+
102
+ 默认配置仅缩小样本数、网络宽度、训练轮数和 ensemble 成员数,不缩小 `144×192` 空间网格和四类指数协议。正式实验需要真实 CMIP6 与 HadEX-CAM 数据和论文规模训练配置,训练产物保存到:
103
+
104
+ ```text
105
+ result/checkpoints/crai_climateextremes.pt
106
+ result/training/metrics.json
107
+ ```
108
+
109
+ ### 训练权重
110
+
111
+ 论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/crai_climateextremes.pt`,不得描述为官方预训练权重。
112
+
113
+ ### 推理
114
+
115
+ ```bash
116
+ python scripts/inference.py
117
+ ```
118
+
119
+ 推理加载统一 ensemble checkpoint,重建缺测极端指数并保存成员均值和离散度。完整数值结果保存到:
120
+
121
+ ```text
122
+ result/output/predictions.npz
123
+ result/output/metadata.json
124
+ ```
125
+
126
+ ### 评估和可视化
127
+
128
+ ```bash
129
+ python scripts/result.py
130
+ ```
131
+
132
+ 评估计算缺失区域 RMSE、Spearman 相关系数、bias 和邻域空间相关,并生成目标、输入和重建结果对比图。虚拟数据结果仅用于工程验证,不代表论文正式性能;结果保存到:
133
+
134
+ ```text
135
+ result/evaluation/metrics.json
136
+ result/evaluation/comparison.png
137
+ ```
138
+
139
+ # OneScience官方信息
140
+
141
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
142
+ | --- | --- | --- |
143
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
144
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
145
+
146
+ # 引用与许可证
147
+
148
+ 本仓库为 CRAI-ClimateExtremes 公开规格的独立工程复现版本��
149
+
150
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ data_path: data/crai_fake.npz
3
+ height: 144
4
+ width: 192
5
+ base_channels: 8
6
+ batch_size: 2
7
+ epochs: 1
8
+ max_iterations: 8
9
+ learning_rate: 0.00005
10
+ ensemble_members: 2
11
+ paper_batch_size: 16
12
+ paper_iterations: 1000000
13
+ paper_ensemble_members: 20
14
+ paper_model: false
15
+ num_samples: 8
16
+ checkpoint_path: result/checkpoints/crai_climateextremes.pt
17
+ output_dir: result/output
18
+ evaluation_dir: result/evaluation
config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "CRAI-ClimateExtremes",
3
+ "model_type": "crai_climateextremes",
4
+ "architectures": ["CRAIClimateExtremes"],
5
+ "framework": "PyTorch",
6
+ "domain": "earth-science",
7
+ "task": "climate-extreme-index-reconstruction",
8
+ "implementation": {
9
+ "entry_point": "model/crai_climateextremes.py",
10
+ "scope": "core-method and full-grid reduced-sample engineering reproduction",
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
+ "input_shape": ["B", 2, 144, 192],
18
+ "output_shape": ["B", 1, 144, 192],
19
+ "core": "partial-convolution U-Net with mask propagation and skip connections",
20
+ "output_range_percent": [0, 100]
21
+ },
22
+ "data": {
23
+ "dataset": "HadEX-CAM and CMIP6-derived extreme indices",
24
+ "format_version": "1.0",
25
+ "resolution_degrees": [1.25, 1.875],
26
+ "indices": ["TX90p", "TN90p", "TX10p", "TN10p"],
27
+ "input_layout": "BCHW",
28
+ "synthetic": true
29
+ },
30
+ "configuration_sources": [
31
+ "conf/config.yaml",
32
+ "model/crai_climateextremes.py",
33
+ "scripts/fake_data.py",
34
+ "scripts/train.py",
35
+ "scripts/inference.py",
36
+ "scripts/result.py"
37
+ ]
38
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "climate extreme index reconstruction",
4
+ "model": "CRAI-ClimateExtremes",
5
+ "input_format": "BCHW: incomplete index field and validity mask [B,2,144,192]",
6
+ "protocol": "partial-convolution reconstruction of masked HadEX-resolution climate-extreme fields",
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/crai_climateextremes.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Independent PyTorch implementation of the CRAI reconstruction architecture."""
2
+
3
+ import torch
4
+ from torch import nn
5
+ import torch.nn.functional as F
6
+
7
+
8
+ class PartialConv2d(nn.Module):
9
+ """Convolution normalized by valid pixels, with the corresponding mask update."""
10
+
11
+ def __init__(self, in_channels, out_channels, kernel_size=3, stride=1):
12
+ super().__init__()
13
+ padding = kernel_size // 2
14
+ self.input_conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
15
+ self.kernel_area = float(kernel_size * kernel_size)
16
+ self.stride = stride
17
+ self.padding = padding
18
+ self.kernel_size = kernel_size
19
+ self.register_buffer("mask_kernel", torch.ones(1, 1, kernel_size, kernel_size))
20
+
21
+ def forward(self, values, mask):
22
+ if mask.shape[1] != 1:
23
+ mask = mask.amax(dim=1, keepdim=True)
24
+ with torch.no_grad():
25
+ valid_count = F.conv2d(
26
+ mask, self.mask_kernel, stride=self.stride, padding=self.padding
27
+ )
28
+ new_mask = (valid_count > 0).to(values.dtype)
29
+ scale = self.kernel_area / valid_count.clamp_min(1.0)
30
+ raw = self.input_conv(values * mask)
31
+ if self.input_conv.bias is not None:
32
+ bias = self.input_conv.bias.view(1, -1, 1, 1)
33
+ raw = (raw - bias) * scale + bias
34
+ else:
35
+ raw = raw * scale
36
+ return raw * new_mask, new_mask
37
+
38
+
39
+ class PConvBlock(nn.Module):
40
+ def __init__(self, in_channels, out_channels, stride=1):
41
+ super().__init__()
42
+ self.partial = PartialConv2d(in_channels, out_channels, 3, stride)
43
+ self.norm = nn.GroupNorm(1, out_channels)
44
+
45
+ def forward(self, values, mask):
46
+ values, mask = self.partial(values, mask)
47
+ return F.leaky_relu(self.norm(values), 0.2), mask
48
+
49
+
50
+ class CRAIClimateExtremes(nn.Module):
51
+ """Partial-convolution U-Net producing bounded extreme-index percentages."""
52
+
53
+ def __init__(self, base_channels=8):
54
+ super().__init__()
55
+ c = base_channels
56
+ self.enc1 = PConvBlock(1, c)
57
+ self.enc2 = PConvBlock(c, 2 * c, stride=2)
58
+ self.enc3 = PConvBlock(2 * c, 4 * c, stride=2)
59
+ self.bottleneck = PConvBlock(4 * c, 8 * c, stride=2)
60
+ self.dec3 = PConvBlock(8 * c + 4 * c, 4 * c)
61
+ self.dec2 = PConvBlock(4 * c + 2 * c, 2 * c)
62
+ self.dec1 = PConvBlock(2 * c + c, c)
63
+ self.output = nn.Conv2d(c, 1, 1)
64
+
65
+ @staticmethod
66
+ def _upsample(values, mask, size):
67
+ return (
68
+ F.interpolate(values, size=size, mode="bilinear", align_corners=False),
69
+ F.interpolate(mask, size=size, mode="nearest"),
70
+ )
71
+
72
+ def forward(self, inputs):
73
+ if inputs.ndim != 4 or inputs.shape[1] != 2:
74
+ raise ValueError("expected [B, 2, H, W] containing index and valid mask")
75
+ values = inputs[:, :1] / 100.0
76
+ mask = (inputs[:, 1:2] > 0.5).to(values.dtype)
77
+ e1, m1 = self.enc1(values, mask)
78
+ e2, m2 = self.enc2(e1, m1)
79
+ e3, m3 = self.enc3(e2, m2)
80
+ x, m = self.bottleneck(e3, m3)
81
+ x, m = self._upsample(x, m, e3.shape[-2:])
82
+ x, m = self.dec3(torch.cat((x, e3), 1), torch.maximum(m, m3))
83
+ x, m = self._upsample(x, m, e2.shape[-2:])
84
+ x, m = self.dec2(torch.cat((x, e2), 1), torch.maximum(m, m2))
85
+ x, m = self._upsample(x, m, e1.shape[-2:])
86
+ x, _ = self.dec1(torch.cat((x, e1), 1), torch.maximum(m, m1))
87
+ return 100.0 * torch.sigmoid(self.output(x))
88
+
89
+
90
+ if __name__ == "__main__":
91
+ model = CRAIClimateExtremes()
92
+ output = model(torch.rand(2, 2, 144, 192))
93
+ print(tuple(output.shape), float(output.min()), float(output.max()))
scripts/fake_data.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Create structurally realistic full-grid samples with irregular HadEX-style masks."""
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import numpy as np
6
+
7
+
8
+ INDICES = np.array(["TX90p", "TN90p", "TX10p", "TN10p"])
9
+ H, W = 144, 192
10
+
11
+
12
+ def europe_mask(lat, lon):
13
+ yy, xx = np.meshgrid(lat, lon, indexing="ij")
14
+ broad = (yy >= 30) & (yy <= 72) & (xx >= -25) & (xx <= 45)
15
+ # A coarse geographic silhouette keeps the scientific global-grid contract.
16
+ atlantic_cut = (xx < -10) & (yy < 44)
17
+ southeast_cut = (xx > 30) & (yy < 40)
18
+ north_cut = (yy > 68) & ((xx < 5) | (xx > 30))
19
+ return (broad & ~atlantic_cut & ~southeast_cut & ~north_cut).astype(np.float32)
20
+
21
+
22
+ def main():
23
+ rng = np.random.default_rng(42)
24
+ root = Path(__file__).resolve().parents[1]
25
+ output = root / "data"
26
+ output.mkdir(exist_ok=True)
27
+ lat = np.linspace(-89.375, 89.375, H, dtype=np.float32)
28
+ lon = np.linspace(-179.0625, 179.0625, W, dtype=np.float32)
29
+ land = europe_mask(lat, lon)
30
+ yy, xx = np.meshgrid(lat, lon, indexing="ij")
31
+ n = 8
32
+ target = np.zeros((n, 1, H, W), dtype=np.float32)
33
+ valid = np.zeros_like(target)
34
+ index_ids = np.arange(n, dtype=np.int64) % 4
35
+ for sample in range(n):
36
+ phase = 0.55 * sample
37
+ field = 50 + 21 * np.sin(np.deg2rad(2.3 * xx) + phase)
38
+ field += 16 * np.cos(np.deg2rad(3.2 * yy) - 0.4 * phase)
39
+ field += 5 * np.sin(np.deg2rad(xx + yy) * 4 + phase)
40
+ field += rng.normal(0, 1.2, (H, W))
41
+ if index_ids[sample] >= 2:
42
+ field = 100 - field
43
+ target[sample, 0] = np.clip(field, 0, 100) * land
44
+ observed = land.copy()
45
+ observed[rng.random((H, W)) < (0.35 + 0.04 * (sample % 3))] = 0
46
+ for _ in range(5):
47
+ cy, cx = rng.integers(45, 99), rng.integers(78, 121)
48
+ ry, rx = rng.integers(3, 11), rng.integers(4, 15)
49
+ hole = ((np.arange(H)[:, None] - cy) / ry) ** 2
50
+ hole = hole + ((np.arange(W)[None, :] - cx) / rx) ** 2
51
+ observed[hole < 1] = 0
52
+ valid[sample, 0] = observed
53
+ observed_values = target * valid
54
+ np.savez_compressed(
55
+ output / "crai_fake.npz", target=target, observed=observed_values,
56
+ valid_mask=valid, europe_mask=land, index_ids=index_ids,
57
+ index_names=INDICES, latitude=lat, longitude=lon,
58
+ )
59
+ metadata = {
60
+ "kind": "structured_synthetic",
61
+ "shape": [n, 1, H, W],
62
+ "grid_resolution": {"longitude_degrees": 1.875, "latitude_degrees": 1.25},
63
+ "indices": INDICES.tolist(),
64
+ "mask": "global grid with coarse Europe land support and irregular missing regions",
65
+ }
66
+ (output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
67
+ print(f"wrote {output / 'crai_fake.npz'} with shape {target.shape}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run bounded ensemble reconstruction from a unified checkpoint."""
2
+
3
+ from pathlib import Path
4
+ import argparse
5
+ import json
6
+ import sys
7
+ import numpy as np
8
+ import torch
9
+ import yaml
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ from model.crai_climateextremes import CRAIClimateExtremes
15
+
16
+
17
+ def main():
18
+ parser = argparse.ArgumentParser()
19
+ parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml")
20
+ args = parser.parse_args()
21
+ config_path = args.config if args.config.is_absolute() else ROOT / args.config
22
+ with open(config_path, encoding="utf-8") as handle:
23
+ cfg = yaml.safe_load(handle)
24
+ data = np.load(ROOT / cfg["data_path"])
25
+ inputs = torch.from_numpy(np.concatenate((data["observed"], data["valid_mask"]), axis=1))
26
+ checkpoint_path = ROOT / cfg["checkpoint_path"]
27
+ if not checkpoint_path.is_file():
28
+ raise FileNotFoundError(f"checkpoint not found: {checkpoint_path}; run scripts/train.py first")
29
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
+ checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=True)
31
+ if checkpoint.get("format_version") != "1.0" or not isinstance(checkpoint.get("model"), list):
32
+ raise ValueError(f"unsupported checkpoint format: {checkpoint_path}")
33
+ model_config = checkpoint.get("model_config", {})
34
+ predictions = []
35
+ for state in checkpoint["model"]:
36
+ model = CRAIClimateExtremes(**model_config).to(device)
37
+ model.load_state_dict(state); model.eval()
38
+ with torch.no_grad():
39
+ predictions.append(model(inputs.to(device)).cpu().numpy())
40
+ if not predictions:
41
+ raise ValueError(f"checkpoint contains no ensemble members: {checkpoint_path}")
42
+ members = np.stack(predictions)
43
+ output = ROOT / cfg["output_dir"]
44
+ output.mkdir(parents=True, exist_ok=True)
45
+ np.savez_compressed(
46
+ output / "predictions.npz", prediction=members.mean(0),
47
+ ensemble_std=members.std(0), target=data["target"],
48
+ observed=data["observed"], valid_mask=data["valid_mask"],
49
+ europe_mask=data["europe_mask"], index_ids=data["index_ids"],
50
+ index_names=data["index_names"],
51
+ )
52
+ metadata = {"ensemble_members": len(predictions), "checkpoint_semantics": "member state list in one checkpoint", "output_range": [0, 100]}
53
+ (output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n")
54
+ print(f"predicted {inputs.shape[0]} samples with {len(predictions)} members")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
scripts/result.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evaluate missing-region reconstruction and create a comparison figure."""
2
+
3
+ from pathlib import Path
4
+ import argparse
5
+ import json
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn.functional as F
9
+ import yaml
10
+
11
+
12
+ def correlation(a, b):
13
+ if a.size < 2 or np.std(a) == 0 or np.std(b) == 0:
14
+ return 0.0
15
+ return float(np.corrcoef(a, b)[0, 1])
16
+
17
+
18
+ def rankdata(values):
19
+ order = np.argsort(values, kind="mergesort")
20
+ ranks = np.empty(len(values), dtype=np.float64)
21
+ ranks[order] = np.arange(len(values), dtype=np.float64)
22
+ unique, inverse, counts = np.unique(values, return_inverse=True, return_counts=True)
23
+ del unique
24
+ for group, count in enumerate(counts):
25
+ if count > 1:
26
+ positions = np.flatnonzero(inverse == group)
27
+ ranks[positions] = ranks[positions].mean()
28
+ return ranks
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser()
33
+ root = Path(__file__).resolve().parents[1]
34
+ parser.add_argument("--config", type=Path, default=root / "conf/config.yaml")
35
+ args = parser.parse_args()
36
+ config_path = args.config if args.config.is_absolute() else root / args.config
37
+ with open(config_path, encoding="utf-8") as handle:
38
+ cfg = yaml.safe_load(handle)
39
+ archive = np.load(root / cfg["output_dir"] / "predictions.npz")
40
+ pred, target = archive["prediction"], archive["target"]
41
+ missing = archive["europe_mask"][None, None] * (1 - archive["valid_mask"])
42
+ selected = missing.astype(bool)
43
+ error = pred[selected] - target[selected]
44
+ sample_spearman = []
45
+ for i in range(len(pred)):
46
+ mask = selected[i, 0]
47
+ sample_spearman.append(correlation(rankdata(pred[i, 0][mask]), rankdata(target[i, 0][mask])))
48
+ kernel = torch.ones(1, 1, 3, 3) / 8
49
+ kernel[0, 0, 1, 1] = 0
50
+ pred_neighbor = F.conv2d(torch.from_numpy(pred), kernel, padding=1).numpy()
51
+ target_neighbor = F.conv2d(torch.from_numpy(target), kernel, padding=1).numpy()
52
+ metrics = {
53
+ "missing_rmse": float(np.sqrt(np.mean(error ** 2))),
54
+ "sample_spearman_mean": float(np.mean(sample_spearman)),
55
+ "sample_spearman": [float(x) for x in sample_spearman],
56
+ "missing_bias": float(np.mean(error)),
57
+ "neighborhood_spatial_correlation_prediction": correlation(pred[selected], pred_neighbor[selected]),
58
+ "neighborhood_spatial_correlation_target": correlation(target[selected], target_neighbor[selected]),
59
+ "missing_points": int(selected.sum()),
60
+ }
61
+ output = root / cfg["evaluation_dir"]
62
+ output.mkdir(parents=True, exist_ok=True)
63
+ (output / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n")
64
+ import matplotlib
65
+ matplotlib.use("Agg")
66
+ import matplotlib.pyplot as plt
67
+ fig, axes = plt.subplots(1, 3, figsize=(13, 4), constrained_layout=True)
68
+ sample = 0
69
+ fields = [archive["observed"][sample, 0], target[sample, 0], pred[sample, 0]]
70
+ titles = ["Irregular observations", "Synthetic truth", "CRAI reconstruction"]
71
+ for axis, field, title in zip(axes, fields, titles):
72
+ image = axis.imshow(np.where(archive["europe_mask"] > 0, field, np.nan), origin="lower", vmin=0, vmax=100, cmap="RdYlBu_r")
73
+ axis.set_title(title); axis.set_axis_off()
74
+ fig.colorbar(image, ax=axes, label="Extreme index (%)", shrink=0.8)
75
+ fig.suptitle(f"Missing RMSE={metrics['missing_rmse']:.3f} | Spearman={metrics['sample_spearman_mean']:.3f} | Bias={metrics['missing_bias']:.3f}")
76
+ fig.savefig(output / "comparison.png", dpi=160); plt.close(fig)
77
+ print(json.dumps(metrics))
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
scripts/train.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train independent CRAI ensemble members, optionally under torchrun DDP."""
2
+
3
+ from pathlib import Path
4
+ import argparse
5
+ import json
6
+ import os
7
+ import random
8
+ import sys
9
+ import numpy as np
10
+ import torch
11
+ from torch import distributed as dist
12
+ from torch.nn.parallel import DistributedDataParallel
13
+ from torch.utils.data import DataLoader, Dataset, DistributedSampler
14
+ import yaml
15
+
16
+ ROOT = Path(__file__).resolve().parents[1]
17
+ sys.path.insert(0, str(ROOT))
18
+
19
+ from model.crai_climateextremes import CRAIClimateExtremes
20
+
21
+
22
+ class ClimateDataset(Dataset):
23
+ def __init__(self, archive, limit):
24
+ self.observed = torch.from_numpy(archive["observed"][:limit])
25
+ self.valid = torch.from_numpy(archive["valid_mask"][:limit])
26
+ self.target = torch.from_numpy(archive["target"][:limit])
27
+ land = torch.from_numpy(archive["europe_mask"])[None, None]
28
+ self.missing = land * (1.0 - self.valid)
29
+
30
+ def __len__(self):
31
+ return len(self.target)
32
+
33
+ def __getitem__(self, index):
34
+ return torch.cat((self.observed[index], self.valid[index]), 0), self.target[index], self.missing[index]
35
+
36
+
37
+ def load_config(path):
38
+ with open(path, encoding="utf-8") as handle:
39
+ return yaml.safe_load(handle)
40
+
41
+
42
+ def main():
43
+ parser = argparse.ArgumentParser()
44
+ parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml")
45
+ parser.add_argument("--paper-model", action="store_true")
46
+ args = parser.parse_args()
47
+ config_path = args.config if args.config.is_absolute() else ROOT / args.config
48
+ cfg = load_config(config_path)
49
+ use_paper = args.paper_model or cfg["paper_model"]
50
+ batch_size = cfg["paper_batch_size"] if use_paper else cfg["batch_size"]
51
+ iterations = cfg["paper_iterations"] if use_paper else cfg["max_iterations"]
52
+ members = cfg["paper_ensemble_members"] if use_paper else cfg["ensemble_members"]
53
+ rank, world = int(os.getenv("RANK", 0)), int(os.getenv("WORLD_SIZE", 1))
54
+ if world > 1:
55
+ dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
56
+ device = torch.device(f"cuda:{int(os.getenv('LOCAL_RANK', 0))}" if torch.cuda.is_available() else "cpu")
57
+ if device.type == "cuda":
58
+ torch.cuda.set_device(device)
59
+ archive = np.load(ROOT / cfg["data_path"])
60
+ dataset = ClimateDataset(archive, cfg["num_samples"])
61
+ checkpoint_path = ROOT / cfg["checkpoint_path"]
62
+ if rank == 0:
63
+ checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
64
+ (ROOT / "result/training").mkdir(parents=True, exist_ok=True)
65
+ if world > 1:
66
+ dist.barrier()
67
+ records, member_states = [], []
68
+ for member in range(members):
69
+ seed = int(cfg["seed"]) + member
70
+ random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
71
+ sampler = DistributedSampler(dataset, shuffle=True, seed=seed) if world > 1 else None
72
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=sampler is None, sampler=sampler)
73
+ model = CRAIClimateExtremes(cfg["base_channels"]).to(device)
74
+ if world > 1:
75
+ model = DistributedDataParallel(model, device_ids=[device.index] if device.type == "cuda" else None)
76
+ optimizer = torch.optim.Adam(model.parameters(), lr=cfg["learning_rate"])
77
+ step, losses = 0, []
78
+ for epoch in range(cfg["epochs"] if not use_paper else 10**9):
79
+ if sampler is not None:
80
+ sampler.set_epoch(epoch)
81
+ for inputs, target, missing in loader:
82
+ inputs, target, missing = inputs.to(device), target.to(device), missing.to(device)
83
+ prediction = model(inputs)
84
+ loss = (torch.abs(prediction - target) * missing).sum() / missing.sum().clamp_min(1)
85
+ optimizer.zero_grad(); loss.backward(); optimizer.step()
86
+ losses.append(float(loss.detach()))
87
+ step += 1
88
+ if step >= iterations:
89
+ break
90
+ if step >= iterations:
91
+ break
92
+ raw_model = model.module if isinstance(model, DistributedDataParallel) else model
93
+ if rank == 0:
94
+ member_states.append({key: value.detach().cpu() for key, value in raw_model.state_dict().items()})
95
+ records.append({"member": member, "iterations": step, "final_missing_mae": losses[-1]})
96
+ if rank == 0:
97
+ torch.save(
98
+ {
99
+ "format_version": "1.0",
100
+ "model_config": {"base_channels": cfg["base_channels"]},
101
+ "model": member_states,
102
+ },
103
+ checkpoint_path,
104
+ )
105
+ payload = {"paper_model": bool(use_paper), "world_size": world, "members": records}
106
+ (ROOT / "result/training/metrics.json").write_text(json.dumps(payload, indent=2) + "\n")
107
+ print(json.dumps(payload))
108
+ if world > 1:
109
+ dist.destroy_process_group()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
weight/.gitkeep ADDED
File without changes