zhangrenchao commited on
Commit
2e913c2
·
verified ·
1 Parent(s): c067b56

Publish ClimateBench engineering reproduction

Browse files
.gitattributes CHANGED
@@ -1,35 +1,12 @@
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
  *.bin filter=lfs diff=lfs merge=lfs -text
 
2
  *.ckpt filter=lfs diff=lfs merge=lfs -text
 
 
 
3
  *.joblib filter=lfs diff=lfs merge=lfs -text
 
 
4
  *.model filter=lfs diff=lfs merge=lfs -text
 
5
  *.npy filter=lfs diff=lfs merge=lfs -text
6
  *.npz filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
7
  *.pkl filter=lfs diff=lfs merge=lfs -text
8
  *.pt filter=lfs diff=lfs merge=lfs -text
9
  *.pth filter=lfs diff=lfs merge=lfs -text
 
10
  *.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
11
  *.zip filter=lfs diff=lfs merge=lfs -text
12
+ weight/* filter=lfs diff=lfs merge=lfs -text
 
README.md ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Climate Emulation
9
+ - CNN-LSTM
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong><span style="font-size: 30px;">ClimateBench CNN-LSTM</span></strong>
15
+ </p>
16
+
17
+ # Model Introduction
18
+
19
+ The ClimateBench CNN-LSTM addresses the high computational cost of Earth system models and the difficulty of rapidly comparing many emission scenarios by estimating global temperature, diurnal-temperature-range, and precipitation responses from greenhouse-gas and aerosol emissions. It is primarily intended for data-driven climate projection, rapid emission-scenario assessment, and standardized comparison of climate-emulation methods, providing an efficient baseline for climate-response research.
20
+
21
+ Paper: ClimateBench v1.0: A Benchmark for Data-Driven Climate Projections
22
+ https://doi.org/10.1029/2021MS002954
23
+
24
+ # Model Description
25
+
26
+ ClimateBench was proposed by a University of Oxford-led research team with institutions including North Carolina State University, the Norwegian Meteorological Institute, the University of East Anglia, and Universitat de València. The paper trained and validated baseline models with anthropogenic forcing and climate-response data from NorESM2-LM experiments in CMIP6, ScenarioMIP, AerChemMIP, and DAMIP. The model supports globally resolved climate-response emulation, data-driven climate projection, and SSP scenario evaluation.
27
+
28
+ # Use Cases
29
+
30
+ | Use Case | Description |
31
+ | :---: | :--- |
32
+ | Climate response emulation | Predict four global response fields from a ten-year forcing sequence. |
33
+ | Architecture reproduction | Verify the exact CNN, pooling, ReLU-LSTM, and dense parameterization. |
34
+ | SSP245 evaluation | Evaluate ClimateBench NRMSE with 2080-2100 target semantics. |
35
+ | ModelScope/OneCode execution | Validate training, inference, evaluation, visualization, and checkpoint workflows in ModelScope or OneCode. |
36
+ | Multi-device training | Train all independent branches with `torchrun` and DDP. |
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/ClimateBench --local-dir ./ClimateBench
50
+ cd ClimateBench
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
+ This repository uses a small number of structured synthetic samples to validate the engineering workflow. Inputs contain annual sequences of cumulative CO2, CH4, SO2, and black-carbon emissions, while the targets are the corresponding global temperature, diurnal-temperature-range, and precipitation responses. The data preserves the paper's temporal, channel, and global-grid dimensions with plausible temporal and spatial climate structure; it does not represent the official data distribution or paper performance.
82
+
83
+ ```bash
84
+ python scripts/fake_data.py --force
85
+ ```
86
+
87
+ ### Training
88
+
89
+ For single-device 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
+ Training outputs are saved to:
102
+
103
+ ```text
104
+ result/checkpoints/climatebench.pt
105
+ result/training/metrics.json
106
+ ```
107
+
108
+ ### Trained Weights
109
+
110
+ No weights are bundled under `weight/`. The paper provides no confirmed official checkpoint, and the engineering checkpoint is not claimed to be compatible with external weights.
111
+
112
+ ### Inference
113
+
114
+ ```bash
115
+ python scripts/inference.py
116
+ ```
117
+
118
+ Inference writes `result/output/predictions.npz` with all four predictions, targets, coordinates, scenario, and 2080-2100 evaluation semantics.
119
+
120
+ ### Evaluation and Visualization
121
+
122
+ ```bash
123
+ python scripts/result.py
124
+ ```
125
+
126
+ Evaluation computes spatial error, global-mean error, and combined NRMSE for all four climate variables and saves them to `result/evaluation/metrics.json`. It also generates the target, prediction, and error comparison figure `result/evaluation/four_targets.png`. Synthetic-data results are only for engineering-workflow validation and do not represent paper performance on the real test set.
127
+
128
+ # Official OneScience Information
129
+
130
+ | Platform | OneScience Main Repository | Skills Repository |
131
+ | --- | --- | --- |
132
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
133
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
134
+
135
+ # Citation and License
136
+
137
+ This repository is an independent engineering reproduction of the public ClimateBench paper specifications.
138
+
139
+ 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,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: Apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 气候模拟
10
+ - CNN-LSTM
11
+ frameworks: PyTorch
12
+ datasets:
13
+ - ClimateBench
14
+ ---
15
+
16
+ <p align="center">
17
+ <strong><span style="font-size: 30px;">ClimateBench CNN-LSTM</span></strong>
18
+ </p>
19
+
20
+ # 模型介绍
21
+
22
+ ClimateBench CNN-LSTM 用于解决地球系统模式计算成本高、难以快速比较大量排放情景的问题,根据温室气体和气溶胶排放估计全球气温、昼夜温差和降水响应。模型主要用于数据驱动气候投影、不同排放情景的快速评估以及气候模拟方法的标准化比较,为研究气候变化响应提供高效基线。
23
+
24
+ 论文:ClimateBench v1.0: A Benchmark for Data-Driven Climate Projections
25
+ https://doi.org/10.1029/2021MS002954
26
+
27
+ # 模型描述
28
+
29
+ ClimateBench 由 University of Oxford 牵头,联合 North Carolina State University、Norwegian Meteorological Institute、University of East Anglia、Universitat de València 等机构的研究团队提出。论文使用 CMIP6、ScenarioMIP、AerChemMIP 和 DAMIP 中 NorESM2-LM 的人为强迫与气候响应数据训练和验证基线模型。模型适用于全球空间气候响应模拟、数据驱动气候投影和 SSP 情景评估。
30
+
31
+ # 适用场景
32
+
33
+ | 场景 | 说明 |
34
+ | :---: | :--- |
35
+ | 气候响应模拟 | 从 10 年人为强迫序列预测四类全球空间响应。 |
36
+ | 架构复现 | 验证论文 CNN、池化、ReLU LSTM 和稠密输出的精确参数化。 |
37
+ | SSP245 评估 | 按 2080-2100 目标语义计算 ClimateBench NRMSE。 |
38
+ | ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证训练、推理、评估、可视化和 checkpoint 流程。 |
39
+ | 多卡训练 | 使用 `torchrun` 和 DDP 训练四个独立分支。 |
40
+
41
+ # 使用说明
42
+
43
+ ## 1.OneCode
44
+
45
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
46
+
47
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
48
+
49
+ ## 2.下载安装
50
+
51
+ ```bash
52
+ modelscope download --model OneScience/ClimateBench --local_dir ./ClimateBench
53
+ cd ClimateBench
54
+ ```
55
+
56
+ ### 环境依赖
57
+
58
+ **硬件要求**
59
+
60
+ - 推荐使用 GPU 或 DCU 运行。
61
+ - CPU 可用于默认小样本配置的连通性验证。
62
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
63
+
64
+ **DCU环境**
65
+
66
+ ```bash
67
+ # 请首先激活DTK及CONDA
68
+ conda create -n onescience311 python=3.11 -y
69
+ conda activate onescience311
70
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
71
+ ```
72
+
73
+ **GPU环境**
74
+
75
+ ```bash
76
+ # 请首先激活CONDA
77
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
78
+ conda activate onescience311
79
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
80
+ ```
81
+
82
+ ### 训练数据介绍
83
+
84
+ 本仓库使用少量结构化虚拟样本验证工程流程,输入包含连续年度的累积 CO2、CH4、SO2 和黑碳排放,目标为相应的全球气温、昼夜温差及降水响应。虚拟数据保留论文的时间、通道和全球网格维度,并包含合理的时间变化与空间气候结构,仅用于验证模型训练、推理和评估流程,不代表官方数据分布与论文性能。
85
+
86
+ ```bash
87
+ python scripts/fake_data.py --force
88
+ ```
89
+
90
+ ### 训练
91
+
92
+ 单卡训练可使用:
93
+
94
+ ```bash
95
+ python scripts/train.py
96
+ ```
97
+
98
+ 多卡训练可使用:
99
+
100
+ ```bash
101
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
102
+ ```
103
+
104
+ 训练输出保存到:
105
+
106
+ ```text
107
+ result/checkpoints/climatebench.pt
108
+ result/training/metrics.json
109
+ ```
110
+
111
+ ### 训练权重
112
+
113
+ 本仓库不在 `weight/` 中内置权重。论文未提供可确认的官方 checkpoint,当前工程 checkpoint 不声明兼容外部权重。
114
+
115
+ ### 推理
116
+
117
+ ```bash
118
+ python scripts/inference.py
119
+ ```
120
+
121
+ 推理输出 `result/output/predictions.npz`,保留四目标预测、真实目标、坐标、场景和 2080-2100 评估语义。
122
+
123
+ ### 评估和可视化
124
+
125
+ ```bash
126
+ python scripts/result.py
127
+ ```
128
+
129
+ 评估计算四个气候变量的空间误差、全球平均误差和综合 NRMSE,并保存到 `result/evaluation/metrics.json`。脚本同时生成目标、预测和误差对比图 `result/evaluation/four_targets.png`。虚拟数据结果仅用于验证工程流程,不代表论文真实测试集性能。
130
+
131
+ # OneScience官方信息
132
+
133
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
134
+ | --- | --- | --- |
135
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
136
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
137
+
138
+ # 引用与许可证
139
+
140
+ 本仓库为 ClimateBench 论文公开规格的独立工程复现版本。
141
+
142
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中��许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ data:
3
+ root: data
4
+ format_version: climatebench_annual_npz_v1
5
+ train_samples: 2
6
+ test_samples: 1
7
+ time_steps: 10
8
+ channels: [co2_cumulative, ch4, so2, bc]
9
+ targets: [tas, dtr, pr, pr90]
10
+ height: 96
11
+ width: 144
12
+ storage_layout: NTCHW
13
+ paper_layout: NTHWC
14
+ model:
15
+ convolution_filters: 20
16
+ convolution_kernel: 3
17
+ pooling_kernel: 2
18
+ lstm_input: 20
19
+ lstm_hidden: 25
20
+ output_features: 13824
21
+ parameters_per_target: 364764
22
+ paper_training:
23
+ epochs: 30
24
+ batch_size: 16
25
+ optimizer: RMSprop
26
+ loss: mean_squared_error
27
+ train:
28
+ epochs: 1
29
+ batch_size: 1
30
+ learning_rate: 0.001
31
+ optimizer: RMSprop
32
+ loss: mean_squared_error
33
+ gradient_clip_norm: 10.0
34
+ num_workers: 0
35
+ runtime:
36
+ device: auto
37
+ paths:
38
+ checkpoint: result/checkpoints/climatebench.pt
39
+ training_metrics: result/training/metrics.json
40
+ inference: result/output/predictions.npz
41
+ evaluation_dir: result/evaluation
42
+ evaluation:
43
+ start_year: 2080
44
+ end_year: 2100
45
+ global_weight: 5.0
46
+ scenario: ssp245
config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "ClimateBench",
3
+ "model_type": "climatebench",
4
+ "architectures": ["ClimateBench", "ClimateBenchBranch"],
5
+ "framework": "PyTorch",
6
+ "domain": "climate-emulation",
7
+ "task": "annual-spatial-climate-response-emulation",
8
+ "license": "Apache-2.0",
9
+ "implementation": {
10
+ "entry_point": "model/climatebench.py",
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
+ "per_target": "TimeDistributed Conv2d(4,20,3,same)+ReLU+AvgPool2d(2)+global spatial average+ReLU-LSTM(20,25)+Dense(25,13824)",
18
+ "parameters_per_target": 364764,
19
+ "targets": ["tas", "dtr", "pr", "pr90"],
20
+ "independent_branches": 4
21
+ },
22
+ "data": {
23
+ "format": "NPZ",
24
+ "stored_input_layout": "NTCHW",
25
+ "paper_input_layout": "NTHWC",
26
+ "recommended_input_shape": ["B", 10, 4, 96, 144],
27
+ "output_shape": ["B", 4, 96, 144],
28
+ "channels": ["co2_cumulative", "ch4", "so2", "bc"],
29
+ "temporal_resolution": "annual",
30
+ "protocol": "climatebench_annual_npz_v1"
31
+ },
32
+ "configuration_sources": [
33
+ "conf/config.yaml",
34
+ "model/climatebench.py",
35
+ "scripts/fake_data.py",
36
+ "scripts/train.py",
37
+ "scripts/inference.py",
38
+ "scripts/result.py"
39
+ ]
40
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "annual-spatial-climate-response-emulation",
4
+ "model": "ClimateBench",
5
+ "input_format": "NTCHW",
6
+ "protocol": "four independent paper-faithful CNN-LSTM target emulators",
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/climatebench.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Paper-faithful PyTorch CNN-LSTM baseline for ClimateBench."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import nn
7
+ from torch.nn import functional as F
8
+
9
+
10
+ TARGETS = ("tas", "dtr", "pr", "pr90")
11
+
12
+
13
+ class ReLULSTM(nn.Module):
14
+ """Keras-compatible LSTM using sigmoid gates and ReLU activation."""
15
+
16
+ def __init__(self, input_size: int = 20, hidden_size: int = 25):
17
+ super().__init__()
18
+ self.input_size = int(input_size)
19
+ self.hidden_size = int(hidden_size)
20
+ self.kernel = nn.Parameter(torch.empty(input_size, 4 * hidden_size))
21
+ self.recurrent_kernel = nn.Parameter(torch.empty(hidden_size, 4 * hidden_size))
22
+ self.bias = nn.Parameter(torch.zeros(4 * hidden_size))
23
+ self.reset_parameters()
24
+
25
+ def reset_parameters(self) -> None:
26
+ nn.init.xavier_uniform_(self.kernel)
27
+ nn.init.orthogonal_(self.recurrent_kernel)
28
+ nn.init.zeros_(self.bias)
29
+ with torch.no_grad():
30
+ self.bias[self.hidden_size:2 * self.hidden_size].fill_(1.0)
31
+
32
+ def forward(self, sequence: torch.Tensor) -> torch.Tensor:
33
+ if sequence.ndim != 3 or sequence.shape[-1] != self.input_size:
34
+ raise ValueError(f"expected [B,T,{self.input_size}], got {tuple(sequence.shape)}")
35
+ batch = sequence.shape[0]
36
+ hidden = sequence.new_zeros(batch, self.hidden_size)
37
+ cell = sequence.new_zeros(batch, self.hidden_size)
38
+ for step in range(sequence.shape[1]):
39
+ gates = sequence[:, step] @ self.kernel + hidden @ self.recurrent_kernel + self.bias
40
+ input_gate, forget_gate, candidate, output_gate = gates.chunk(4, dim=-1)
41
+ input_gate = torch.sigmoid(input_gate)
42
+ forget_gate = torch.sigmoid(forget_gate)
43
+ candidate = F.relu(candidate)
44
+ output_gate = torch.sigmoid(output_gate)
45
+ cell = forget_gate * cell + input_gate * candidate
46
+ hidden = output_gate * F.relu(cell)
47
+ return hidden
48
+
49
+
50
+ class ClimateBenchBranch(nn.Module):
51
+ """One 364,764-parameter paper CNN-LSTM target emulator."""
52
+
53
+ PAPER_PARAMETER_COUNT = 364_764
54
+
55
+ def __init__(self, height: int = 96, width: int = 144):
56
+ super().__init__()
57
+ self.height = int(height)
58
+ self.width = int(width)
59
+ self.conv = nn.Conv2d(4, 20, kernel_size=3, padding="same")
60
+ self.pool = nn.AvgPool2d(kernel_size=2)
61
+ self.lstm = ReLULSTM(20, 25)
62
+ self.dense = nn.Linear(25, height * width)
63
+ count = sum(parameter.numel() for parameter in self.parameters())
64
+ if count != self.PAPER_PARAMETER_COUNT:
65
+ raise RuntimeError(f"paper branch must have 364764 parameters, got {count}")
66
+
67
+ def forward(self, inputs: torch.Tensor) -> torch.Tensor:
68
+ if inputs.ndim != 5 or inputs.shape[2:] != (4, self.height, self.width):
69
+ raise ValueError(f"expected [B,T,4,{self.height},{self.width}], got {tuple(inputs.shape)}")
70
+ batch, time = inputs.shape[:2]
71
+ features = F.relu(self.conv(inputs.reshape(batch * time, 4, self.height, self.width)))
72
+ features = self.pool(features).mean(dim=(-2, -1)).reshape(batch, time, 20)
73
+ return self.dense(self.lstm(features)).reshape(batch, 1, self.height, self.width)
74
+
75
+
76
+ class ClimateBench(nn.Module):
77
+ """Four independent paper branches ordered as tas, dtr, pr and pr90."""
78
+
79
+ def __init__(self, height: int = 96, width: int = 144):
80
+ super().__init__()
81
+ self.height = int(height)
82
+ self.width = int(width)
83
+ self.branches = nn.ModuleDict({name: ClimateBenchBranch(height, width) for name in TARGETS})
84
+
85
+ def forward(self, inputs: torch.Tensor) -> torch.Tensor:
86
+ return torch.cat([self.branches[name](inputs) for name in TARGETS], dim=1)
87
+
88
+ def parameter_counts(self) -> dict[str, int]:
89
+ return {name: sum(parameter.numel() for parameter in branch.parameters())
90
+ for name, branch in self.branches.items()}
scripts/fake_data.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate structured annual forcing windows and climate responses."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import yaml
8
+
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+
12
+
13
+ def make_split(path: Path, count: int, config: dict, seed: int, test: bool) -> None:
14
+ rng = np.random.default_rng(seed)
15
+ data = config["data"]
16
+ time, height, width = int(data["time_steps"]), int(data["height"]), int(data["width"])
17
+ lat = np.linspace(-90, 90, height, dtype=np.float32)
18
+ lon = np.linspace(0, 360, width, endpoint=False, dtype=np.float32)
19
+ lat2d, lon2d = np.meshgrid(lat, lon, indexing="ij")
20
+ windows, responses, years = [], [], []
21
+ for sample in range(count):
22
+ end_year = 2100 if test else 2060 + 10 * sample
23
+ annual_years = np.arange(end_year - time + 1, end_year + 1, dtype=np.int32)
24
+ progress = (annual_years - 1850).astype(np.float32)
25
+ co2 = 0.90 * progress + 0.0018 * progress ** 2
26
+ ch4 = 0.42 * progress + 12.0 * np.sin(progress / 38.0)
27
+ industrial = np.exp(-((lat2d - 35) / 22) ** 2) * (0.65 + 0.35 * np.cos(np.deg2rad(lon2d - 90)))
28
+ tropical = np.exp(-(lat2d / 20) ** 2) * (0.65 + 0.35 * np.sin(np.deg2rad(2 * lon2d)))
29
+ forcing = np.empty((time, 4, height, width), dtype=np.float32)
30
+ for index, year_progress in enumerate(progress):
31
+ forcing[index, 0] = co2[index]
32
+ forcing[index, 1] = ch4[index]
33
+ forcing[index, 2] = industrial * (32.0 * np.exp(-((year_progress - 125) / 75) ** 2))
34
+ forcing[index, 3] = tropical * (5.0 + 2.0 * np.sin(year_progress / 27.0))
35
+ forcing += rng.normal(0, 0.01, forcing.shape).astype(np.float32)
36
+ co2_level = forcing[-1, 0].mean() / 300.0
37
+ methane = forcing[-3:, 1].mean() / 120.0
38
+ aerosol = forcing[-3:, 2:].mean(axis=(0, 1)) / 25.0
39
+ arctic = 1.0 + 1.3 * (np.abs(lat2d) / 90.0) ** 2
40
+ land_pattern = np.cos(np.deg2rad(2 * lon2d)) * np.cos(np.deg2rad(lat2d))
41
+ itcz = np.sin(np.deg2rad(lon2d)) * np.exp(-(lat2d / 17) ** 2)
42
+ tas = 1.35 * co2_level * arctic + 0.22 * methane - 0.30 * aerosol + 0.05 * land_pattern
43
+ dtr = 0.10 * co2_level + 0.23 * aerosol * land_pattern - 0.04 * methane
44
+ pr = 0.16 * co2_level * np.cos(np.deg2rad(lat2d)) + 0.20 * itcz - 0.07 * aerosol
45
+ pr90 = 1.35 * pr + 0.08 * co2_level * np.exp(-(lat2d / 28) ** 2)
46
+ response = np.stack([tas, dtr, pr, pr90]).astype(np.float32)
47
+ response += rng.normal(0, 0.003, response.shape).astype(np.float32)
48
+ windows.append(forcing)
49
+ responses.append(response)
50
+ years.append(annual_years)
51
+ evaluation = config["evaluation"]
52
+ np.savez_compressed(
53
+ path, inputs=np.stack(windows).astype(np.float32), targets=np.stack(responses).astype(np.float32),
54
+ years=np.stack(years), latitude=lat, longitude=lon,
55
+ channel_names=np.asarray(data["channels"]), target_names=np.asarray(data["targets"]),
56
+ format_version=np.asarray(data["format_version"]), storage_layout=np.asarray("NTCHW"),
57
+ temporal_resolution=np.asarray("annual"), data_source=np.asarray("structured_synthetic"),
58
+ scenario=np.asarray(evaluation["scenario"] if test else "synthetic_training_scenarios"),
59
+ target_aggregation=np.asarray("2080-2100 climatological mean" if test else "annual response"),
60
+ evaluation_start_year=np.asarray(evaluation["start_year"], dtype=np.int32),
61
+ evaluation_end_year=np.asarray(evaluation["end_year"], dtype=np.int32),
62
+ )
63
+
64
+
65
+ def main() -> None:
66
+ parser = argparse.ArgumentParser()
67
+ parser.add_argument("--force", action="store_true")
68
+ args = parser.parse_args()
69
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
70
+ output = ROOT / config["data"]["root"]
71
+ output.mkdir(parents=True, exist_ok=True)
72
+ for offset, (name, count, test) in enumerate((("train.npz", config["data"]["train_samples"], False),
73
+ ("test.npz", config["data"]["test_samples"], True))):
74
+ path = output / name
75
+ if args.force or not path.exists():
76
+ make_split(path, int(count), config, int(config["seed"]) + offset, test)
77
+ print(f"generated={path.relative_to(ROOT)} samples={count} layout=NTCHW annual=true")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the four ClimateBench emulators on the held-out scenario."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+ import yaml
9
+ from torch.utils.data import DataLoader
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(ROOT))
14
+ from model.climatebench import ClimateBench
15
+ from train import ClimateDataset, device_from_config
16
+
17
+
18
+ def main() -> None:
19
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
20
+ device = device_from_config(config)
21
+ checkpoint = torch.load(ROOT / config["paths"]["checkpoint"], map_location=device, weights_only=False)
22
+ if checkpoint["format_version"] != config["data"]["format_version"]:
23
+ raise ValueError("checkpoint and data format versions differ")
24
+ model = ClimateBench(**checkpoint["model_config"]).to(device)
25
+ model.load_state_dict(checkpoint["model"])
26
+ model.eval()
27
+ dataset = ClimateDataset(ROOT / config["data"]["root"] / "test.npz", config)
28
+ loader = DataLoader(dataset, batch_size=1, shuffle=False)
29
+ predictions = []
30
+ with torch.no_grad():
31
+ for inputs, _ in loader:
32
+ predictions.append(model(inputs.to(device)).cpu().numpy())
33
+ prediction = np.concatenate(predictions).astype(np.float32)
34
+ if not np.isfinite(prediction).all():
35
+ raise FloatingPointError("inference produced NaN or Inf")
36
+ source = dataset.data
37
+ output = ROOT / config["paths"]["inference"]
38
+ output.parent.mkdir(parents=True, exist_ok=True)
39
+ np.savez_compressed(output, predictions=prediction, targets=source["targets"], years=source["years"],
40
+ latitude=source["latitude"], longitude=source["longitude"],
41
+ target_names=source["target_names"], scenario=source["scenario"],
42
+ target_aggregation=source["target_aggregation"], storage_layout=np.asarray("NCHW"),
43
+ format_version=source["format_version"],
44
+ evaluation_start_year=source["evaluation_start_year"],
45
+ evaluation_end_year=source["evaluation_end_year"])
46
+ print(f"predictions={output.relative_to(ROOT)} shape={prediction.shape} test_batches={len(predictions)}")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
scripts/result.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compute ClimateBench spatial/global NRMSE and plot all four targets."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import matplotlib
7
+ matplotlib.use("Agg")
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import yaml
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+
15
+
16
+ def area_mean(field: np.ndarray, latitude: np.ndarray) -> np.ndarray:
17
+ weights = np.cos(np.deg2rad(latitude)).clip(0)[:, None]
18
+ return np.sum(field * weights, axis=(-2, -1)) / (weights.sum() * field.shape[-1])
19
+
20
+
21
+ def climatebench_metrics(prediction: np.ndarray, target: np.ndarray, latitude: np.ndarray, alpha: float) -> dict:
22
+ normalization = float(area_mean(target, latitude).mean())
23
+ if abs(normalization) <= 1e-12:
24
+ raise ValueError("area-weighted target mean must be nonzero for NRMSE normalization")
25
+ temporal_rmse = np.sqrt(np.mean((prediction - target) ** 2, axis=0))
26
+ spatial = float(area_mean(temporal_rmse, latitude) / normalization)
27
+ predicted_global = area_mean(prediction, latitude)
28
+ target_global = area_mean(target, latitude)
29
+ global_nrmse = float(np.sqrt(np.mean((predicted_global - target_global) ** 2)) / normalization)
30
+ return {"spatial_nrmse": spatial, "global_nrmse": global_nrmse,
31
+ "total_nrmse": spatial + alpha * global_nrmse}
32
+
33
+
34
+ def main() -> None:
35
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
36
+ data = np.load(ROOT / config["paths"]["inference"])
37
+ if str(data["format_version"]) != config["data"]["format_version"]:
38
+ raise ValueError("incompatible prediction format")
39
+ start, end = int(data["evaluation_start_year"]), int(data["evaluation_end_year"])
40
+ if (start, end) != (int(config["evaluation"]["start_year"]), int(config["evaluation"]["end_year"])):
41
+ raise ValueError("evaluation period metadata must be 2080-2100")
42
+ names = data["target_names"].tolist()
43
+ alpha = float(config["evaluation"]["global_weight"])
44
+ metrics = {name: climatebench_metrics(data["predictions"][:, index], data["targets"][:, index],
45
+ data["latitude"], alpha) for index, name in enumerate(names)}
46
+ report = {"variables": metrics, "mean_total_nrmse": float(np.mean([value["total_nrmse"] for value in metrics.values()])),
47
+ "evaluation_protocol": {"scenario": str(data["scenario"]), "period": f"{start}-{end}",
48
+ "target_aggregation": str(data["target_aggregation"]),
49
+ "spatial_metric": "cosine-area mean of grid-cell temporal RMSE",
50
+ "global_weighting": "cosine latitude cell area",
51
+ "total": "spatial_nrmse + 5 * global_nrmse",
52
+ "normalization": "cosine-area and sample mean of target field"}}
53
+ numeric = [number for values in metrics.values() for number in values.values()]
54
+ if not np.isfinite(numeric).all():
55
+ raise FloatingPointError("evaluation contains NaN or Inf")
56
+ output = ROOT / config["paths"]["evaluation_dir"]
57
+ output.mkdir(parents=True, exist_ok=True)
58
+ (output / "metrics.json").write_text(json.dumps(report, indent=2) + "\n")
59
+ figure, axes = plt.subplots(4, 3, figsize=(12, 12), constrained_layout=True)
60
+ longitude, latitude = data["longitude"], data["latitude"]
61
+ for index, name in enumerate(names):
62
+ fields = (data["targets"][:, index].mean(0), data["predictions"][:, index].mean(0),
63
+ (data["predictions"][:, index] - data["targets"][:, index]).mean(0))
64
+ for column, (field, title) in enumerate(zip(fields, ("Target", "Prediction", "Error"))):
65
+ image = axes[index, column].pcolormesh(longitude, latitude, field, shading="auto", cmap="coolwarm")
66
+ axes[index, column].set(title=f"{name}: {title}", xlabel="Longitude", ylabel="Latitude")
67
+ figure.colorbar(image, ax=axes[index, column], shrink=0.75)
68
+ figure.suptitle(f"ClimateBench {str(data['scenario'])} {start}-{end}")
69
+ figure.savefig(output / "four_targets.png", dpi=150)
70
+ plt.close(figure)
71
+ print(f"evaluation={output.relative_to(ROOT)} variables={','.join(names)}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
scripts/train.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train four independent paper CNN-LSTM branches with RMSprop and MSE."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import torch
10
+ import yaml
11
+ from torch.nn.parallel import DistributedDataParallel
12
+ from torch.utils.data import DataLoader, Dataset, DistributedSampler
13
+
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(ROOT))
17
+ from model.climatebench import ClimateBench, ClimateBenchBranch, TARGETS
18
+
19
+
20
+ class ClimateDataset(Dataset):
21
+ def __init__(self, path: Path, config: dict):
22
+ self.data = np.load(path)
23
+ expected = config["data"]
24
+ shape = (int(expected["time_steps"]), 4, int(expected["height"]), int(expected["width"]))
25
+ target_shape = (4, int(expected["height"]), int(expected["width"]))
26
+ if str(self.data["format_version"]) != expected["format_version"] or str(self.data["storage_layout"]) != "NTCHW":
27
+ raise ValueError("incompatible ClimateBench NPZ format or storage layout")
28
+ if self.data["inputs"].shape[1:] != shape or self.data["targets"].shape[1:] != target_shape:
29
+ raise ValueError(f"expected inputs [N,{shape}] and targets [N,{target_shape}]")
30
+ if self.data["inputs"].dtype != np.float32 or self.data["targets"].dtype != np.float32:
31
+ raise TypeError("inputs and targets must be float32")
32
+ if tuple(self.data["channel_names"].tolist()) != tuple(expected["channels"]):
33
+ raise ValueError("forcing channels must be [co2_cumulative,ch4,so2,bc]")
34
+
35
+ def __len__(self) -> int:
36
+ return len(self.data["inputs"])
37
+
38
+ def __getitem__(self, index: int):
39
+ return torch.from_numpy(self.data["inputs"][index]), torch.from_numpy(self.data["targets"][index])
40
+
41
+
42
+ def device_from_config(config: dict, rank: int = 0) -> torch.device:
43
+ requested = config["runtime"]["device"]
44
+ if requested == "auto":
45
+ return torch.device("cuda", rank) if torch.cuda.is_available() else torch.device("cpu")
46
+ return torch.device(requested)
47
+
48
+
49
+ def main() -> None:
50
+ config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
51
+ np.random.seed(int(config["seed"]))
52
+ torch.manual_seed(int(config["seed"]))
53
+ distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
54
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
55
+ if distributed:
56
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
57
+ rank = torch.distributed.get_rank() if distributed else 0
58
+ device = device_from_config(config, local_rank)
59
+ if device.type == "cuda":
60
+ torch.cuda.set_device(device)
61
+ dataset = ClimateDataset(ROOT / config["data"]["root"] / "train.npz", config)
62
+ sampler = DistributedSampler(dataset, shuffle=True) if distributed else None
63
+ loader = DataLoader(dataset, batch_size=int(config["train"]["batch_size"]), sampler=sampler,
64
+ shuffle=sampler is None, num_workers=int(config["train"]["num_workers"]))
65
+ model = ClimateBench(int(config["data"]["height"]), int(config["data"]["width"])).to(device)
66
+ counts = model.parameter_counts()
67
+ expected_count = int(config["model"]["parameters_per_target"])
68
+ if any(count != expected_count for count in counts.values()):
69
+ raise RuntimeError(f"parameter count mismatch: {counts}")
70
+ wrapped = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None) if distributed else model
71
+ optimizer = torch.optim.RMSprop(wrapped.parameters(), lr=float(config["train"]["learning_rate"]))
72
+ history = []
73
+ for epoch in range(int(config["train"]["epochs"])):
74
+ if sampler is not None:
75
+ sampler.set_epoch(epoch)
76
+ total, batches = 0.0, 0
77
+ for inputs, targets in loader:
78
+ prediction = wrapped(inputs.to(device))
79
+ loss = torch.nn.functional.mse_loss(prediction, targets.to(device))
80
+ optimizer.zero_grad(set_to_none=True)
81
+ loss.backward()
82
+ torch.nn.utils.clip_grad_norm_(wrapped.parameters(), float(config["train"]["gradient_clip_norm"]))
83
+ optimizer.step()
84
+ total += float(loss.detach())
85
+ batches += 1
86
+ history.append({"epoch": epoch + 1, "mse": total / max(batches, 1), "batches": batches})
87
+ model = wrapped.module if distributed else wrapped
88
+ if rank == 0:
89
+ checkpoint_path = ROOT / config["paths"]["checkpoint"]
90
+ metrics_path = ROOT / config["paths"]["training_metrics"]
91
+ checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
92
+ metrics_path.parent.mkdir(parents=True, exist_ok=True)
93
+ torch.save({"model": model.state_dict(), "parameter_counts": counts, "targets": TARGETS,
94
+ "format_version": config["data"]["format_version"], "storage_layout": "NTCHW",
95
+ "model_config": {"height": model.height, "width": model.width}}, checkpoint_path)
96
+ metrics_path.write_text(json.dumps({"history": history, "parameter_counts": counts}, indent=2) + "\n")
97
+ print(f"checkpoint={checkpoint_path.relative_to(ROOT)} batches={history[-1]['batches']} parameters={counts}")
98
+ if distributed:
99
+ torch.distributed.destroy_process_group()
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
weight/.gitkeep ADDED
File without changes