Upload folder using huggingface_hub
Browse files- .gitattributes +2 -34
- README.md +143 -0
- conf/config.yaml +33 -0
- config.json +42 -0
- configuration.json +17 -0
- model/stablenn_phys.py +51 -0
- scripts/fake_data.py +70 -0
- scripts/inference.py +57 -0
- scripts/result.py +67 -0
- scripts/train.py +95 -0
- weight/.gitkeep +0 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 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 |
-
*.
|
| 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
|
| 3 |
+
*.png binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- OneScience
|
| 7 |
+
- Earth Science
|
| 8 |
+
- Physics Parameterization
|
| 9 |
+
frameworks: PyTorch
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
<p align="center">
|
| 13 |
+
<strong><span style="font-size: 30px;">StableNN-Phys</span></strong>
|
| 14 |
+
</p>
|
| 15 |
+
|
| 16 |
+
# Model Introduction
|
| 17 |
+
|
| 18 |
+
StableNN-Phys is an engineering reproduction of a neural-network unified physics parameterization for an atmospheric single-column model, continuously predicting thermodynamic-state evolution at three-hour intervals from column states and surface fluxes.
|
| 19 |
+
|
| 20 |
+
Paper: Prognostic Validation of a Neural Network Unified Physics Parameterization
|
| 21 |
+
https://doi.org/10.1029/2018GL078510
|
| 22 |
+
|
| 23 |
+
# Model Description
|
| 24 |
+
|
| 25 |
+
The method reproduced by StableNN-Phys was proposed by researchers from the Department of Atmospheric Sciences at the University of Washington. The model takes 34-level liquid-water static energy and total-water states with surface sensible heat, latent heat, and incoming solar radiation, maps 71 input features to 68 physics tendencies at each step, and integrates multiple steps with advective forcing and no teacher forcing. The model is suitable for neural physics-parameterization training, three-hourly single-column prediction, 64-step long-term stability validation, and column-water-budget diagnosis.
|
| 26 |
+
|
| 27 |
+
# Use Cases
|
| 28 |
+
|
| 29 |
+
| Use Case | Description |
|
| 30 |
+
| :---: | :--- |
|
| 31 |
+
| Single-column prediction | Continuously predict three-hourly state evolution from 34-level thermodynamic states and surface fluxes. |
|
| 32 |
+
| Physics-parameterization training | Learn the mapping from 71 input features to 68 physics tendencies with `T=20` multistep windows. |
|
| 33 |
+
| Long-term stability validation | Run a fixed 64-step, eight-day single-column integration without teacher forcing. |
|
| 34 |
+
| Water-budget evaluation | Diagnose precipitation from column-water storage, latent heat flux, and advective moisture convergence. |
|
| 35 |
+
| Hugging Face/OneCode execution | Validate training, inference, evaluation, visualization, and checkpoint workflows in Hugging Face or OneCode environments. |
|
| 36 |
+
|
| 37 |
+
# Usage Instructions
|
| 38 |
+
|
| 39 |
+
## 1.OneCode
|
| 40 |
+
|
| 41 |
+
Experience intelligent, one-click AI4S programming through the OneCode online environment:
|
| 42 |
+
|
| 43 |
+
[Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 44 |
+
|
| 45 |
+
## 2. Download and Installation
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
hf download OneScience-Group/StableNN-Phys --local-dir ./StableNN-Phys
|
| 49 |
+
cd StableNN-Phys
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### Environment Dependencies
|
| 53 |
+
|
| 54 |
+
**Hardware Requirements**
|
| 55 |
+
|
| 56 |
+
- A GPU or DCU is recommended.
|
| 57 |
+
- A CPU can be used for connectivity validation with the default small-sample configuration.
|
| 58 |
+
- DCU users must install DTK first. DTK 25.04.2 or later, or the OneScience-recommended version matching the cluster, is recommended.
|
| 59 |
+
|
| 60 |
+
**DCU Environment**
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
# Activate DTK and Conda first
|
| 64 |
+
conda create -n onescience311 python=3.11 -y
|
| 65 |
+
conda activate onescience311
|
| 66 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**GPU Environment**
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
# Activate Conda first
|
| 73 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 74 |
+
conda activate onescience311
|
| 75 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Training Data
|
| 79 |
+
|
| 80 |
+
Training data consist of atmospheric-column thermodynamic states and external physical forcing. Inputs contain 34-level static energy, 34-level total water, surface fluxes, and insolation for 71 values. Targets are 34-level temperature and moisture physics tendencies for the same column, giving 68 values. Samples form continuous `T=20` windows at three-hour intervals. This repository uses a small amount of synthetic data to validate training, inference, and evaluation and does not represent the paper data distribution, training scale, or formal performance.
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
python scripts/fake_data.py
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Training
|
| 87 |
+
|
| 88 |
+
For single-device training, use:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
python scripts/train.py
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
For multi-GPU training, use:
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
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
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Training uses Adam to fit `T=20` multistep state sequences, with layer-mass-weighted MAD as the default `paper` loss. The recorded paper configuration uses a learning rate of 0.01, batch size 200, five epochs, and hidden width 128, while the default engineering configuration reduces the hidden width and sample count for quick workflow validation; results are saved to:
|
| 101 |
+
|
| 102 |
+
```text
|
| 103 |
+
result/checkpoints/stablenn_phys.pt
|
| 104 |
+
result/training/metrics.json
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Trained Weights
|
| 108 |
+
|
| 109 |
+
This repository does not include pretrained weights under `weight/`. Checkpoints generated with synthetic data validate the engineering workflow only and are not official paper weights.
|
| 110 |
+
|
| 111 |
+
### Inference
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
python scripts/inference.py
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Inference performs a fixed 64-step, eight-day single-column rollout without teacher forcing and saves the complete state, tendency, and forcing sequence at every three-hour time. Complete numerical results are saved to `result/output/rollout.npz`.
|
| 118 |
+
|
| 119 |
+
### Evaluation and Visualization
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
python scripts/result.py
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
Evaluation reports length-65 per-lead mass-weighted MAD and bias and computes R2 by data source. Precipitation is diagnosed from column-water storage, latent heat flux, and advective moisture convergence while the budget residual is checked; synthetic-data results validate the engineering workflow only and do not represent formal paper performance. Structured results and the figure are saved to:
|
| 126 |
+
|
| 127 |
+
```text
|
| 128 |
+
result/evaluation/metrics.json
|
| 129 |
+
result/evaluation/state_precipitation_timeseries.png
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
# Official OneScience Information
|
| 133 |
+
|
| 134 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 135 |
+
| --- | --- | --- |
|
| 136 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 137 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 138 |
+
|
| 139 |
+
# Citation and License
|
| 140 |
+
|
| 141 |
+
This repository is an independent engineering reproduction of the public StableNN-Phys specifications.
|
| 142 |
+
|
| 143 |
+
Use of this repository's code, official model weights, and data remains subject to the licenses and terms of their respective projects.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
data:
|
| 3 |
+
file: data/stablenn_phys_virtual.npz
|
| 4 |
+
format_version: stablenn_phys_v1
|
| 5 |
+
levels: 34
|
| 6 |
+
window_steps: 20
|
| 7 |
+
columns: 6
|
| 8 |
+
windows_per_column: 4
|
| 9 |
+
step_hours: 3
|
| 10 |
+
model:
|
| 11 |
+
name: StableNN-Phys
|
| 12 |
+
hidden_size: 32
|
| 13 |
+
paper_hidden_size: 128
|
| 14 |
+
input_features: 71
|
| 15 |
+
output_features: 68
|
| 16 |
+
train:
|
| 17 |
+
epochs: 3
|
| 18 |
+
batch_size: 4
|
| 19 |
+
learning_rate: 0.003
|
| 20 |
+
loss_mode: paper
|
| 21 |
+
paper_model:
|
| 22 |
+
hidden_size: 128
|
| 23 |
+
learning_rate: 0.01
|
| 24 |
+
batch_size: 200
|
| 25 |
+
epochs: 5
|
| 26 |
+
runtime:
|
| 27 |
+
device: auto
|
| 28 |
+
rollout_steps: 64
|
| 29 |
+
paths:
|
| 30 |
+
checkpoint: result/checkpoints/stablenn_phys.pt
|
| 31 |
+
training_metrics: result/training/metrics.json
|
| 32 |
+
inference: result/output/rollout.npz
|
| 33 |
+
evaluation: result/evaluation/metrics.json
|
config.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "StableNN-Phys",
|
| 3 |
+
"model_type": "stablenn_phys",
|
| 4 |
+
"architectures": ["StableNNPhys"],
|
| 5 |
+
"framework": "PyTorch",
|
| 6 |
+
"domain": "atmospheric physics",
|
| 7 |
+
"task": "single-column prognostic rollout",
|
| 8 |
+
"implementation": {
|
| 9 |
+
"entry_point": "model/stablenn_phys.py",
|
| 10 |
+
"scope": "Core-method, full-window and full-vertical-dimension reduced-sample engineering reproduction",
|
| 11 |
+
"train_script": "scripts/train.py",
|
| 12 |
+
"inference_script": "scripts/inference.py",
|
| 13 |
+
"evaluation_script": "scripts/result.py",
|
| 14 |
+
"synthetic_data_script": "scripts/fake_data.py"
|
| 15 |
+
},
|
| 16 |
+
"architecture": {
|
| 17 |
+
"levels": 34,
|
| 18 |
+
"input_features": 71,
|
| 19 |
+
"output_features": 68,
|
| 20 |
+
"engineering_hidden_size": 32,
|
| 21 |
+
"paper_hidden_size": 128,
|
| 22 |
+
"activation": "ReLU",
|
| 23 |
+
"linear_bypass": true
|
| 24 |
+
},
|
| 25 |
+
"integration": {
|
| 26 |
+
"step_hours": 3,
|
| 27 |
+
"training_window_steps": 20,
|
| 28 |
+
"rollout_steps": 64,
|
| 29 |
+
"forcing": "trapezoidal horizontal-advection update before neural-network Euler physics update",
|
| 30 |
+
"teacher_forcing": false
|
| 31 |
+
},
|
| 32 |
+
"checkpoint": {
|
| 33 |
+
"path": "result/checkpoints/stablenn_phys.pt",
|
| 34 |
+
"required_fields": ["model", "model_config", "format_version"],
|
| 35 |
+
"format_version": "stablenn_phys_checkpoint_v1"
|
| 36 |
+
},
|
| 37 |
+
"loss_modes": {
|
| 38 |
+
"default": "paper",
|
| 39 |
+
"paper": "layer-mass-weighted MAD of prognostic state errors over all rollout steps",
|
| 40 |
+
"official_v0_3": "equal-level normalized MAD of prognostic state errors over all rollout steps"
|
| 41 |
+
}
|
| 42 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "PyTorch",
|
| 3 |
+
"task": "atmospheric_single_column_rollout",
|
| 4 |
+
"model": "StableNN-Phys",
|
| 5 |
+
"config": "config.json",
|
| 6 |
+
"input_format": "sL [B,34] + qT [B,34] + SHF/LHF/SOLIN [B,3] = [B,71]",
|
| 7 |
+
"output_format": "sL and qT tendencies [B,68]",
|
| 8 |
+
"training_window_steps": 20,
|
| 9 |
+
"rollout_steps": 64,
|
| 10 |
+
"per_step_length": 65,
|
| 11 |
+
"step_hours": 3,
|
| 12 |
+
"default_config": "conf/config.yaml",
|
| 13 |
+
"training": "scripts/train.py",
|
| 14 |
+
"inference": "scripts/inference.py",
|
| 15 |
+
"evaluation": "scripts/result.py",
|
| 16 |
+
"visualization": "scripts/result.py"
|
| 17 |
+
}
|
model/stablenn_phys.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class StableNNPhys(nn.Module):
|
| 6 |
+
def __init__(self, hidden_size=32, input_size=71, output_size=68):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.hidden_size = hidden_size
|
| 9 |
+
self.input_size = input_size
|
| 10 |
+
self.output_size = output_size
|
| 11 |
+
self.hidden = nn.Linear(input_size, hidden_size)
|
| 12 |
+
self.output = nn.Linear(hidden_size, output_size)
|
| 13 |
+
self.bypass = nn.Linear(input_size, output_size)
|
| 14 |
+
nn.init.zeros_(self.output.weight)
|
| 15 |
+
nn.init.zeros_(self.output.bias)
|
| 16 |
+
nn.init.zeros_(self.bypass.weight)
|
| 17 |
+
nn.init.zeros_(self.bypass.bias)
|
| 18 |
+
|
| 19 |
+
def forward(self, x):
|
| 20 |
+
return self.output(torch.relu(self.hidden(x))) + self.bypass(x)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def rollout(model, initial_state, surface, advection, state_mean, state_std,
|
| 24 |
+
tendency_mean, tendency_std, dt_seconds=10800.0):
|
| 25 |
+
"""Integrate advection trapezoidally, then neural physics with Euler."""
|
| 26 |
+
states = [initial_state]
|
| 27 |
+
physics = []
|
| 28 |
+
state = initial_state
|
| 29 |
+
for step in range(surface.shape[1]):
|
| 30 |
+
adv_now = advection[:, step]
|
| 31 |
+
adv_next = advection[:, min(step + 1, advection.shape[1] - 1)]
|
| 32 |
+
forced = state + 0.5 * dt_seconds * (adv_now + adv_next)
|
| 33 |
+
surface_scaled = surface[:, step] / surface.new_tensor([100.0, 100.0, 1000.0])
|
| 34 |
+
features = torch.cat(((forced - state_mean) / state_std, surface_scaled), dim=-1)
|
| 35 |
+
tendency = model(features) * tendency_std + tendency_mean
|
| 36 |
+
state = forced + dt_seconds * tendency
|
| 37 |
+
physics.append(tendency)
|
| 38 |
+
states.append(state)
|
| 39 |
+
return torch.stack(states, dim=1), torch.stack(physics, dim=1)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def rollout_loss(prediction, target, layer_mass, mode="paper"):
|
| 43 |
+
error = torch.abs(prediction[:, 1:] - target[:, 1:])
|
| 44 |
+
if mode == "paper":
|
| 45 |
+
weights = torch.cat((layer_mass, layer_mass), dim=-1)
|
| 46 |
+
weights = weights / weights.mean(dim=-1, keepdim=True)
|
| 47 |
+
return (error * weights[:, None]).mean()
|
| 48 |
+
if mode == "official_v0_3":
|
| 49 |
+
scale = target[:, 1:].std(dim=(0, 1), unbiased=False).clamp_min(1e-6)
|
| 50 |
+
return (error / scale).mean()
|
| 51 |
+
raise ValueError(f"Unknown loss mode: {mode}")
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import math
|
| 3 |
+
import yaml
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
cfg = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 12 |
+
rng = np.random.default_rng(cfg["seed"])
|
| 13 |
+
ncol = cfg["data"]["columns"]
|
| 14 |
+
nwin = cfg["data"]["windows_per_column"]
|
| 15 |
+
steps = cfg["data"]["window_steps"]
|
| 16 |
+
levels = cfg["data"]["levels"]
|
| 17 |
+
dt = cfg["data"]["step_hours"] * 3600.0
|
| 18 |
+
total_steps = nwin * steps + 64
|
| 19 |
+
sigma = np.linspace(0.02, 0.995, levels, dtype=np.float32)
|
| 20 |
+
pressure = 100000.0 * sigma
|
| 21 |
+
interfaces = np.linspace(0.0, 100000.0, levels + 1, dtype=np.float32)
|
| 22 |
+
layer_mass = np.diff(interfaces) / 9.80665
|
| 23 |
+
time = np.arange(total_steps + 1, dtype=np.float32)
|
| 24 |
+
states = np.empty((ncol, total_steps + 1, 68), dtype=np.float32)
|
| 25 |
+
surface = np.empty((ncol, total_steps, 3), dtype=np.float32)
|
| 26 |
+
advection = np.empty((ncol, total_steps + 1, 68), dtype=np.float32)
|
| 27 |
+
for c in range(ncol):
|
| 28 |
+
phase = 2.0 * math.pi * c / ncol
|
| 29 |
+
sl0 = 2.82e5 + 1.25e5 * (1.0 - sigma) + 1400.0 * np.sin(math.pi * sigma + phase)
|
| 30 |
+
qt0 = 0.0175 * sigma ** 2.6 + 0.00012 + 0.0004 * np.sin(2 * math.pi * sigma + phase)
|
| 31 |
+
states[c, 0] = np.concatenate((sl0, np.maximum(qt0, 2e-5)))
|
| 32 |
+
solar = np.maximum(0.0, np.sin(2.0 * math.pi * (time[:-1] % 8.0) / 8.0 + phase))
|
| 33 |
+
surface[c, :, 0] = 30.0 + 18.0 * np.sin(2 * math.pi * time[:-1] / 8.0 + phase)
|
| 34 |
+
surface[c, :, 1] = 92.0 + 34.0 * np.sin(2 * math.pi * time[:-1] / 8.0 + phase - 0.5)
|
| 35 |
+
surface[c, :, 2] = 760.0 * solar
|
| 36 |
+
vertical = np.sin(math.pi * sigma)
|
| 37 |
+
for t in range(total_steps + 1):
|
| 38 |
+
wave = math.sin(2 * math.pi * t / 16.0 + phase)
|
| 39 |
+
advection[c, t, :levels] = (0.010 * wave * vertical + rng.normal(0, 0.0003, levels))
|
| 40 |
+
advection[c, t, levels:] = (1.4e-9 * wave * vertical + rng.normal(0, 4e-11, levels))
|
| 41 |
+
for t in range(total_steps):
|
| 42 |
+
adv = 0.5 * (advection[c, t] + advection[c, t + 1])
|
| 43 |
+
sl, qt = np.split(states[c, t], 2)
|
| 44 |
+
shf, lhf, solin = surface[c, t]
|
| 45 |
+
heat = 2.0e-7 * (300000.0 - sl) + 1.0e-5 * shf * sigma + 1.0e-7 * solin * vertical
|
| 46 |
+
moist = 1.4e-7 * (0.012 * sigma ** 2.4 - qt) + 2.5e-11 * lhf * sigma
|
| 47 |
+
next_state = states[c, t] + dt * (adv + np.concatenate((heat, moist)))
|
| 48 |
+
next_state[levels:] = np.maximum(next_state[levels:], 1e-6)
|
| 49 |
+
states[c, t + 1] = next_state
|
| 50 |
+
initial, targets, surf_windows, adv_windows, source = [], [], [], [], []
|
| 51 |
+
for c in range(ncol):
|
| 52 |
+
for w in range(nwin):
|
| 53 |
+
start = w * steps
|
| 54 |
+
initial.append(states[c, start])
|
| 55 |
+
targets.append(states[c, start:start + steps + 1])
|
| 56 |
+
surf_windows.append(surface[c, start:start + steps])
|
| 57 |
+
adv_windows.append(advection[c, start:start + steps + 1])
|
| 58 |
+
source.append(c)
|
| 59 |
+
out = ROOT / cfg["data"]["file"]
|
| 60 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 61 |
+
np.savez_compressed(out, initial=np.array(initial), target=np.array(targets),
|
| 62 |
+
surface=np.array(surf_windows), advection=np.array(adv_windows),
|
| 63 |
+
layer_mass=layer_mass, pressure=pressure, source=np.array(source),
|
| 64 |
+
long_state=states, long_surface=surface, long_advection=advection,
|
| 65 |
+
dt_seconds=np.float32(dt), format_version=cfg["data"]["format_version"])
|
| 66 |
+
print(f"saved {out}: windows={len(initial)}, T={steps}, levels={levels}")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
import yaml
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 10 |
+
if str(ROOT) not in sys.path:
|
| 11 |
+
sys.path.insert(0, str(ROOT))
|
| 12 |
+
|
| 13 |
+
from model.stablenn_phys import StableNNPhys, rollout
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
CHECKPOINT_FORMAT_VERSION = "stablenn_phys_checkpoint_v1"
|
| 17 |
+
REQUIRED_CHECKPOINT_FIELDS = {"model", "model_config", "format_version"}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
cfg = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 22 |
+
raw = np.load(ROOT / cfg["data"]["file"])
|
| 23 |
+
checkpoint = torch.load(ROOT / cfg["paths"]["checkpoint"], map_location="cpu", weights_only=False)
|
| 24 |
+
missing = REQUIRED_CHECKPOINT_FIELDS.difference(checkpoint)
|
| 25 |
+
if missing:
|
| 26 |
+
raise ValueError(f"Checkpoint is missing required fields: {', '.join(sorted(missing))}")
|
| 27 |
+
if checkpoint["format_version"] != CHECKPOINT_FORMAT_VERSION:
|
| 28 |
+
raise ValueError(
|
| 29 |
+
f"Unsupported checkpoint format_version {checkpoint['format_version']!r}; "
|
| 30 |
+
f"expected {CHECKPOINT_FORMAT_VERSION!r}"
|
| 31 |
+
)
|
| 32 |
+
model = StableNNPhys(checkpoint["model_config"]["hidden_size"])
|
| 33 |
+
model.load_state_dict(checkpoint["model"])
|
| 34 |
+
model.eval()
|
| 35 |
+
steps = cfg["runtime"]["rollout_steps"]
|
| 36 |
+
if steps != 64:
|
| 37 |
+
raise ValueError(f"runtime.rollout_steps must be 64, got {steps}")
|
| 38 |
+
initial = torch.from_numpy(raw["long_state"][:, 0].astype(np.float32))
|
| 39 |
+
surface = torch.from_numpy(raw["long_surface"][:, :steps].astype(np.float32))
|
| 40 |
+
advection = torch.from_numpy(raw["long_advection"][:, :steps + 1].astype(np.float32))
|
| 41 |
+
norm = checkpoint["normalization"]
|
| 42 |
+
with torch.no_grad():
|
| 43 |
+
prediction, tendency = rollout(model, initial, surface, advection, norm["state_mean"], norm["state_std"],
|
| 44 |
+
norm["tendency_mean"], norm["tendency_std"], checkpoint["dt_seconds"])
|
| 45 |
+
target = raw["long_state"][:, :steps + 1]
|
| 46 |
+
out = ROOT / cfg["paths"]["inference"]
|
| 47 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
np.savez_compressed(out, prediction=prediction.numpy(), target=target, tendency=tendency.numpy(),
|
| 49 |
+
surface=surface.numpy(), advection=advection.numpy(), layer_mass=raw["layer_mass"],
|
| 50 |
+
source=np.arange(initial.shape[0]),
|
| 51 |
+
lead_hours=np.arange(steps + 1) * cfg["data"]["step_hours"],
|
| 52 |
+
dt_seconds=np.float32(checkpoint["dt_seconds"]))
|
| 53 |
+
print(f"saved {out}: {steps} steps, {steps * 3 / 24:.1f} days")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import matplotlib
|
| 5 |
+
matplotlib.use("Agg")
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import numpy as np
|
| 8 |
+
import yaml
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
cfg = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 16 |
+
data = np.load(ROOT / cfg["paths"]["inference"])
|
| 17 |
+
pred, target = data["prediction"], data["target"]
|
| 18 |
+
if pred.shape != target.shape or pred.shape[1:] != (65, 68):
|
| 19 |
+
raise ValueError(f"Expected prediction and target shaped [N,65,68], got {pred.shape} and {target.shape}")
|
| 20 |
+
mass = np.concatenate((data["layer_mass"], data["layer_mass"]))
|
| 21 |
+
mass = mass / mass.mean()
|
| 22 |
+
error = pred - target
|
| 23 |
+
mad = (np.abs(error) * mass[None, None]).mean(axis=(0, 2))
|
| 24 |
+
bias = error.mean(axis=(0, 2))
|
| 25 |
+
flat_target = target[:, 1:].astype(np.float64).reshape(target.shape[0], -1)
|
| 26 |
+
flat_pred = pred[:, 1:].astype(np.float64).reshape(pred.shape[0], -1)
|
| 27 |
+
source_r2 = []
|
| 28 |
+
for truth, estimate in zip(flat_target, flat_pred):
|
| 29 |
+
denom = np.sum((truth - truth.mean()) ** 2)
|
| 30 |
+
source_r2.append(float(1.0 - np.sum((truth - estimate) ** 2) / max(denom, 1e-12)))
|
| 31 |
+
dt = float(data["dt_seconds"])
|
| 32 |
+
qt_pred, qt_target = pred[..., 34:], target[..., 34:]
|
| 33 |
+
column_water_pred = (qt_pred * data["layer_mass"][None, None]).sum(-1)
|
| 34 |
+
column_water_target = (qt_target * data["layer_mass"][None, None]).sum(-1)
|
| 35 |
+
lhf_evap = data["surface"][:, :, 1] / 2.5e6
|
| 36 |
+
adv_q = (0.5 * (data["advection"][:, :-1, 34:] + data["advection"][:, 1:, 34:]) *
|
| 37 |
+
data["layer_mass"][None, None]).sum(-1)
|
| 38 |
+
storage = np.diff(column_water_pred, axis=1) / dt
|
| 39 |
+
precipitation = np.maximum(0.0, lhf_evap + adv_q - storage)
|
| 40 |
+
residual = storage + precipitation - lhf_evap - adv_q
|
| 41 |
+
metrics = {"mass_weighted_mad_per_step": mad.tolist(), "bias_per_step": bias.tolist(),
|
| 42 |
+
"source_r2": source_r2, "mean_source_r2": float(np.mean(source_r2)),
|
| 43 |
+
"water_budget": {"mean_precipitation_kg_m2_s": float(precipitation.mean()),
|
| 44 |
+
"mean_abs_residual_kg_m2_s": float(np.abs(residual).mean())},
|
| 45 |
+
"per_step_length": int(len(mad)), "step_hours": cfg["data"]["step_hours"]}
|
| 46 |
+
out = ROOT / cfg["paths"]["evaluation"]
|
| 47 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
out.write_text(json.dumps(metrics, indent=2))
|
| 49 |
+
hours = data["lead_hours"]
|
| 50 |
+
fig, axes = plt.subplots(2, 1, figsize=(9, 7), constrained_layout=True)
|
| 51 |
+
axes[0].plot(hours, target[:, :, :34].mean((0, 2)), label="target sL")
|
| 52 |
+
axes[0].plot(hours, pred[:, :, :34].mean((0, 2)), "--", label="predicted sL")
|
| 53 |
+
ax2 = axes[0].twinx()
|
| 54 |
+
ax2.plot(hours, target[:, :, 34:].mean((0, 2)), color="tab:green", label="target qT")
|
| 55 |
+
ax2.plot(hours, pred[:, :, 34:].mean((0, 2)), "--", color="tab:red", label="predicted qT")
|
| 56 |
+
axes[0].set(xlabel="lead time (h)", title="SCM state rollout")
|
| 57 |
+
axes[0].legend(loc="upper left")
|
| 58 |
+
ax2.legend(loc="upper right")
|
| 59 |
+
axes[1].plot(hours[1:], precipitation.mean(0) * 86400.0, color="navy")
|
| 60 |
+
axes[1].set(xlabel="lead time (h)", ylabel="mm day-1", title="Diagnosed precipitation")
|
| 61 |
+
fig.savefig(out.parent / "state_precipitation_timeseries.png", dpi=150)
|
| 62 |
+
plt.close(fig)
|
| 63 |
+
print(f"saved {out}: per_step={len(mad)}, source_R2={metrics['mean_source_r2']:.4f}")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 10 |
+
from torch.utils.data import DataLoader, DistributedSampler, TensorDataset
|
| 11 |
+
import yaml
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 15 |
+
if str(ROOT) not in sys.path:
|
| 16 |
+
sys.path.insert(0, str(ROOT))
|
| 17 |
+
|
| 18 |
+
from model.stablenn_phys import StableNNPhys, rollout, rollout_loss
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
CHECKPOINT_FORMAT_VERSION = "stablenn_phys_checkpoint_v1"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
cfg = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
|
| 26 |
+
torch.manual_seed(cfg["seed"])
|
| 27 |
+
distributed = int(os.environ.get("WORLD_SIZE", "1")) > 1
|
| 28 |
+
world_size = int(os.environ.get("WORLD_SIZE", "1"))
|
| 29 |
+
use_accelerator = torch.cuda.is_available() and torch.cuda.device_count() >= world_size
|
| 30 |
+
if distributed:
|
| 31 |
+
dist.init_process_group("nccl" if use_accelerator else "gloo")
|
| 32 |
+
rank = dist.get_rank() if distributed else 0
|
| 33 |
+
local_rank = int(os.environ.get("LOCAL_RANK", "0"))
|
| 34 |
+
device = torch.device(f"cuda:{local_rank}" if use_accelerator else "cpu")
|
| 35 |
+
if use_accelerator:
|
| 36 |
+
torch.cuda.set_device(device)
|
| 37 |
+
raw = np.load(ROOT / cfg["data"]["file"])
|
| 38 |
+
tensors = [torch.from_numpy(raw[name].astype(np.float32)) for name in ("initial", "target", "surface", "advection")]
|
| 39 |
+
source = torch.from_numpy(raw["source"].astype(np.int64))
|
| 40 |
+
dataset = TensorDataset(*tensors, source)
|
| 41 |
+
sampler = DistributedSampler(dataset, shuffle=True, seed=cfg["seed"]) if distributed else None
|
| 42 |
+
loader = DataLoader(dataset, batch_size=cfg["train"]["batch_size"], sampler=sampler,
|
| 43 |
+
shuffle=sampler is None)
|
| 44 |
+
all_state = torch.from_numpy(raw["target"].astype(np.float32))
|
| 45 |
+
state_mean = all_state.mean((0, 1)).to(device)
|
| 46 |
+
state_std = all_state.std((0, 1), unbiased=False).clamp_min(1e-5).to(device)
|
| 47 |
+
tendency_mean = torch.zeros(68, device=device)
|
| 48 |
+
tendency_std = torch.cat((torch.full((34,), 0.02), torch.full((34,), 2e-7))).to(device)
|
| 49 |
+
layer_mass = torch.from_numpy(raw["layer_mass"].astype(np.float32)).to(device)
|
| 50 |
+
model = StableNNPhys(cfg["model"]["hidden_size"]).to(device)
|
| 51 |
+
if distributed:
|
| 52 |
+
model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
|
| 53 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=cfg["train"]["learning_rate"])
|
| 54 |
+
history = []
|
| 55 |
+
for epoch in range(cfg["train"]["epochs"]):
|
| 56 |
+
if sampler:
|
| 57 |
+
sampler.set_epoch(epoch)
|
| 58 |
+
total = 0.0
|
| 59 |
+
for initial, target, surface, advection, _ in loader:
|
| 60 |
+
initial, target, surface, advection = [x.to(device) for x in (initial, target, surface, advection)]
|
| 61 |
+
prediction, _ = rollout(model, initial, surface, advection, state_mean, state_std,
|
| 62 |
+
tendency_mean, tendency_std, float(raw["dt_seconds"]))
|
| 63 |
+
loss = rollout_loss(prediction, target, layer_mass.expand(initial.shape[0], -1), cfg["train"]["loss_mode"])
|
| 64 |
+
optimizer.zero_grad()
|
| 65 |
+
loss.backward()
|
| 66 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 67 |
+
optimizer.step()
|
| 68 |
+
total += loss.item() * initial.shape[0]
|
| 69 |
+
value = total / len(dataset)
|
| 70 |
+
history.append(value)
|
| 71 |
+
if rank == 0:
|
| 72 |
+
print(f"epoch={epoch + 1} loss={value:.6g}")
|
| 73 |
+
if rank == 0:
|
| 74 |
+
module = model.module if distributed else model
|
| 75 |
+
checkpoint = ROOT / cfg["paths"]["checkpoint"]
|
| 76 |
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
| 77 |
+
payload = {"format_version": CHECKPOINT_FORMAT_VERSION, "model": module.state_dict(),
|
| 78 |
+
"optimizer_state_dict": optimizer.state_dict(), "epoch": cfg["train"]["epochs"],
|
| 79 |
+
"model_config": dict(cfg["model"]), "training_config": dict(cfg["train"]),
|
| 80 |
+
"normalization": {"state_mean": state_mean.cpu(), "state_std": state_std.cpu(),
|
| 81 |
+
"tendency_mean": tendency_mean.cpu(), "tendency_std": tendency_std.cpu()},
|
| 82 |
+
"variables": {"input": ["sL[34]", "qT[34]", "SHF", "LHF", "SOLIN"],
|
| 83 |
+
"output": ["dsL_dt[34]", "dqT_dt[34]"]}, "dt_seconds": float(raw["dt_seconds"])}
|
| 84 |
+
torch.save(payload, checkpoint)
|
| 85 |
+
metrics = ROOT / cfg["paths"]["training_metrics"]
|
| 86 |
+
metrics.parent.mkdir(parents=True, exist_ok=True)
|
| 87 |
+
metrics.write_text(json.dumps({"loss": history, "world_size": dist.get_world_size() if distributed else 1,
|
| 88 |
+
"paper_model": cfg["train"]["paper_model"]}, indent=2))
|
| 89 |
+
print(f"saved {checkpoint}")
|
| 90 |
+
if distributed:
|
| 91 |
+
dist.destroy_process_group()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|