zhangrenchao commited on
Commit
702a3cf
·
verified ·
1 Parent(s): a349ff4

Publish PBL-Emulator engineering 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,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - Earth Science
8
+ - Planetary Boundary Layer
9
+ - Domain-aware Neural Network
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong><span style="font-size: 30px;">PBL-Emulator</span></strong>
15
+ </p>
16
+
17
+ # Model Introduction
18
+
19
+ PBL-Emulator uses domain-aware neural networks to diagnose same-timestamp vertical profiles of planetary-boundary-layer winds, temperature, and water vapor offline from near-surface states and forcing variables.
20
+
21
+ Paper: Fast domain-aware neural network emulation of a planetary boundary layer parameterization in a numerical weather forecast model
22
+ https://doi.org/10.5194/gmd-12-4261-2019
23
+
24
+ # Model Description
25
+
26
+ The method was proposed by research teams in the Environmental Science Division and Mathematics and Computer Science Division at Argonne National Laboratory.
27
+ The paper uses data from 1984-2005 generated by WRF v3.3.1 with NCEP-R2 forcing and the YSU planetary boundary layer scheme.
28
+ The model is intended for offline diagnosis of PBL vertical profiles at the same timestamp, not forecasting future states.
29
+
30
+ # Applicable Scenarios
31
+
32
+ | Scenario | Description |
33
+ | :---: | :--- |
34
+ | Offline PBL profile diagnosis | Diagnose same-timestamp wind, temperature, and water-vapor profiles from existing near-surface states and forcing variables. |
35
+ | HPC/HAC vertical-dependency studies | Validate adjacent-lower-level conditioning in HPC and all-lower-level conditioning in HAC. |
36
+ | Engineering validation | Validate data generation, training, inference, evaluation, and visualization with structured synthetic data. |
37
+ | ModelScope/OneCode execution | Validate structured data, training, inference, PBL-profile metrics, and visualization in ModelScope or OneCode environments. |
38
+ | Multi-GPU training | Validate distributed training and the checkpoint workflow through `torchrun`. |
39
+
40
+ # Usage Instructions
41
+
42
+ ## 1.OneCode
43
+
44
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
45
+
46
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
47
+
48
+ ## 2. Download and Installation
49
+
50
+ ```bash
51
+ hf download OneScience-Group/PBL-Emulator --local-dir ./PBL-Emulator
52
+ cd PBL-Emulator
53
+ ```
54
+
55
+ ### Environment Dependencies
56
+
57
+ **Hardware Requirements**
58
+
59
+ - A GPU or DCU is recommended.
60
+ - A CPU can be used to validate the workflow with the default small-sample configuration.
61
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
62
+
63
+ **DCU Environment**
64
+
65
+ ```bash
66
+ # Activate DTK and Conda first
67
+ conda create -n onescience311 python=3.11 -y
68
+ conda activate onescience311
69
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
70
+ pip install numpy pyyaml matplotlib
71
+ ```
72
+
73
+ **GPU Environment**
74
+
75
+ ```bash
76
+ # Activate Conda first
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
+ pip install numpy pyyaml matplotlib
81
+ ```
82
+
83
+ ### Training Data
84
+
85
+ Each sample maps 16 near-surface state and forcing inputs to five outputs at 17 vertical positions at the same timestamp, from `[N,16]` to `[N,17,5]`. The structured synthetic data include diurnal and seasonal cycles, vertical structure, and physical relationships among heat, moisture, and wind. These data are for engineering validation only and do not represent WRF data distributions, dataset scale, or the paper's performance.
86
+
87
+ ```bash
88
+ python scripts/fake_data.py
89
+ ```
90
+
91
+ ### Training
92
+
93
+ For single-device training, use:
94
+
95
+ ```bash
96
+ python scripts/train.py
97
+ ```
98
+
99
+ For multi-GPU training, use:
100
+
101
+ ```bash
102
+ 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
103
+ ```
104
+
105
+ The default configuration reduces only the training duration from the paper protocol's 1,000 epochs to six epochs; it does not reduce the input or output dimensions. Formal experiments require the 1984-2005 WRF/NCEP-R2/YSU data and the complete training duration, and training artifacts are saved to:
106
+
107
+ ```text
108
+ result/checkpoints/pbl_emulator.pt
109
+ result/training/metrics.json
110
+ ```
111
+
112
+ ### Trained Weights
113
+
114
+ No official paper weights are bundled under `weight/`, and no directly loadable pretrained checkpoint released by the paper authors was identified. The locally generated `result/checkpoints/pbl_emulator.pt` is an engineering checkpoint for the current data and must not be represented as an official pretrained weight.
115
+
116
+ ### Inference
117
+
118
+ ```bash
119
+ python scripts/inference.py
120
+ ```
121
+
122
+ Inference loads the training checkpoint and generates same-timestamp PBL vertical-profile diagnoses for the test samples. Complete numerical results are saved to:
123
+
124
+ ```text
125
+ result/output/predictions.npz
126
+ ```
127
+
128
+ ### Evaluation and Visualization
129
+
130
+ ```bash
131
+ python scripts/result.py
132
+ ```
133
+
134
+ Evaluation reports per-variable errors and correlations and plots target and predicted profiles. Results on synthetic data are for engineering validation only and do not represent the paper's performance; outputs are saved to:
135
+
136
+ ```text
137
+ result/evaluation/metrics.json
138
+ result/evaluation/profiles.png
139
+ ```
140
+
141
+ # Official OneScience Information
142
+
143
+ | Platform | OneScience Main Repository | Skills Repository |
144
+ | --- | --- | --- |
145
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
146
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
147
+
148
+ # Citation and License
149
+
150
+ This repository is an independent engineering reproduction of the public PBL-Emulator specifications; the paper text is licensed under CC BY 4.0, while the official paper code uses BSD-3-Clause.
151
+
152
+ 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,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 行星边界层
10
+ - Domain-aware Neural Network
11
+ frameworks: PyTorch
12
+ ---
13
+
14
+ <p align="center">
15
+ <strong><span style="font-size: 30px;">PBL-Emulator</span></strong>
16
+ </p>
17
+
18
+ # 模型介绍
19
+
20
+ PBL-Emulator 使用领域感知神经网络,根据近地面状态与强迫变量离线诊断同一时刻的行星边界层风、温度和水汽垂直剖面。
21
+
22
+ 论文:Fast domain-aware neural network emulation of a planetary boundary layer parameterization in a numerical weather forecast model
23
+ https://doi.org/10.5194/gmd-12-4261-2019
24
+
25
+ # 模型描述
26
+
27
+ 该方法由 Argonne National Laboratory 环境科学部、数学与计算机科学部的研究团队提出。
28
+ 论文使用 WRF v3.3.1 在 NCEP-R2 驱动下采用 YSU 行星边界层方案生成的 1984-2005 年数据。
29
+ 模型适用于离线诊断同一时刻的 PBL 垂直剖面,不用于未来时刻预报。
30
+
31
+ # 适用场景
32
+
33
+ | 场景 | 说明 |
34
+ | :---: | :--- |
35
+ | 离线 PBL 剖面诊断 | 根据已有近地面状态与强迫变量诊断同一时刻的风、温度和水汽剖面。 |
36
+ | HPC/HAC 垂向依赖研究 | 验证 HPC 的相邻低层条件依赖和 HAC 的全部低层条件依赖。 |
37
+ | 工程验证 | 使用结构化虚拟数据验证数据生成、训练、推理、评估和可视化流程。 |
38
+ | ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证结构化数据、训练、推理、PBL 剖面指标和可视化流程。 |
39
+ | 多卡训练 | 通过 `torchrun` 验证分布式训练和 checkpoint 流程。 |
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/PBL-Emulator --local_dir ./PBL-Emulator
53
+ cd PBL-Emulator
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
+ pip install numpy pyyaml matplotlib
72
+ ```
73
+
74
+ **GPU环境**
75
+
76
+ ```bash
77
+ # 请首先激活 Conda
78
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
79
+ conda activate onescience311
80
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
81
+ pip install numpy pyyaml matplotlib
82
+ ```
83
+
84
+ ### 训练数据介绍
85
+
86
+ 每个样本由 16 个近地面状态和强迫变量输入映射到同一时刻 17 个垂直位置上的 5 个变量输出,数据形状为 `[N,16]` 到 `[N,17,5]`。结构化虚拟数据包含昼夜和季节变化、垂向结构以及热量、湿度和风场等物理关联。这些数据仅用于工程验证,不代表 WRF 的数据分布、数据规模或论文性能。
87
+
88
+ ```bash
89
+ python scripts/fake_data.py
90
+ ```
91
+
92
+ ### 训练
93
+
94
+ 单卡训练可使用:
95
+
96
+ ```bash
97
+ python scripts/train.py
98
+ ```
99
+
100
+ 多卡训练可使用:
101
+
102
+ ```bash
103
+ 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
104
+ ```
105
+
106
+ 默认配置只将训练轮数从论文协议的 1000 epochs 缩小为 6 epochs,不缩小输入或输出维度。正式实验应使用 1984-2005 年 WRF/NCEP-R2/YSU 数据和完整训练轮数,训练产物保存到:
107
+
108
+ ```text
109
+ result/checkpoints/pbl_emulator.pt
110
+ result/training/metrics.json
111
+ ```
112
+
113
+ ### 训练权重
114
+
115
+ 本仓库不在 `weight/` 中内置论文官方权重,也未发现论文作者发布的可直接加载预训练 checkpoint。本地训练生成的 `result/checkpoints/pbl_emulator.pt` 是当前数据对应的工程 checkpoint,不得描述为官方预训练权重。
116
+
117
+ ### 推理
118
+
119
+ ```bash
120
+ python scripts/inference.py
121
+ ```
122
+
123
+ 推理加载训练 checkpoint,为测试样本生成同一时刻的 PBL 垂直剖面诊断结果。完整数值结果保存到:
124
+
125
+ ```text
126
+ result/output/predictions.npz
127
+ ```
128
+
129
+ ### 评估和可视化
130
+
131
+ ```bash
132
+ python scripts/result.py
133
+ ```
134
+
135
+ 评估提供分变量误差和相关性,并绘制目标与预测剖面图。虚拟数据结果仅用于工程验证,不代表论文性能,结果保存到:
136
+
137
+ ```text
138
+ result/evaluation/metrics.json
139
+ result/evaluation/profiles.png
140
+ ```
141
+
142
+ # OneScience官方信息
143
+
144
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
145
+ | --- | --- | --- |
146
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
147
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
148
+
149
+ # 引用与许可证
150
+
151
+ 本仓���为 PBL-Emulator 公开规格的独立工程复现版本;论文文本采用 CC BY 4.0,论文官方代码采用 BSD-3-Clause。
152
+
153
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project:
2
+ name: PBL-Emulator
3
+ format_version: "1.0"
4
+ seed: 19
5
+ independent_code_license: Apache-2.0
6
+
7
+ paper_specification:
8
+ title: Fast domain-aware neural network emulation of a planetary boundary layer parameterization in a numerical weather forecast model
9
+ doi: 10.5194/gmd-12-4261-2019
10
+ dimensions: {input: 16, levels: 17, variables_per_level: 5}
11
+ output_variables: [U, V, W, tk, QVAPOR]
12
+ architectures: {FFN_hidden_dense: 34, hierarchical_hidden_dense_per_level: 3, width: 16}
13
+ training: {optimizer: Adam, learning_rate: 0.001, epochs: 1000, batch_size: 64, early_stopping_patience: 10}
14
+ temporal_protocol: "3-hourly input and same-timestamp diagnostic output"
15
+
16
+ engineering_assumptions:
17
+ default_architecture: HAC
18
+ synthetic_data: "structured engineering data, not WRF observations or paper data"
19
+ split: "chronological 70/15/15 percent"
20
+ vertical_order: bottom_to_top
21
+
22
+ paths:
23
+ data: data/pbl_emulator_synthetic.npz
24
+ checkpoint: result/checkpoints/pbl_emulator.pt
25
+ training_metrics: result/training/metrics.json
26
+ predictions: result/output/predictions.npz
27
+ evaluation_metrics: result/evaluation/metrics.json
28
+ figure: result/evaluation/profiles.png
29
+
30
+ data:
31
+ samples: 768
32
+ seed: 7
33
+ interval_hours: 3
34
+
35
+ model:
36
+ architecture: HAC
37
+ width: 16
38
+ levels: 17
39
+ output_variables: 5
40
+
41
+ training:
42
+ epochs: 6
43
+ batch_size: 64
44
+ learning_rate: 0.001
45
+ early_stopping_patience: 10
46
+ seed: 19
47
+ resume: false
48
+ num_workers: 0
49
+
50
+ paper_model:
51
+ epochs: 1000
52
+ batch_size: 64
53
+ width: 16
config.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "PBL-Emulator",
3
+ "model_type": "pbl_emulator",
4
+ "architectures": ["PBLEmulator"],
5
+ "framework": "PyTorch",
6
+ "domain": "earth-science",
7
+ "task": "same-time-pbl-profile-diagnosis",
8
+ "implementation": {
9
+ "entry_point": "model/pbl_emulator.py",
10
+ "scope": "independent paper-method 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", 16],
18
+ "output_shape": ["B", 17, 5],
19
+ "default": "HAC",
20
+ "width": 16,
21
+ "parameter_counts": {"FFN": 10693, "HPC": 16597, "HAC": 26197}
22
+ },
23
+ "data": {
24
+ "format": "NPZ",
25
+ "protocol": "pbl_emulator_synthetic_v1",
26
+ "split": "chronological 70/15/15 percent",
27
+ "interval_hours": 3,
28
+ "input_shape": ["N", 16],
29
+ "target_shape": ["N", 17, 5],
30
+ "input_variables": ["Q2", "T2", "U10", "V10", "GRDFLX", "SWDOWN", "GLW", "LH", "HFX", "PBLH", "UST", "TSK", "TSLB", "SMOIS", "Ug", "Vg"],
31
+ "output_variables": ["U", "V", "W", "tk", "QVAPOR"],
32
+ "metadata": ["timestamps_hours", "virtual_heights_m", "synthetic_pblh_m"],
33
+ "synthetic": true
34
+ },
35
+ "configuration_sources": [
36
+ "conf/config.yaml",
37
+ "model/pbl_emulator.py",
38
+ "scripts/fake_data.py",
39
+ "scripts/train.py",
40
+ "scripts/inference.py",
41
+ "scripts/result.py"
42
+ ]
43
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "Offline emulation of WRF YSU planetary boundary layer diagnostics",
4
+ "model": "PBL-Emulator",
5
+ "input_format": "N16 near-surface and forcing variables",
6
+ "output_format": "N17x5 [U,V,W,tk,QVAPOR] at the same 3-hour timestamp",
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/pbl_emulator.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import os
6
+ import random
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import torch
13
+ from torch import nn
14
+ from torch.nn.parallel import DistributedDataParallel
15
+ from torch.utils.data import DataLoader, TensorDataset
16
+
17
+
18
+ INPUT_NAMES = ["Q2", "T2", "U10", "V10", "GRDFLX", "SWDOWN", "GLW", "LH", "HFX", "PBLH", "UST", "TSK", "TSLB", "SMOIS", "Ug", "Vg"]
19
+ OUTPUT_NAMES = ["U", "V", "W", "tk", "QVAPOR"]
20
+ PARAMETER_COUNTS = {"FFN": 10693, "HPC": 16597, "HAC": 26197}
21
+ CHECKPOINT_FORMAT_VERSION = "1.0"
22
+
23
+
24
+ def load_yaml(path: str | Path) -> dict[str, Any]:
25
+ try:
26
+ import yaml
27
+ except ImportError as exc:
28
+ raise RuntimeError("PyYAML is required to read conf/config.yaml") from exc
29
+ with open(path, "r", encoding="utf-8") as handle:
30
+ return yaml.safe_load(handle)
31
+
32
+
33
+ class FFN(nn.Module):
34
+ def __init__(self, width: int = 16, levels: int = 17, variables: int = 5):
35
+ super().__init__()
36
+ layers: list[nn.Module] = []
37
+ in_features = 16
38
+ for _ in range(34):
39
+ layers.extend((nn.Linear(in_features, width), nn.ReLU()))
40
+ in_features = width
41
+ self.hidden = nn.Sequential(*layers)
42
+ self.output = nn.Linear(width, levels * variables)
43
+ self.levels, self.variables = levels, variables
44
+
45
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
46
+ return self.output(self.hidden(x)).reshape(-1, self.levels, self.variables)
47
+
48
+
49
+ class HierarchicalNetwork(nn.Module):
50
+ def __init__(self, mode: str, width: int = 16, levels: int = 17, variables: int = 5):
51
+ super().__init__()
52
+ self.mode, self.levels, self.variables = mode, levels, variables
53
+ blocks = []
54
+ for level in range(levels):
55
+ conditioned_outputs = variables * (level if mode == "HAC" else min(level, 1))
56
+ blocks.append(nn.Sequential(
57
+ nn.Linear(16 + conditioned_outputs, width), nn.ReLU(),
58
+ nn.Linear(width, width), nn.ReLU(),
59
+ nn.Linear(width, width), nn.ReLU(),
60
+ nn.Linear(width, variables),
61
+ ))
62
+ self.blocks = nn.ModuleList(blocks)
63
+
64
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
65
+ outputs = []
66
+ for block in self.blocks:
67
+ if not outputs:
68
+ conditioned = x
69
+ elif self.mode == "HPC":
70
+ conditioned = torch.cat((x, outputs[-1]), dim=-1)
71
+ else:
72
+ conditioned = torch.cat((x, *outputs), dim=-1)
73
+ outputs.append(block(conditioned))
74
+ return torch.stack(outputs, dim=1)
75
+
76
+
77
+ class HAC(HierarchicalNetwork):
78
+ def __init__(self, width: int = 16, levels: int = 17, variables: int = 5):
79
+ super().__init__("HAC", width, levels, variables)
80
+
81
+
82
+ class PBLEmulator(nn.Module):
83
+ def __init__(self, architecture: str = "HAC", width: int = 16, levels: int = 17, variables: int = 5):
84
+ super().__init__()
85
+ self.architecture = architecture.upper()
86
+ if self.architecture == "FFN":
87
+ self.model = FFN(width, levels, variables)
88
+ elif self.architecture == "HPC":
89
+ self.model = HierarchicalNetwork(self.architecture, width, levels, variables)
90
+ elif self.architecture == "HAC":
91
+ self.model = HAC(width, levels, variables)
92
+ else:
93
+ raise ValueError(f"Unknown architecture: {architecture}")
94
+
95
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
96
+ return self.model(x)
97
+
98
+
99
+ def build_model(architecture: str = "HAC", width: int = 16, levels: int = 17, variables: int = 5) -> PBLEmulator:
100
+ model = PBLEmulator(architecture, width, levels, variables)
101
+ count = sum(parameter.numel() for parameter in model.parameters())
102
+ if width == 16 and levels == 17 and variables == 5:
103
+ assert count == PARAMETER_COUNTS[model.architecture], f"{model.architecture}: expected {PARAMETER_COUNTS[model.architecture]}, got {count}"
104
+ return model
105
+
106
+
107
+ @dataclass
108
+ class ColumnScaler:
109
+ mean: np.ndarray
110
+ scale: np.ndarray
111
+ minimum: np.ndarray
112
+ span: np.ndarray
113
+
114
+ @classmethod
115
+ def fit(cls, values: np.ndarray) -> "ColumnScaler":
116
+ flat = np.asarray(values, dtype=np.float64).reshape(len(values), -1)
117
+ mean = flat.mean(axis=0)
118
+ scale = flat.std(axis=0)
119
+ scale[scale < 1e-12] = 1.0
120
+ standardized = (flat - mean) / scale
121
+ minimum = standardized.min(axis=0)
122
+ span = standardized.max(axis=0) - minimum
123
+ span[span < 1e-12] = 1.0
124
+ return cls(mean, scale, minimum, span)
125
+
126
+ def transform(self, values: np.ndarray) -> np.ndarray:
127
+ shape = values.shape
128
+ flat = np.asarray(values, dtype=np.float64).reshape(len(values), -1)
129
+ return (((flat - self.mean) / self.scale - self.minimum) / self.span).reshape(shape).astype(np.float32)
130
+
131
+ def inverse_transform(self, values: np.ndarray) -> np.ndarray:
132
+ shape = values.shape
133
+ flat = np.asarray(values, dtype=np.float64).reshape(len(values), -1)
134
+ return ((flat * self.span + self.minimum) * self.scale + self.mean).reshape(shape).astype(np.float32)
135
+
136
+ def state_dict(self) -> dict[str, np.ndarray]:
137
+ return {"mean": self.mean, "scale": self.scale, "minimum": self.minimum, "span": self.span}
138
+
139
+ @classmethod
140
+ def from_state_dict(cls, state: dict[str, Any]) -> "ColumnScaler":
141
+ return cls(*(np.asarray(state[key]) for key in ("mean", "scale", "minimum", "span")))
142
+
143
+
144
+ def _distributed() -> tuple[bool, int, int, int]:
145
+ world_size = int(os.environ.get("WORLD_SIZE", "1"))
146
+ rank = int(os.environ.get("RANK", "0"))
147
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
148
+ if world_size > 1 and not torch.distributed.is_initialized():
149
+ torch.distributed.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
150
+ return world_size > 1, rank, local_rank, world_size
151
+
152
+
153
+ def _reduce_loss(total: float, count: int, device: torch.device) -> float:
154
+ pair = torch.tensor([total, count], dtype=torch.float64, device=device)
155
+ if torch.distributed.is_initialized():
156
+ torch.distributed.all_reduce(pair, op=torch.distributed.ReduceOp.SUM)
157
+ return float(pair[0] / pair[1].clamp_min(1))
158
+
159
+
160
+ def _epoch(model: nn.Module, loader: DataLoader, device: torch.device, optimizer: torch.optim.Optimizer | None) -> float:
161
+ model.train(optimizer is not None)
162
+ total, count = 0.0, 0
163
+ context = torch.enable_grad() if optimizer is not None else torch.no_grad()
164
+ with context:
165
+ for x_batch, y_batch in loader:
166
+ x_batch, y_batch = x_batch.to(device), y_batch.to(device)
167
+ if optimizer is not None:
168
+ optimizer.zero_grad(set_to_none=True)
169
+ loss = torch.mean((model(x_batch) - y_batch) ** 2)
170
+ if optimizer is not None:
171
+ loss.backward()
172
+ optimizer.step()
173
+ total += float(loss.detach()) * len(x_batch)
174
+ count += len(x_batch)
175
+ return _reduce_loss(total, count, device)
176
+
177
+
178
+ def train_model(data_path: str | Path, checkpoint_path: str | Path, metrics_path: str | Path, settings: dict[str, Any]) -> dict[str, Any]:
179
+ distributed, rank, local_rank, world_size = _distributed()
180
+ seed = int(settings.get("seed", 19))
181
+ random.seed(seed + rank); np.random.seed(seed + rank); torch.manual_seed(seed + rank)
182
+ if torch.cuda.is_available():
183
+ torch.cuda.set_device(local_rank)
184
+ device = torch.device("cuda", local_rank)
185
+ else:
186
+ device = torch.device("cpu")
187
+ raw = np.load(data_path)
188
+ x_train, y_train = raw["x_train"], raw["y_train"]
189
+ x_val, y_val = raw["x_val"], raw["y_val"]
190
+ assert x_train.shape[1:] == (16,) and y_train.shape[1:] == (17, 5)
191
+ model_config = {
192
+ "architecture": settings.get("architecture", "HAC"),
193
+ "width": int(settings.get("width", 16)),
194
+ "levels": int(settings.get("levels", 17)),
195
+ "output_variables": int(settings.get("output_variables", 5)),
196
+ }
197
+ start_epoch, history, best_loss = 0, [], math.inf
198
+ checkpoint_path = Path(checkpoint_path)
199
+ saved = None
200
+ if settings.get("resume") and checkpoint_path.exists():
201
+ saved = torch.load(checkpoint_path, map_location=device, weights_only=False)
202
+ if saved.get("format_version") != CHECKPOINT_FORMAT_VERSION:
203
+ raise ValueError(f"Unsupported checkpoint format_version: {saved.get('format_version')!r}; expected {CHECKPOINT_FORMAT_VERSION!r}")
204
+ model_config = saved["model_config"]
205
+ x_scaler = ColumnScaler.from_state_dict(saved["x_scaler"])
206
+ y_scaler = ColumnScaler.from_state_dict(saved["y_scaler"])
207
+ else:
208
+ x_scaler, y_scaler = ColumnScaler.fit(x_train), ColumnScaler.fit(y_train)
209
+ x_train, y_train = x_scaler.transform(x_train), y_scaler.transform(y_train)
210
+ x_val, y_val = x_scaler.transform(x_val), y_scaler.transform(y_val)
211
+ model = build_model(model_config["architecture"], model_config["width"], model_config["levels"], model_config["output_variables"]).to(device)
212
+ optimizer = torch.optim.Adam(model.parameters(), lr=float(settings.get("learning_rate", 0.001)))
213
+ best_state = None
214
+ if saved is not None:
215
+ model.load_state_dict(saved.get("last_model", saved["model"]))
216
+ optimizer.load_state_dict(saved["optimizer_state"])
217
+ start_epoch, history, best_loss = saved["epoch"] + 1, saved["history"], saved["best_val_loss"]
218
+ best_state = {key: value.detach().cpu().clone() for key, value in saved["model"].items()}
219
+ sampler = torch.utils.data.distributed.DistributedSampler(TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train)), shuffle=True) if distributed else None
220
+ train_set = sampler.dataset if sampler else TensorDataset(torch.from_numpy(x_train), torch.from_numpy(y_train))
221
+ train_loader = DataLoader(train_set, batch_size=int(settings.get("batch_size", 64)), sampler=sampler, shuffle=sampler is None, num_workers=int(settings.get("num_workers", 0)))
222
+ val_loader = DataLoader(TensorDataset(torch.from_numpy(x_val), torch.from_numpy(y_val)), batch_size=int(settings.get("batch_size", 64)), shuffle=False)
223
+ if distributed:
224
+ model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
225
+ patience, stale = int(settings.get("early_stopping_patience", 10)), 0
226
+ for epoch in range(start_epoch, int(settings.get("epochs", 6))):
227
+ if sampler is not None:
228
+ sampler.set_epoch(epoch)
229
+ train_loss = _epoch(model, train_loader, device, optimizer)
230
+ val_loss = _epoch(model, val_loader, device, None)
231
+ history.append({"epoch": epoch, "train_mse": train_loss, "val_mse": val_loss})
232
+ if val_loss < best_loss:
233
+ best_loss, stale = val_loss, 0
234
+ best_state = {key: value.detach().cpu().clone() for key, value in (model.module if distributed else model).state_dict().items()}
235
+ else:
236
+ stale += 1
237
+ if stale >= patience:
238
+ break
239
+ if rank == 0:
240
+ checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
241
+ final_model = model.module if distributed else model
242
+ last_state = {key: value.detach().cpu().clone() for key, value in final_model.state_dict().items()}
243
+ if best_state is not None:
244
+ final_model.load_state_dict(best_state)
245
+ checkpoint = {
246
+ "format_version": CHECKPOINT_FORMAT_VERSION,
247
+ "model_config": model_config,
248
+ "model": best_state or last_state,
249
+ "last_model": last_state,
250
+ "optimizer_state": optimizer.state_dict(), "epoch": history[-1]["epoch"],
251
+ "best_val_loss": best_loss, "history": history, "settings": settings, "x_scaler": x_scaler.state_dict(), "y_scaler": y_scaler.state_dict(),
252
+ "random_state": {"python": random.getstate(), "numpy": np.random.get_state(), "torch": torch.get_rng_state()},
253
+ "world_size": world_size,
254
+ }
255
+ torch.save(checkpoint, checkpoint_path)
256
+ metrics_path = Path(metrics_path); metrics_path.parent.mkdir(parents=True, exist_ok=True)
257
+ metrics_path.write_text(json.dumps({"architecture": model_config["architecture"], "parameters": sum(p.numel() for p in final_model.parameters()), "best_val_mse_normalized": best_loss, "epochs_completed": len(history), "history": history, "world_size": world_size}, indent=2), encoding="utf-8")
258
+ if distributed:
259
+ torch.distributed.barrier(); torch.distributed.destroy_process_group()
260
+ return {"best_val_mse_normalized": best_loss, "epochs_completed": len(history)}
261
+
262
+
263
+ def run_inference(data_path: str | Path, checkpoint_path: str | Path, output_path: str | Path) -> dict[str, Any]:
264
+ data = np.load(data_path)
265
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
266
+ if checkpoint.get("format_version") != CHECKPOINT_FORMAT_VERSION:
267
+ raise ValueError(f"Unsupported checkpoint format_version: {checkpoint.get('format_version')!r}; expected {CHECKPOINT_FORMAT_VERSION!r}")
268
+ settings = checkpoint["model_config"]
269
+ model = build_model(settings["architecture"], int(settings["width"]), int(settings["levels"]), int(settings["output_variables"]))
270
+ model.load_state_dict(checkpoint["model"]); model.eval()
271
+ x_scaler, y_scaler = ColumnScaler.from_state_dict(checkpoint["x_scaler"]), ColumnScaler.from_state_dict(checkpoint["y_scaler"])
272
+ with torch.no_grad():
273
+ prediction_scaled = model(torch.from_numpy(x_scaler.transform(data["x_test"]))).numpy()
274
+ prediction = y_scaler.inverse_transform(prediction_scaled)
275
+ output_path = Path(output_path); output_path.parent.mkdir(parents=True, exist_ok=True)
276
+ np.savez_compressed(output_path, inputs=data["x_test"], targets=data["y_test"], predictions=prediction,
277
+ timestamps=data["time_test"], heights_m=data["heights_m"], pblh_m=data["pblh_test"],
278
+ input_names=np.asarray(INPUT_NAMES), output_names=np.asarray(OUTPUT_NAMES), architecture=np.asarray(settings["architecture"]))
279
+ return {"samples": len(prediction), "shape": list(prediction.shape), "architecture": settings["architecture"]}
280
+
281
+
282
+ def _pearson(a: np.ndarray, b: np.ndarray) -> float:
283
+ a, b = a.ravel(), b.ravel()
284
+ if len(a) < 2 or np.std(a) < 1e-12 or np.std(b) < 1e-12:
285
+ return 0.0
286
+ return float(np.corrcoef(a, b)[0, 1])
287
+
288
+
289
+ def _scores(target: np.ndarray, prediction: np.ndarray) -> dict[str, float]:
290
+ return {"rmse": float(np.sqrt(np.mean((prediction - target) ** 2))), "pearson": _pearson(target, prediction)}
291
+
292
+
293
+ def evaluate(predictions_path: str | Path, metrics_path: str | Path, figure_path: str | Path) -> dict[str, Any]:
294
+ data = np.load(predictions_path)
295
+ target, prediction = data["targets"], data["predictions"]
296
+ heights, pblh = data["heights_m"], data["pblh_m"]
297
+ mask = heights[None, :] <= pblh[:, None]
298
+ metrics: dict[str, Any] = {
299
+ "physical_scale": True,
300
+ "primary_protocol": "synthetic virtual-level height <= synthetic PBLH mask",
301
+ "primary": {"standardized_rmse_by_variable": {}},
302
+ "full_17_level_diagnostics": {"by_variable": {}, "by_level_and_variable": {}},
303
+ }
304
+ for index, name in enumerate(OUTPUT_NAMES):
305
+ masked_target, masked_prediction = target[:, :, index][mask], prediction[:, :, index][mask]
306
+ scale = max(float(np.std(masked_target)), 1e-12)
307
+ metrics["primary"]["standardized_rmse_by_variable"][name] = float(np.sqrt(np.mean(((masked_prediction - masked_target) / scale) ** 2)))
308
+ metrics["full_17_level_diagnostics"]["by_variable"][name] = _scores(target[:, :, index], prediction[:, :, index])
309
+ primary_values = metrics["primary"]["standardized_rmse_by_variable"].values()
310
+ metrics["primary"]["macro_mean_standardized_rmse"] = float(np.mean(list(primary_values)))
311
+ metrics["primary"]["definition"] = "Unweighted mean of per-variable RMSE divided by that variable's target standard deviation within the synthetic PBLH mask; no physical units are mixed."
312
+ for level, height in enumerate(heights):
313
+ metrics["full_17_level_diagnostics"]["by_level_and_variable"][str(level)] = {
314
+ "height_m": float(height),
315
+ "by_variable": {name: _scores(target[:, level, index], prediction[:, level, index]) for index, name in enumerate(OUTPUT_NAMES)},
316
+ }
317
+ speed_true = np.hypot(target[:, :, 0], target[:, :, 1]); speed_pred = np.hypot(prediction[:, :, 0], prediction[:, :, 1])
318
+ direction_true = np.mod(1.5 * np.pi - np.arctan2(target[:, :, 1], target[:, :, 0]), 2 * np.pi)
319
+ direction_pred = np.mod(1.5 * np.pi - np.arctan2(prediction[:, :, 1], prediction[:, :, 0]), 2 * np.pi)
320
+ delta = np.arctan2(np.sin(direction_pred - direction_true), np.cos(direction_pred - direction_true))
321
+ metrics["wind_speed"] = _scores(speed_true, speed_pred)
322
+ metrics["wind_direction"] = {"convention": "meteorological direction from: 0 degrees from north, increasing clockwise", "circular_rmse_degrees": float(np.degrees(np.sqrt(np.mean(delta ** 2)))), "mean_absolute_circular_error_degrees": float(np.degrees(np.mean(np.abs(delta)))), "circular_correlation_cosine": float(np.mean(np.cos(delta)))}
323
+ metrics["synthetic_pblh_mask"] = {"synthetic": True, "definition": "virtual level height <= synthetic PBLH; not a paper or real-WRF PBL mask", "sample_level_pairs": int(mask.sum())}
324
+ metrics_path = Path(metrics_path); metrics_path.parent.mkdir(parents=True, exist_ok=True)
325
+ metrics_path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
326
+ try:
327
+ import matplotlib.pyplot as plt
328
+ figure_path = Path(figure_path); figure_path.parent.mkdir(parents=True, exist_ok=True)
329
+ fig, axes = plt.subplots(1, 5, figsize=(15, 4), sharey=True)
330
+ for index, (axis, name) in enumerate(zip(axes, OUTPUT_NAMES)):
331
+ axis.plot(target[:, :, index].mean(0), heights, label="target")
332
+ axis.plot(prediction[:, :, index].mean(0), heights, "--", label="prediction")
333
+ axis.set_title(name); axis.grid(alpha=0.25)
334
+ axes[0].set_ylabel("synthetic height (m)"); axes[-1].legend()
335
+ fig.tight_layout(); fig.savefig(figure_path, dpi=150); plt.close(fig)
336
+ except ImportError:
337
+ metrics["figure_note"] = "matplotlib unavailable; numerical evaluation completed"
338
+ return metrics
scripts/fake_data.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ import numpy as np
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+ sys.path.insert(0, str(ROOT))
8
+
9
+ from model.pbl_emulator import INPUT_NAMES, OUTPUT_NAMES, load_yaml
10
+
11
+
12
+ def main() -> None:
13
+ config = load_yaml(ROOT / "conf/config.yaml")
14
+ count, seed = int(config["data"]["samples"]), int(config["data"]["seed"])
15
+ interval_hours = int(config["data"]["interval_hours"])
16
+ rng = np.random.default_rng(seed)
17
+ hours = (np.arange(count) * interval_hours) % 24
18
+ days = (np.arange(count) * interval_hours / 24) % 365
19
+ daylight = np.maximum(0.0, np.sin(np.pi * (hours - 6) / 12))
20
+ season = np.sin(2 * np.pi * (days - 172) / 365)
21
+ swdown = (760 + 110 * season) * daylight
22
+ t2 = 284 + 10 * season + 5 * np.sin(2 * np.pi * (hours - 14) / 24) + rng.normal(0, 0.8, count)
23
+ q2 = np.clip(0.0065 + 0.004 * (season + 1) / 2 + 0.0012 * daylight + rng.normal(0, 0.0003, count), 0.001, None)
24
+ ug = 5.5 + 1.8 * np.sin(2 * np.pi * days / 9) + rng.normal(0, 0.5, count)
25
+ vg = 2.0 + 1.5 * np.cos(2 * np.pi * days / 13) + rng.normal(0, 0.5, count)
26
+ u10 = 0.62 * ug + rng.normal(0, 0.45, count); v10 = 0.62 * vg + rng.normal(0, 0.45, count)
27
+ hfx = 12 + 150 * daylight * (0.75 + 0.25 * season) + rng.normal(0, 5, count)
28
+ lh = 25 + 110 * daylight * (0.8 + 0.2 * season) + rng.normal(0, 5, count)
29
+ pblh = np.clip(170 + 1200 * daylight * (0.75 + 0.25 * season) + rng.normal(0, 45, count), 80, 1800)
30
+ ust = np.clip(0.12 + 0.0014 * hfx + 0.022 * np.hypot(u10, v10), 0.05, 1.2)
31
+ tsk = t2 + 1.5 + 4.0 * daylight; tslb = 283 + 7.0 * season
32
+ smois = np.clip(0.25 + 0.04 * np.sin(2 * np.pi * days / 40) - 0.025 * season + rng.normal(0, 0.008, count), 0.08, 0.45)
33
+ grdflx = 0.12 * hfx - 8 * (1 - daylight); glw = 285 + 2.2 * (t2 - 273.15) + 450 * q2
34
+ x = np.column_stack((q2, t2, u10, v10, grdflx, swdown, glw, lh, hfx, pblh, ust, tsk, tslb, smois, ug, vg)).astype(np.float32)
35
+ heights = np.array([10, 35, 70, 110, 160, 220, 290, 370, 460, 560, 680, 820, 980, 1160, 1370, 1600, 1880], dtype=np.float32)
36
+ z = heights[None, :]; mix = np.exp(-z / pblh[:, None])
37
+ u = ug[:, None] + (u10 - ug)[:, None] * mix + 0.18 * np.sin(z / 260)
38
+ v = vg[:, None] + (v10 - vg)[:, None] * mix + 0.14 * np.cos(z / 300)
39
+ w = 0.20 * daylight[:, None] * np.sin(np.pi * np.minimum(z / pblh[:, None], 1)) - 0.015 * (1 - daylight[:, None]) + rng.normal(0, 0.008, (count, 17))
40
+ tk = t2[:, None] - 0.0062 * z + 0.9 * mix * daylight[:, None]
41
+ qv = np.clip(q2[:, None] * np.exp(-z / 2100) * (0.88 + 0.12 * mix) + 1e-4 * smois[:, None], 1e-5, None)
42
+ y = np.stack((u, v, w, tk, qv), axis=-1).astype(np.float32)
43
+ y += rng.normal(size=y.shape).astype(np.float32) * np.array([0.035, 0.035, 0.003, 0.04, 0.000015], dtype=np.float32)
44
+ n_train, n_val = int(0.70 * count), int(0.15 * count)
45
+ slices = {"train": slice(0, n_train), "val": slice(n_train, n_train + n_val), "test": slice(n_train + n_val, count)}
46
+ output = ROOT / config["paths"]["data"]; output.parent.mkdir(parents=True, exist_ok=True)
47
+ payload = {"heights_m": heights, "input_names": np.asarray(INPUT_NAMES), "output_names": np.asarray(OUTPUT_NAMES)}
48
+ for name, section in slices.items():
49
+ payload.update({f"x_{name}": x[section], f"y_{name}": y[section], f"time_{name}": (np.arange(count)[section] * interval_hours).astype(np.int64), f"pblh_{name}": pblh[section].astype(np.float32)})
50
+ np.savez_compressed(output, **payload)
51
+ print(f"saved {output}: x={x.shape}, y={y.shape}, split={[len(range(*s.indices(count))) for s in slices.values()]}")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ sys.path.insert(0, str(ROOT))
6
+
7
+ from model.pbl_emulator import load_yaml, run_inference
8
+
9
+ if __name__ == "__main__":
10
+ config = load_yaml(ROOT / "conf/config.yaml")
11
+ summary = run_inference(ROOT / config["paths"]["data"], ROOT / config["paths"]["checkpoint"], ROOT / config["paths"]["predictions"])
12
+ print(summary)
scripts/result.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ sys.path.insert(0, str(ROOT))
6
+
7
+ from model.pbl_emulator import evaluate, load_yaml
8
+
9
+ if __name__ == "__main__":
10
+ config = load_yaml(ROOT / "conf/config.yaml")
11
+ summary = evaluate(ROOT / config["paths"]["predictions"], ROOT / config["paths"]["evaluation_metrics"], ROOT / config["paths"]["figure"])
12
+ print({"primary": summary["primary"], "wind_direction": summary["wind_direction"]})
scripts/train.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ ROOT = Path(__file__).resolve().parents[1]
5
+ sys.path.insert(0, str(ROOT))
6
+
7
+ from model.pbl_emulator import load_yaml, train_model
8
+
9
+ if __name__ == "__main__":
10
+ config = load_yaml(ROOT / "conf/config.yaml")
11
+ settings = {**config["model"], **config["training"], "format_version": config["project"]["format_version"]}
12
+ result = train_model(ROOT / config["paths"]["data"], ROOT / config["paths"]["checkpoint"], ROOT / config["paths"]["training_metrics"], settings)
13
+ print(result)
weight/.gitkeep ADDED
File without changes