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

Publish NNCAM reproduction

Browse files
.gitattributes CHANGED
@@ -1,35 +1,7 @@
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
+ *.py text eol=lf
2
+ *.md text eol=lf
3
+ *.json text eol=lf
4
+ *.yaml text eol=lf
5
+ *.npz binary
6
+ *.pt binary
7
+ *.png binary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Climate Parameterization
9
+ - Subgrid Processes
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong><span style="font-size: 30px;">NNCAM</span></strong>
15
+ </p>
16
+
17
+ # Model Introduction
18
+
19
+ NNCAM predicts physical tendencies and fluxes produced by cloud, convection, and radiation subgrid processes from atmospheric-column states for data-driven climate-model parameterization research.
20
+
21
+ Paper: Deep learning to represent subgrid processes in climate models
22
+ https://gmd.copernicus.org/articles/11/3999/2018/
23
+
24
+ # Model Description
25
+
26
+ The method was proposed by research teams from Ludwig Maximilian University of Munich, the University of California Irvine, and Columbia University. The paper trains on approximately 140 million atmospheric-column samples from one year of SPCAM aquaplanet simulation. The model predicts 65 heating, moistening, radiative-flux, and precipitation outputs from a 94-dimensional atmospheric-column state.
27
+
28
+ # Use Cases
29
+
30
+ | Use Case | Description |
31
+ | :---: | :--- |
32
+ | Subgrid-process parameterization | Predict physical tendencies and fluxes from temperature, humidity, wind, and surface forcing. |
33
+ | Atmospheric-column diagnostics | Validate heating, moistening, radiation, and precipitation relationships over 30 levels. |
34
+ | Local engineering validation | Validate training, inference, conservation diagnostics, and visualization with structured synthetic samples. |
35
+ | ModelScope/OneCode execution | Validate structured data, training, inference, parameterization 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/NNCAM --local-dir ./NNCAM
50
+ cd NNCAM
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 SPCAM aquaplanet simulations with a 30-minute timestep and 30 vertical levels. Inputs are `[B,94]` temperature, humidity, wind, and surface-forcing columns, and targets are `[B,65]` heating, moistening, four radiative fluxes, and precipitation. This repository uses a small structured synthetic dataset for engineering validation only and does not represent the real SPCAM distribution, 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 reduces the paper's nine 256-node layers to four 32-node layers and shortens training without reducing the 94 inputs, 65 outputs, or 30-level vertical protocol. Formal experiments require real SPCAM data and the paper-scale model, with artifacts saved to:
102
+
103
+ ```text
104
+ result/checkpoints/nncam.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/nncam.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 training checkpoint and generates subgrid tendencies, radiative fluxes, and precipitation from complete atmospheric-column states. Complete numerical outputs are saved to:
119
+
120
+ ```text
121
+ result/output/predictions.npz
122
+ ```
123
+
124
+ ### Evaluation and Visualization
125
+
126
+ ```bash
127
+ python scripts/result.py
128
+ ```
129
+
130
+ Evaluation computes grouped RMSE and R² and generates grouped-error and precipitation-prediction comparisons. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
131
+
132
+ ```text
133
+ result/evaluation/metrics.json
134
+ result/evaluation/comparison.png
135
+ ```
136
+
137
+ # Official OneScience Information
138
+
139
+ | Platform | OneScience Main Repository | Skills Repository |
140
+ | --- | --- | --- |
141
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
142
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
143
+
144
+ # Citation and License
145
+
146
+ This repository is an independent engineering reproduction of the public NNCAM specifications.
147
+
148
+ 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,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;">NNCAM</span></strong>
16
+ </p>
17
+
18
+ # 模型介绍
19
+
20
+ NNCAM 根据大气柱状态预测云、对流和辐射等次网格过程产生的物理倾向与通量,主要用于数据驱动气候模式参数化研究。
21
+
22
+ 论文:Deep learning to represent subgrid processes in climate models
23
+ https://gmd.copernicus.org/articles/11/3999/2018/
24
+
25
+ # 模型描述
26
+
27
+ 该方法由慕尼黑大学、加州大学欧文分校和哥伦比亚大学的研究团队提出。论文使用 SPCAM 水行星模拟一年的约 1.4 亿个大气柱样本训练网络。模型适用于从 94 维大气柱状态预测 65 维加热、湿化、辐射通量和降水输出。
28
+
29
+ # 适用场景
30
+
31
+ | 场景 | 说明 |
32
+ | :---: | :--- |
33
+ | 次网格过程参数化 | 根据温度、湿度、风和地表强迫预测物理倾向与通量。 |
34
+ | 大气柱诊断 | 验证 30 层大气柱的加热、湿化、辐射和降水关系。 |
35
+ | 本地工程验证 | 使用结构化虚拟样本验证训练、推理、守恒诊断和可视化流程。 |
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/NNCAM --local_dir ./NNCAM
51
+ cd NNCAM
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
+ 论文训练数据来自 SPCAM 水行星模拟,时间步为 30 分钟并包含 30 个垂直层。输入为 `[B,94]` 的温度、湿度、风和地表强迫,目标为 `[B,65]` 的加热、湿化、四个辐射通量及降水。本仓库使用少量具有垂直和物理关联的虚拟样本验证工程流程,不代表 SPCAM 的真实数据分布、训练规模或论文性能。
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
+ 默认配置将网络由论文的 9 层、每层 256 个节点缩小为 4 层、每层 32 个节点,并减少训练轮数,但不缩小 94 维输入、65 维输出和 30 层垂直协议。正式实验需要真实 SPCAM 数据和论文规模模型,训练产物保存到:
103
+
104
+ ```text
105
+ result/checkpoints/nncam.pt
106
+ result/training/metrics.json
107
+ ```
108
+
109
+ ### 训练权重
110
+
111
+ 论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/nncam.pt`,不得描述为官方预训练权重。
112
+
113
+ ### 推理
114
+
115
+ ```bash
116
+ python scripts/inference.py
117
+ ```
118
+
119
+ 推理加载训练 checkpoint,根据完整大气柱状态生成次网格倾向、辐射通量和降水预测。完整数值结果保存到:
120
+
121
+ ```text
122
+ result/output/predictions.npz
123
+ ```
124
+
125
+ ### 评估和可视化
126
+
127
+ ```bash
128
+ python scripts/result.py
129
+ ```
130
+
131
+ 评估按输出组计算 RMSE 和 R²,并生成分组误差及降水预测对比图。虚拟数据结果仅用于验证工程流程,不代表论文正式性能;结果保存到:
132
+
133
+ ```text
134
+ result/evaluation/metrics.json
135
+ result/evaluation/comparison.png
136
+ ```
137
+
138
+ # OneScience官方信息
139
+
140
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
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
+ # 引用与许可证
146
+
147
+ 本仓库为 NNCAM 公开规格的独立工程复现版本。
148
+
149
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project:
2
+ name: NNCAM
3
+ format_version: "1.0"
4
+ seed: 42
5
+
6
+ dimensions:
7
+ input: 94
8
+ output: 65
9
+ levels: 30
10
+
11
+ paths:
12
+ data: data/nncam_fake.npz
13
+ checkpoint: result/checkpoints/nncam.pt
14
+ training_metrics: result/training/metrics.json
15
+ predictions: result/output/predictions.npz
16
+ evaluation_metrics: result/evaluation/metrics.json
17
+ figure: result/evaluation/comparison.png
18
+
19
+ data:
20
+ samples: 192
21
+ seed: 42
22
+
23
+ model:
24
+ depth: 4
25
+ width: 32
26
+ negative_slope: 0.3
27
+
28
+ training:
29
+ epochs: 3
30
+ batch_size: 64
31
+ learning_rate: 0.001
32
+
33
+ paper_model:
34
+ depth: 9
35
+ width: 256
36
+ epochs: 18
37
+ batch_size: 1024
38
+ parameters: 567361
39
+ note: Recorded paper-scale architecture; enabled only by the explicit --paper-model flag.
config.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "NNCAM",
3
+ "model_type": "nncam",
4
+ "architectures": ["NNCAM"],
5
+ "framework": "PyTorch",
6
+ "domain": "atmospheric-physics",
7
+ "task": "climate-model-subgrid-parameterization",
8
+ "implementation": {
9
+ "entry_point": "model/nncam.py",
10
+ "scope": "core-method and full-column 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", 94],
18
+ "output_shape": ["B", 65],
19
+ "engineering_depth": 4,
20
+ "engineering_width": 32,
21
+ "paper_depth": 9,
22
+ "paper_width": 256,
23
+ "activation": "LeakyReLU"
24
+ },
25
+ "data": {
26
+ "dataset": "SPCAM aquaplanet simulation",
27
+ "format_version": "1.0",
28
+ "vertical_levels": 30,
29
+ "input_variables": ["T", "Q", "V", "Ps", "Sin", "H", "E"],
30
+ "output_variables": ["dT", "dQ", "SWtoa", "SWsfc", "LWtoa", "LWsfc", "P"],
31
+ "input_layout": "NF",
32
+ "synthetic": true
33
+ },
34
+ "configuration_sources": [
35
+ "conf/config.yaml",
36
+ "model/nncam.py",
37
+ "scripts/fake_data.py",
38
+ "scripts/train.py",
39
+ "scripts/inference.py",
40
+ "scripts/result.py"
41
+ ]
42
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "atmospheric subgrid-process parameterization",
4
+ "model": "NNCAM",
5
+ "input_format": "NF: atmospheric column state [N,94]",
6
+ "protocol": "SPCAM-supervised offline tendency prediction and engineering column diagnostics",
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/nncam.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import numpy as np
4
+ import torch
5
+ from torch import nn
6
+
7
+
8
+ INPUT_GROUPS = {"T": slice(0, 30), "Q": slice(30, 60), "V": slice(60, 90), "Ps": slice(90, 91), "Sin": slice(91, 92), "H": slice(92, 93), "E": slice(93, 94)}
9
+ OUTPUT_GROUPS = {"dT": slice(0, 30), "dQ": slice(30, 60), "SWtoa": slice(60, 61), "SWsfc": slice(61, 62), "LWtoa": slice(62, 63), "LWsfc": slice(63, 64), "P": slice(64, 65)}
10
+ CP = 1004.0
11
+ LV = 2.5e6
12
+ OUTPUT_SCALE = np.r_[np.full(30, CP), np.full(30, LV), np.full(4, 1e-3), 2e-2].astype(np.float32)
13
+
14
+
15
+ class NNCAM(nn.Module):
16
+ """Fully connected 94-to-65 atmospheric-column parameterization."""
17
+
18
+ def __init__(self, input_dim=94, output_dim=65, width=32, depth=4, negative_slope=0.3):
19
+ super().__init__()
20
+ if input_dim != 94 or output_dim != 65:
21
+ raise ValueError("NNCAM requires 94 inputs and 65 outputs")
22
+ if width < 1 or depth < 1:
23
+ raise ValueError("width and depth must be positive")
24
+ layers = []
25
+ in_features = input_dim
26
+ for _ in range(depth):
27
+ layers.extend((nn.Linear(in_features, width), nn.LeakyReLU(negative_slope)))
28
+ in_features = width
29
+ layers.append(nn.Linear(in_features, output_dim))
30
+ self.network = nn.Sequential(*layers)
31
+ self.model_config = {"input_dim": input_dim, "output_dim": output_dim, "width": width, "depth": depth, "negative_slope": negative_slope}
32
+
33
+ def forward(self, x):
34
+ if x.ndim != 2 or x.shape[1] != 94:
35
+ raise ValueError(f"expected input [B, 94], got {tuple(x.shape)}")
36
+ output = self.network(x)
37
+ if output.shape != (x.shape[0], 65):
38
+ raise RuntimeError(f"expected output [B, 65], got {tuple(output.shape)}")
39
+ return output
40
+
41
+
42
+ def build_model(width=32, depth=4, negative_slope=0.3):
43
+ return NNCAM(width=width, depth=depth, negative_slope=negative_slope)
44
+
45
+
46
+ def generate_fake_data(n_samples=192, seed=42):
47
+ rng = np.random.default_rng(seed)
48
+ sigma = np.linspace(0.02, 1.0, 30, dtype=np.float32)[None, :]
49
+ lat = rng.uniform(-math.pi / 2, math.pi / 2, (n_samples, 1)).astype(np.float32)
50
+ time = rng.uniform(0, 2 * math.pi, (n_samples, 1)).astype(np.float32)
51
+ ps = rng.normal(101000.0, 1800.0, (n_samples, 1)).astype(np.float32)
52
+ insolation = np.maximum(0.0, 950.0 * np.cos(lat) * (0.65 + 0.35 * np.sin(time))).astype(np.float32)
53
+ height = rng.uniform(0.0, 2500.0, (n_samples, 1)).astype(np.float32)
54
+ evaporation = (35.0 + 80.0 * np.maximum(np.cos(lat), 0.0) + rng.normal(0, 4, (n_samples, 1))).astype(np.float32)
55
+ temperature = 205.0 + 83.0 * sigma**0.24 - 0.006 * height + 4.0 * np.cos(lat) * sigma
56
+ temperature += rng.normal(0, 1.2, temperature.shape)
57
+ humidity = (0.00005 + 0.017 * sigma**3 * np.maximum(np.cos(lat), 0.15)) * rng.lognormal(0, 0.12, temperature.shape)
58
+ wind = 18.0 * np.sin(lat) * (1.0 - sigma) + 5.0 * np.sin(time + 3.0 * sigma) + rng.normal(0, 2, temperature.shape)
59
+ instability = np.maximum(temperature[:, -1:] - temperature[:, 18:19] - 25.0, 0.0)
60
+ moisture = humidity[:, -8:].mean(1, keepdims=True)
61
+ precipitation = np.maximum(0.0, 4.0e-5 * instability * moisture / 0.012 + rng.normal(0, 1.5e-6, (n_samples, 1))).astype(np.float32)
62
+ heating_shape = np.exp(-((sigma - 0.55) / 0.23) ** 2)
63
+ drying_shape = np.exp(-((sigma - 0.78) / 0.18) ** 2)
64
+ dt = (precipitation * LV / CP / 86400.0 * heating_shape - 8e-6 * (temperature - temperature.mean(1, keepdims=True))).astype(np.float32)
65
+ dq = (-precipitation / 86400.0 * drying_shape + evaporation / LV / 30.0 / 86400.0).astype(np.float32)
66
+ cloud = np.clip(moisture / 0.014, 0.0, 1.0)
67
+ targets = (dt, dq, 0.30 * insolation, insolation * (0.72 - 0.18 * cloud), 215.0 + 0.65 * (temperature[:, -1:] - 273.0) - 24.0 * cloud, 330.0 + 1.1 * (temperature[:, -1:] - 285.0) + 18.0 * cloud, precipitation)
68
+ x = np.concatenate((temperature, humidity, wind, ps, insolation, height, evaporation), axis=1).astype(np.float32)
69
+ y = np.concatenate(targets, axis=1).astype(np.float32)
70
+ return x, y, lat[:, 0], time[:, 0]
71
+
72
+
73
+ def fit_normalizer(values):
74
+ mean = values.mean(0).astype(np.float32)
75
+ scale = np.maximum(np.ptp(values, axis=0), values.std(0))
76
+ return mean, np.where(scale > 1e-12, scale, 1.0).astype(np.float32)
77
+
78
+
79
+ def normalize_input(values, mean, scale):
80
+ return (values - mean) / scale
81
+
82
+
83
+ def scale_output(values):
84
+ return values * OUTPUT_SCALE
85
+
86
+
87
+ def unscale_output(values):
88
+ return values / OUTPUT_SCALE
scripts/fake_data.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ import sys
9
+ sys.path.insert(0, str(ROOT))
10
+ from model.nncam import generate_fake_data
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="Create deterministic synthetic NNCAM columns.")
15
+ parser.add_argument("--samples", type=int, default=192)
16
+ parser.add_argument("--seed", type=int, default=42)
17
+ parser.add_argument("--output", type=Path, default=ROOT / "data/nncam_fake.npz")
18
+ args = parser.parse_args()
19
+ x, y, lat, time = generate_fake_data(args.samples, args.seed)
20
+ assert x.shape == (args.samples, 94) and y.shape == (args.samples, 65)
21
+ assert np.isfinite(x).all() and np.isfinite(y).all() and (y[:, 64] >= 0).all()
22
+ args.output.parent.mkdir(parents=True, exist_ok=True)
23
+ np.savez_compressed(args.output, x=x, y=y, lat=lat, time=time)
24
+ print(f"saved {args.output}: x={x.shape}, y={y.shape}, finite=true")
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import torch
7
+
8
+ from model.nncam import NNCAM, unscale_output
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description="Run offline NNCAM inference.")
16
+ parser.add_argument("--data", type=Path, default=ROOT / "data/nncam_fake.npz")
17
+ parser.add_argument("--checkpoint", type=Path, default=ROOT / "result/checkpoints/nncam.pt")
18
+ parser.add_argument("--output", type=Path, default=ROOT / "result/output/predictions.npz")
19
+ args = parser.parse_args()
20
+ checkpoint = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
21
+ required = {"model", "model_config", "format_version", "normalization"}
22
+ if not required.issubset(checkpoint):
23
+ raise ValueError(f"checkpoint missing {sorted(required - checkpoint.keys())}")
24
+ model = NNCAM(**checkpoint["model_config"])
25
+ model.load_state_dict(checkpoint["model"])
26
+ model.eval()
27
+ with np.load(args.data) as data:
28
+ x, truth, lat, time = (data[name] for name in ("x", "y", "lat", "time"))
29
+ norm = checkpoint["normalization"]
30
+ normalized = (torch.from_numpy(x) - norm["input_mean"]) / norm["input_scale"]
31
+ with torch.no_grad():
32
+ scaled = model(normalized) * norm["target_scale"] + norm["target_mean"]
33
+ prediction = unscale_output(scaled.numpy()).astype(np.float32)
34
+ if prediction.shape != truth.shape or not np.isfinite(prediction).all():
35
+ raise RuntimeError(f"invalid prediction shape or values: {prediction.shape}")
36
+ args.output.parent.mkdir(parents=True, exist_ok=True)
37
+ np.savez_compressed(args.output, input=x, truth=truth, prediction=prediction, lat=lat, time=time)
38
+ print(f"saved {args.output}: prediction={prediction.shape}, finite=true")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
scripts/result.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
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
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+
14
+
15
+ def score(truth, prediction):
16
+ rmse = float(np.sqrt(np.mean((truth - prediction) ** 2)))
17
+ denominator = float(np.sum((truth - truth.mean()) ** 2))
18
+ return {"rmse": rmse, "r2": float(1.0 - np.sum((truth - prediction) ** 2) / denominator) if denominator else None}
19
+
20
+
21
+ def main():
22
+ parser = argparse.ArgumentParser(description="Evaluate NNCAM predictions.")
23
+ parser.add_argument("--input", type=Path, default=ROOT / "result/output/predictions.npz")
24
+ parser.add_argument("--metrics", type=Path, default=ROOT / "result/evaluation/metrics.json")
25
+ parser.add_argument("--figure", type=Path, default=ROOT / "result/evaluation/comparison.png")
26
+ args = parser.parse_args()
27
+ with np.load(args.input) as data:
28
+ truth, prediction = data["truth"], data["prediction"]
29
+ if truth.shape != prediction.shape or truth.ndim != 2 or truth.shape[1] != 65:
30
+ raise ValueError(f"expected matching [N,65] arrays, got {truth.shape}, {prediction.shape}")
31
+ groups = {"dT": slice(0, 30), "dQ": slice(30, 60), "SW": slice(60, 62), "LW": slice(62, 64), "P": slice(64, 65)}
32
+ metrics = {name: score(truth[:, indices], prediction[:, indices]) for name, indices in groups.items()}
33
+ metrics["overall"] = score(truth, prediction)
34
+ values = np.array([value for group in metrics.values() for value in group.values() if value is not None])
35
+ if not np.isfinite(values).all():
36
+ raise RuntimeError("evaluation metrics contain non-finite values")
37
+ args.metrics.parent.mkdir(parents=True, exist_ok=True)
38
+ args.metrics.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
39
+ names = list(groups)
40
+ fig, axes = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True)
41
+ axes[0].bar(names, [metrics[name]["rmse"] for name in names])
42
+ axes[0].set_title("Grouped RMSE")
43
+ axes[1].scatter(truth[:, 64], prediction[:, 64], s=12, alpha=0.7)
44
+ axes[1].set(xlabel="True precipitation", ylabel="Predicted precipitation", title="Precipitation comparison")
45
+ fig.savefig(args.figure, dpi=150)
46
+ plt.close(fig)
47
+ print(f"saved {args.metrics} and {args.figure}; shape={prediction.shape}, finite=true")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
scripts/train.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import torch
8
+ from torch.utils.data import DataLoader, TensorDataset
9
+
10
+ from model.nncam import build_model, fit_normalizer, normalize_input, scale_output
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(description="Train NNCAM on the prepared NPZ dataset.")
18
+ parser.add_argument("--data", type=Path, default=ROOT / "data/nncam_fake.npz")
19
+ parser.add_argument("--checkpoint", type=Path, default=ROOT / "result/checkpoints/nncam.pt")
20
+ parser.add_argument("--metrics", type=Path, default=ROOT / "result/training/metrics.json")
21
+ parser.add_argument("--epochs", type=int)
22
+ parser.add_argument("--batch-size", type=int)
23
+ parser.add_argument("--width", type=int)
24
+ parser.add_argument("--depth", type=int)
25
+ parser.add_argument("--paper-model", action="store_true", help="Explicitly use depth=9, width=256, epochs=18, batch_size=1024 (567361 parameters).")
26
+ parser.add_argument("--lr", type=float, default=1e-3)
27
+ parser.add_argument("--seed", type=int, default=42)
28
+ args = parser.parse_args()
29
+ if not args.data.is_file():
30
+ raise FileNotFoundError(f"missing dataset {args.data}; run python scripts/fake_data.py first")
31
+ defaults = {"depth": 9, "width": 256, "epochs": 18, "batch_size": 1024} if args.paper_model else {"depth": 4, "width": 32, "epochs": 3, "batch_size": 64}
32
+ depth, width = args.depth or defaults["depth"], args.width or defaults["width"]
33
+ epochs, batch_size = args.epochs or defaults["epochs"], args.batch_size or defaults["batch_size"]
34
+ torch.manual_seed(args.seed)
35
+ with np.load(args.data) as data:
36
+ x, y = data["x"].astype(np.float32), data["y"].astype(np.float32)
37
+ if x.ndim != 2 or x.shape[1] != 94 or y.shape != (x.shape[0], 65):
38
+ raise ValueError(f"expected x=[N,94], y=[N,65], got {x.shape}, {y.shape}")
39
+ input_mean, input_scale = fit_normalizer(x)
40
+ scaled_y = scale_output(y)
41
+ target_mean, target_scale = fit_normalizer(scaled_y)
42
+ dataset = TensorDataset(torch.from_numpy(normalize_input(x, input_mean, input_scale)), torch.from_numpy(normalize_input(scaled_y, target_mean, target_scale)))
43
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
44
+ model = build_model(width=width, depth=depth)
45
+ optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
46
+ scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.2)
47
+ history = []
48
+ for epoch in range(epochs):
49
+ total = 0.0
50
+ for xb, yb in loader:
51
+ optimizer.zero_grad(set_to_none=True)
52
+ loss = torch.nn.functional.mse_loss(model(xb), yb)
53
+ loss.backward()
54
+ optimizer.step()
55
+ total += loss.item() * len(xb)
56
+ history.append(total / len(dataset))
57
+ scheduler.step()
58
+ print(f"epoch={epoch + 1:02d} loss={history[-1]:.6f}")
59
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
60
+ checkpoint = {
61
+ "format_version": 1,
62
+ "model": model.state_dict(),
63
+ "model_config": model.model_config,
64
+ "normalization": {"input_mean": torch.from_numpy(input_mean), "input_scale": torch.from_numpy(input_scale), "target_mean": torch.from_numpy(target_mean), "target_scale": torch.from_numpy(target_scale)},
65
+ "training": {"epochs": epochs, "batch_size": batch_size, "learning_rate": args.lr, "paper_model": args.paper_model, "parameters": parameter_count},
66
+ }
67
+ args.checkpoint.parent.mkdir(parents=True, exist_ok=True)
68
+ args.metrics.parent.mkdir(parents=True, exist_ok=True)
69
+ torch.save(checkpoint, args.checkpoint)
70
+ args.metrics.write_text(json.dumps({"loss": history, "final_loss": history[-1], "parameters": parameter_count, "model_config": model.model_config}, indent=2), encoding="utf-8")
71
+ print(f"saved {args.checkpoint}; parameters={parameter_count}; paper_model={args.paper_model}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
weight/.gitkeep ADDED
File without changes