Publish PBL-Emulator engineering reproduction
Browse files- .gitattributes +1 -34
- README.md +152 -0
- README_zh.md +153 -0
- conf/config.yaml +53 -0
- config.json +43 -0
- configuration.json +12 -0
- model/pbl_emulator.py +338 -0
- scripts/fake_data.py +55 -0
- scripts/inference.py +12 -0
- scripts/result.py +12 -0
- scripts/train.py +13 -0
- weight/.gitkeep +0 -0
.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 |
-
*.
|
| 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
|