Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -18
- README.md +204 -0
- conf/config.yaml +527 -0
- config.json +147 -0
- configuration.json +12 -0
- model/NeuralGCM.py +230 -0
- model/NeuralGCM_demo.py +60 -0
- model/data/era5_tl31_19590102T00.nc +3 -0
- model/data/tl63_stochastic_mini.pkl +3 -0
- model/legacy/api.py +601 -0
- model/legacy/correctors.py +177 -0
- model/legacy/decoders.py +749 -0
- model/legacy/diagnostics.py +420 -0
- model/legacy/embeddings.py +380 -0
- model/legacy/encoders.py +874 -0
- model/legacy/equations.py +402 -0
- model/legacy/features.py +867 -0
- model/legacy/filters.py +457 -0
- model/legacy/forcings.py +292 -0
- model/legacy/gin_utils.py +81 -0
- model/legacy/initializers.py +124 -0
- model/legacy/integrators.py +36 -0
- model/legacy/layers.py +443 -0
- model/legacy/mappings.py +206 -0
- model/legacy/model_builder.py +744 -0
- model/legacy/model_utils.py +527 -0
- model/legacy/optimization.py +179 -0
- model/legacy/orographies.py +131 -0
- model/legacy/parameterizations.py +171 -0
- model/legacy/perturbations.py +230 -0
- model/legacy/physics_specifications.py +115 -0
- model/legacy/steps.py +332 -0
- model/legacy/stochastic.py +1225 -0
- model/legacy/towers.py +206 -0
- model/legacy/transforms.py +738 -0
- model/reference_code/datasets.py +70 -0
- model/reference_code/experiment.py +1400 -0
- model/reference_code/linear_transforms.py +342 -0
- model/reference_code/metrics.py +696 -0
- model/reference_code/metrics_base.py +150 -0
- model/reference_code/metrics_util.py +453 -0
- model/reference_code/paper_configs/deterministic_0_7_deg.gin +2368 -0
- model/reference_code/paper_configs/deterministic_1_4_deg.gin +2371 -0
- model/reference_code/paper_configs/deterministic_2_8_deg.gin +0 -0
- model/reference_code/paper_configs/stochastic_1_4_deg.gin +0 -0
- model/reference_code/reader.py +624 -0
- model/reference_code/stochastic_losses.py +458 -0
- model/reference_code/train_utils.py +662 -0
- scripts/checkpoint_info.py +45 -0
- scripts/common.py +405 -0
.gitattributes
CHANGED
|
@@ -1,14 +1,8 @@
|
|
| 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
|
|
@@ -16,20 +10,11 @@
|
|
| 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 |
-
*
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 1 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 2 |
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 3 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.hdf5 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 6 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.npy filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 10 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 11 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 13 |
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 16 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
| 17 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
| 18 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
weight/* filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
model/data/era5_tl31_19590102T00.nc filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: JAX
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Weather Forecasting
|
| 10 |
+
- Climate Simulation
|
| 11 |
+
- Hybrid Physics-ML
|
| 12 |
+
- ERA5
|
| 13 |
+
- NeuralGCM
|
| 14 |
+
tasks: []
|
| 15 |
+
datasets:
|
| 16 |
+
- OneScience/ERA5
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
<p align="center">
|
| 20 |
+
<strong>
|
| 21 |
+
<span style="font-size: 30px;">NeuralGCM</span>
|
| 22 |
+
</strong>
|
| 23 |
+
</p>
|
| 24 |
+
|
| 25 |
+
# Model Introduction
|
| 26 |
+
|
| 27 |
+
NeuralGCM (Neural General Circulation Models) is an open-source hybrid machine-learning and physics-based atmospheric model developed by Google Research for weather forecasting and climate simulation.
|
| 28 |
+
|
| 29 |
+
Paper: Neural General Circulation Models for Weather and Climate
|
| 30 |
+
|
| 31 |
+
https://arxiv.org/abs/2311.07222
|
| 32 |
+
|
| 33 |
+
# Model Description
|
| 34 |
+
|
| 35 |
+
NeuralGCM is built around a differentiable atmospheric dynamical core. Neural networks represent unresolved physical processes, the encoder, and the decoder, improving forecast efficiency while retaining physical constraints.
|
| 36 |
+
|
| 37 |
+
| Profile | Resolution | Type | Bundled official checkpoint |
|
| 38 |
+
| :--- | :---: | :--- | :--- |
|
| 39 |
+
| `weather_forecast` | 0.7 degrees (`512 x 256`) | Deterministic weather forecasting for approximately 2 to 15 days | `weight/models_v1_deterministic_0_7_deg.pkl` |
|
| 40 |
+
| `climate_scale` | 1.4 degrees (`256 x 128`) | Deterministic climate-scale simulation | `weight/models_v1_deterministic_1_4_deg.pkl` |
|
| 41 |
+
| `forecast_2_8_deg` | 2.8 degrees (`128 x 64`) | Deterministic weather forecasting | `weight/models_v1_deterministic_2_8_deg.pkl` |
|
| 42 |
+
| `stochastic_1_4_deg` | 1.4 degrees (`256 x 128`) | Stochastic weather forecasting | `weight/models_v1_stochastic_1_4_deg.pkl` |
|
| 43 |
+
|
| 44 |
+
# Use Cases
|
| 45 |
+
|
| 46 |
+
| Scenario | Description |
|
| 47 |
+
| :---: | :--- |
|
| 48 |
+
| Global weather forecasting | Train the 0.7-degree model on ERA5 data for short- to medium-range weather forecasting. |
|
| 49 |
+
| Climate-scale simulation | Train the 1.4-degree model on ERA5 data for longer atmospheric simulations. |
|
| 50 |
+
| Low-resolution experiments | Use the 2.8-degree data profile for lower-cost weather forecasting experiments. |
|
| 51 |
+
| Local quick validation | Generate HDF5 data with the required channel protocol using `scripts/fake_data.py` and validate the data, model, and checkpoint workflows. |
|
| 52 |
+
| ModelScope / OneCode execution | Download the standalone model package, install the OneScience and JAX dependencies, and run the scripts directly. |
|
| 53 |
+
| Multi-device training | Run synchronous data-parallel training on multiple local accelerators. |
|
| 54 |
+
|
| 55 |
+
# Usage Guide
|
| 56 |
+
|
| 57 |
+
## 1. OneCode Usage
|
| 58 |
+
|
| 59 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 60 |
+
|
| 61 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 62 |
+
|
| 63 |
+
## 2. Manual Installation and Usage
|
| 64 |
+
|
| 65 |
+
**Hardware Requirements**
|
| 66 |
+
|
| 67 |
+
- A GPU or DCU is recommended.
|
| 68 |
+
- A CPU can be used for import checks and small-scale connectivity validation, but full training and inference will be slow.
|
| 69 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version compatible with the current cluster, is recommended.
|
| 70 |
+
|
| 71 |
+
### Download the Model Package
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
hf download OneScience-Group/NeuralGCM --local-dir ./NeuralGCM
|
| 75 |
+
cd NeuralGCM
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### Install the Runtime Environment
|
| 79 |
+
|
| 80 |
+
**DCU Environment**
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
# Activate DTK and conda first.
|
| 84 |
+
conda create -n onescience311 python=3.11 -y
|
| 85 |
+
conda activate onescience311
|
| 86 |
+
# Installation with uv is also supported.
|
| 87 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
**GPU Environment**
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
# Activate conda first.
|
| 94 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 95 |
+
conda activate onescience311
|
| 96 |
+
# Installation with uv is also supported.
|
| 97 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Training Data
|
| 101 |
+
|
| 102 |
+
The OneScience community provides an ERA5 data slice for training. Download it with the following command and confirm that the data path in `conf/config.yaml` is correct:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Generate Synthetic Data
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
python scripts/fake_data.py
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
The script creates yearly HDF5 files under `data/data/`, writes synthetic static fields to `data/static.nc`, and saves channel, time-window, and grid metadata to `data/metadata/dataset_card.json`. The synthetic fields use approximate physical units but are intended only for shape, loading, regridding, and numerical-stability checks.
|
| 115 |
+
|
| 116 |
+
### Training
|
| 117 |
+
|
| 118 |
+
Single device:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
# 0.7-degree deterministic short- to medium-range weather forecasting
|
| 122 |
+
python scripts/train_weather_forecast.py
|
| 123 |
+
# 1.4-degree deterministic climate-scale simulation
|
| 124 |
+
python scripts/train_climate_scale.py
|
| 125 |
+
# 2.8-degree deterministic low-resolution weather forecasting
|
| 126 |
+
python scripts/train_forecast_2_8_deg.py
|
| 127 |
+
# 1.4-degree stochastic weather forecasting
|
| 128 |
+
python scripts/train_stochastic_1_4_deg.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Multiple devices:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
# 0.7-degree deterministic short- to medium-range weather forecasting
|
| 135 |
+
python scripts/train_weather_forecast.py --devices 8
|
| 136 |
+
# 1.4-degree deterministic climate-scale simulation
|
| 137 |
+
python scripts/train_climate_scale.py --devices 8
|
| 138 |
+
# 2.8-degree deterministic low-resolution weather forecasting
|
| 139 |
+
python scripts/train_forecast_2_8_deg.py --devices 8
|
| 140 |
+
# 1.4-degree stochastic weather forecasting
|
| 141 |
+
python scripts/train_stochastic_1_4_deg.py --devices 8
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
### Fine-tuning
|
| 145 |
+
|
| 146 |
+
Fine-tuning can start from either a checkpoint produced by local training or the bundled official checkpoint for the selected profile.
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
# Use the bundled official checkpoint for each profile.
|
| 150 |
+
python scripts/train_weather_forecast.py --finetune weight/models_v1_deterministic_0_7_deg.pkl
|
| 151 |
+
python scripts/train_climate_scale.py --finetune weight/models_v1_deterministic_1_4_deg.pkl
|
| 152 |
+
python scripts/train_forecast_2_8_deg.py --finetune weight/models_v1_deterministic_2_8_deg.pkl
|
| 153 |
+
python scripts/train_stochastic_1_4_deg.py --finetune weight/models_v1_stochastic_1_4_deg.pkl
|
| 154 |
+
|
| 155 |
+
# Alternatively, provide a local checkpoint explicitly.
|
| 156 |
+
python scripts/train_weather_forecast.py --finetune ./data/checkpoint/model_bak.pkl
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
For multi-device fine-tuning, add `--devices` to the corresponding command.
|
| 160 |
+
|
| 161 |
+
### Pre-trained Weights
|
| 162 |
+
|
| 163 |
+
This project includes the following official pre-trained checkpoints:
|
| 164 |
+
|
| 165 |
+
| Local file | Official release path |
|
| 166 |
+
| :--- | :--- |
|
| 167 |
+
| `weight/models_v1_deterministic_0_7_deg.pkl` | `gs://neuralgcm/models/v1/deterministic_0_7_deg.pkl` |
|
| 168 |
+
| `weight/models_v1_deterministic_1_4_deg.pkl` | `gs://neuralgcm/models/v1/deterministic_1_4_deg.pkl` |
|
| 169 |
+
| `weight/models_v1_deterministic_2_8_deg.pkl` | `gs://neuralgcm/models/v1/deterministic_2_8_deg.pkl` |
|
| 170 |
+
| `weight/models_v1_stochastic_1_4_deg.pkl` | `gs://neuralgcm/models/v1/stochastic_1_4_deg.pkl` |
|
| 171 |
+
|
| 172 |
+
### Inference
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
# 0.7-degree deterministic short- to medium-range weather forecasting
|
| 176 |
+
python scripts/inference.py --mode weather_forecast --checkpoint weight/models_v1_deterministic_0_7_deg.pkl
|
| 177 |
+
# 1.4-degree deterministic climate-scale simulation
|
| 178 |
+
python scripts/inference.py --mode climate_scale --checkpoint weight/models_v1_deterministic_1_4_deg.pkl
|
| 179 |
+
# 2.8-degree deterministic low-resolution weather forecasting
|
| 180 |
+
python scripts/inference.py --mode forecast_2_8_deg --checkpoint weight/models_v1_deterministic_2_8_deg.pkl
|
| 181 |
+
# 1.4-degree stochastic weather forecasting
|
| 182 |
+
python scripts/inference.py --mode stochastic_1_4_deg --checkpoint weight/models_v1_stochastic_1_4_deg.pkl
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
Without an explicit `--checkpoint`, inference first checks `./data/checkpoint/model_bak.pkl`. The default output is `results/predictions.nc`, containing pressure-level variables with their official names and rollout time coordinates.
|
| 186 |
+
|
| 187 |
+
### Evaluation and Visualization
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
python scripts/result.py
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
# Official OneScience Resources
|
| 194 |
+
|
| 195 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 196 |
+
| --- | --- | --- |
|
| 197 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 198 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 199 |
+
|
| 200 |
+
# Citation and License
|
| 201 |
+
|
| 202 |
+
- This repository is a reproduction of the original NeuralGCM paper.
|
| 203 |
+
- The repository code is provided under the Apache License 2.0.
|
| 204 |
+
- The trained model weights released by Google, including the four checkpoints in this directory, are licensed under the Creative Commons Attribution-ShareAlike 4.0 International license (CC BY-SA 4.0). Redistribution or adaptation of the weights must preserve attribution and use the same license as required by those terms.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
project:
|
| 2 |
+
name: neuralgcm_develop
|
| 3 |
+
task: earth_system_forecasting
|
| 4 |
+
seed: 20260904
|
| 5 |
+
|
| 6 |
+
paths:
|
| 7 |
+
project_root: .
|
| 8 |
+
# Upstream source is supplied by the external neuralgcm package.
|
| 9 |
+
official_source_dir: null
|
| 10 |
+
virtual_era5_dir: data
|
| 11 |
+
checkpoint_dir: data/checkpoint
|
| 12 |
+
result_dir: results
|
| 13 |
+
metadata_dir: metadata
|
| 14 |
+
|
| 15 |
+
model:
|
| 16 |
+
# Native NeuralGCM pressure-level input contract.
|
| 17 |
+
variant: weather_forecast
|
| 18 |
+
grid_degrees: 0.7
|
| 19 |
+
# Gaussian grid shape in [longitude, latitude] order (the model API reports
|
| 20 |
+
# the same grid as (latitude, longitude) when printing sizes).
|
| 21 |
+
grid_shape: [512, 256]
|
| 22 |
+
profiles:
|
| 23 |
+
weather_forecast:
|
| 24 |
+
description: "未来2至15天天气预报"
|
| 25 |
+
official_reference: models_v1_deterministic_0_7_deg.pkl
|
| 26 |
+
grid_degrees: 0.7
|
| 27 |
+
grid_shape: [512, 256]
|
| 28 |
+
climate_scale:
|
| 29 |
+
description: "气候尺度模拟"
|
| 30 |
+
official_reference: models_v1_deterministic_1_4_deg.pkl
|
| 31 |
+
grid_degrees: 1.4
|
| 32 |
+
grid_shape: [256, 128]
|
| 33 |
+
forecast_2_8_deg:
|
| 34 |
+
description: "2.8度天气预报"
|
| 35 |
+
official_reference: models_v1_deterministic_2_8_deg.pkl
|
| 36 |
+
grid_degrees: 2.8
|
| 37 |
+
grid_shape: [128, 64]
|
| 38 |
+
stochastic_1_4_deg:
|
| 39 |
+
description: "1.4度随机预报"
|
| 40 |
+
official_reference: models_v1_stochastic_1_4_deg.pkl
|
| 41 |
+
grid_degrees: 1.4
|
| 42 |
+
grid_shape: [256, 128]
|
| 43 |
+
pressure_levels_hpa: [1, 2, 3, 5, 7, 10, 20, 30, 50, 70, 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 775, 800, 825, 850, 875, 900, 925, 950, 975, 1000]
|
| 44 |
+
input_variables: [geopotential, specific_humidity, temperature, u_component_of_wind, v_component_of_wind]
|
| 45 |
+
optional_input_variables: [specific_cloud_ice_water_content, specific_cloud_liquid_water_content]
|
| 46 |
+
forcing_variables: [sea_ice_cover, sea_surface_temperature]
|
| 47 |
+
official_checkpoint: null
|
| 48 |
+
load_pretrained: false
|
| 49 |
+
|
| 50 |
+
data:
|
| 51 |
+
dataset_class: onescience.datapipes.climate.ERA5Dataset
|
| 52 |
+
data_dir: data
|
| 53 |
+
# Optional auxiliary static fields generated by fake_data.py or supplied by
|
| 54 |
+
# a real ERA5 preprocessing job. Dynamic channels remain in data/*.h5.
|
| 55 |
+
static_file: data/static.nc
|
| 56 |
+
# Exact Gaussian-grid static fields extracted from the four official
|
| 57 |
+
# checkpoints by scripts/prepare_static_data.py. These take precedence over
|
| 58 |
+
# the source-grid synthetic fallback above.
|
| 59 |
+
static_files:
|
| 60 |
+
weather_forecast: data/static/weather_forecast.nc
|
| 61 |
+
climate_scale: data/static/climate_scale.nc
|
| 62 |
+
forecast_2_8_deg: data/static/forecast_2_8_deg.nc
|
| 63 |
+
stochastic_1_4_deg: data/static/stochastic_1_4_deg.nc
|
| 64 |
+
field_key: fields
|
| 65 |
+
time_step_hours: 6
|
| 66 |
+
input_steps: 1
|
| 67 |
+
# Official training consumes a time trajectory. Increase for production
|
| 68 |
+
# rollouts; 1 is retained for the minimal data validation command.
|
| 69 |
+
output_steps: 1
|
| 70 |
+
normalize: false
|
| 71 |
+
batch_size: 1
|
| 72 |
+
num_workers: 0
|
| 73 |
+
train_years: [1999]
|
| 74 |
+
val_years: [2000]
|
| 75 |
+
test_years: [2001]
|
| 76 |
+
virtual:
|
| 77 |
+
# Memory-conscious default: one initial frame + eight future 6-hour
|
| 78 |
+
# frames. Use --forecast-steps 60 for the full official 15-day horizon.
|
| 79 |
+
timesteps_per_year: 9
|
| 80 |
+
forecast_steps: 8
|
| 81 |
+
forecast_horizon_days: 2
|
| 82 |
+
height: 721
|
| 83 |
+
width: 1440
|
| 84 |
+
seed: 20260904
|
| 85 |
+
# Exact flattened fields order used by fake_data.py and ERA5Dataset.
|
| 86 |
+
channel_order: &channel_order
|
| 87 |
+
- geopotential_1
|
| 88 |
+
- geopotential_2
|
| 89 |
+
- geopotential_3
|
| 90 |
+
- geopotential_5
|
| 91 |
+
- geopotential_7
|
| 92 |
+
- geopotential_10
|
| 93 |
+
- geopotential_20
|
| 94 |
+
- geopotential_30
|
| 95 |
+
- geopotential_50
|
| 96 |
+
- geopotential_70
|
| 97 |
+
- geopotential_100
|
| 98 |
+
- geopotential_125
|
| 99 |
+
- geopotential_150
|
| 100 |
+
- geopotential_175
|
| 101 |
+
- geopotential_200
|
| 102 |
+
- geopotential_225
|
| 103 |
+
- geopotential_250
|
| 104 |
+
- geopotential_300
|
| 105 |
+
- geopotential_350
|
| 106 |
+
- geopotential_400
|
| 107 |
+
- geopotential_450
|
| 108 |
+
- geopotential_500
|
| 109 |
+
- geopotential_550
|
| 110 |
+
- geopotential_600
|
| 111 |
+
- geopotential_650
|
| 112 |
+
- geopotential_700
|
| 113 |
+
- geopotential_750
|
| 114 |
+
- geopotential_775
|
| 115 |
+
- geopotential_800
|
| 116 |
+
- geopotential_825
|
| 117 |
+
- geopotential_850
|
| 118 |
+
- geopotential_875
|
| 119 |
+
- geopotential_900
|
| 120 |
+
- geopotential_925
|
| 121 |
+
- geopotential_950
|
| 122 |
+
- geopotential_975
|
| 123 |
+
- geopotential_1000
|
| 124 |
+
- specific_humidity_1
|
| 125 |
+
- specific_humidity_2
|
| 126 |
+
- specific_humidity_3
|
| 127 |
+
- specific_humidity_5
|
| 128 |
+
- specific_humidity_7
|
| 129 |
+
- specific_humidity_10
|
| 130 |
+
- specific_humidity_20
|
| 131 |
+
- specific_humidity_30
|
| 132 |
+
- specific_humidity_50
|
| 133 |
+
- specific_humidity_70
|
| 134 |
+
- specific_humidity_100
|
| 135 |
+
- specific_humidity_125
|
| 136 |
+
- specific_humidity_150
|
| 137 |
+
- specific_humidity_175
|
| 138 |
+
- specific_humidity_200
|
| 139 |
+
- specific_humidity_225
|
| 140 |
+
- specific_humidity_250
|
| 141 |
+
- specific_humidity_300
|
| 142 |
+
- specific_humidity_350
|
| 143 |
+
- specific_humidity_400
|
| 144 |
+
- specific_humidity_450
|
| 145 |
+
- specific_humidity_500
|
| 146 |
+
- specific_humidity_550
|
| 147 |
+
- specific_humidity_600
|
| 148 |
+
- specific_humidity_650
|
| 149 |
+
- specific_humidity_700
|
| 150 |
+
- specific_humidity_750
|
| 151 |
+
- specific_humidity_775
|
| 152 |
+
- specific_humidity_800
|
| 153 |
+
- specific_humidity_825
|
| 154 |
+
- specific_humidity_850
|
| 155 |
+
- specific_humidity_875
|
| 156 |
+
- specific_humidity_900
|
| 157 |
+
- specific_humidity_925
|
| 158 |
+
- specific_humidity_950
|
| 159 |
+
- specific_humidity_975
|
| 160 |
+
- specific_humidity_1000
|
| 161 |
+
- temperature_1
|
| 162 |
+
- temperature_2
|
| 163 |
+
- temperature_3
|
| 164 |
+
- temperature_5
|
| 165 |
+
- temperature_7
|
| 166 |
+
- temperature_10
|
| 167 |
+
- temperature_20
|
| 168 |
+
- temperature_30
|
| 169 |
+
- temperature_50
|
| 170 |
+
- temperature_70
|
| 171 |
+
- temperature_100
|
| 172 |
+
- temperature_125
|
| 173 |
+
- temperature_150
|
| 174 |
+
- temperature_175
|
| 175 |
+
- temperature_200
|
| 176 |
+
- temperature_225
|
| 177 |
+
- temperature_250
|
| 178 |
+
- temperature_300
|
| 179 |
+
- temperature_350
|
| 180 |
+
- temperature_400
|
| 181 |
+
- temperature_450
|
| 182 |
+
- temperature_500
|
| 183 |
+
- temperature_550
|
| 184 |
+
- temperature_600
|
| 185 |
+
- temperature_650
|
| 186 |
+
- temperature_700
|
| 187 |
+
- temperature_750
|
| 188 |
+
- temperature_775
|
| 189 |
+
- temperature_800
|
| 190 |
+
- temperature_825
|
| 191 |
+
- temperature_850
|
| 192 |
+
- temperature_875
|
| 193 |
+
- temperature_900
|
| 194 |
+
- temperature_925
|
| 195 |
+
- temperature_950
|
| 196 |
+
- temperature_975
|
| 197 |
+
- temperature_1000
|
| 198 |
+
- u_component_of_wind_1
|
| 199 |
+
- u_component_of_wind_2
|
| 200 |
+
- u_component_of_wind_3
|
| 201 |
+
- u_component_of_wind_5
|
| 202 |
+
- u_component_of_wind_7
|
| 203 |
+
- u_component_of_wind_10
|
| 204 |
+
- u_component_of_wind_20
|
| 205 |
+
- u_component_of_wind_30
|
| 206 |
+
- u_component_of_wind_50
|
| 207 |
+
- u_component_of_wind_70
|
| 208 |
+
- u_component_of_wind_100
|
| 209 |
+
- u_component_of_wind_125
|
| 210 |
+
- u_component_of_wind_150
|
| 211 |
+
- u_component_of_wind_175
|
| 212 |
+
- u_component_of_wind_200
|
| 213 |
+
- u_component_of_wind_225
|
| 214 |
+
- u_component_of_wind_250
|
| 215 |
+
- u_component_of_wind_300
|
| 216 |
+
- u_component_of_wind_350
|
| 217 |
+
- u_component_of_wind_400
|
| 218 |
+
- u_component_of_wind_450
|
| 219 |
+
- u_component_of_wind_500
|
| 220 |
+
- u_component_of_wind_550
|
| 221 |
+
- u_component_of_wind_600
|
| 222 |
+
- u_component_of_wind_650
|
| 223 |
+
- u_component_of_wind_700
|
| 224 |
+
- u_component_of_wind_750
|
| 225 |
+
- u_component_of_wind_775
|
| 226 |
+
- u_component_of_wind_800
|
| 227 |
+
- u_component_of_wind_825
|
| 228 |
+
- u_component_of_wind_850
|
| 229 |
+
- u_component_of_wind_875
|
| 230 |
+
- u_component_of_wind_900
|
| 231 |
+
- u_component_of_wind_925
|
| 232 |
+
- u_component_of_wind_950
|
| 233 |
+
- u_component_of_wind_975
|
| 234 |
+
- u_component_of_wind_1000
|
| 235 |
+
- v_component_of_wind_1
|
| 236 |
+
- v_component_of_wind_2
|
| 237 |
+
- v_component_of_wind_3
|
| 238 |
+
- v_component_of_wind_5
|
| 239 |
+
- v_component_of_wind_7
|
| 240 |
+
- v_component_of_wind_10
|
| 241 |
+
- v_component_of_wind_20
|
| 242 |
+
- v_component_of_wind_30
|
| 243 |
+
- v_component_of_wind_50
|
| 244 |
+
- v_component_of_wind_70
|
| 245 |
+
- v_component_of_wind_100
|
| 246 |
+
- v_component_of_wind_125
|
| 247 |
+
- v_component_of_wind_150
|
| 248 |
+
- v_component_of_wind_175
|
| 249 |
+
- v_component_of_wind_200
|
| 250 |
+
- v_component_of_wind_225
|
| 251 |
+
- v_component_of_wind_250
|
| 252 |
+
- v_component_of_wind_300
|
| 253 |
+
- v_component_of_wind_350
|
| 254 |
+
- v_component_of_wind_400
|
| 255 |
+
- v_component_of_wind_450
|
| 256 |
+
- v_component_of_wind_500
|
| 257 |
+
- v_component_of_wind_550
|
| 258 |
+
- v_component_of_wind_600
|
| 259 |
+
- v_component_of_wind_650
|
| 260 |
+
- v_component_of_wind_700
|
| 261 |
+
- v_component_of_wind_750
|
| 262 |
+
- v_component_of_wind_775
|
| 263 |
+
- v_component_of_wind_800
|
| 264 |
+
- v_component_of_wind_825
|
| 265 |
+
- v_component_of_wind_850
|
| 266 |
+
- v_component_of_wind_875
|
| 267 |
+
- v_component_of_wind_900
|
| 268 |
+
- v_component_of_wind_925
|
| 269 |
+
- v_component_of_wind_950
|
| 270 |
+
- v_component_of_wind_975
|
| 271 |
+
- v_component_of_wind_1000
|
| 272 |
+
- specific_cloud_ice_water_content_1
|
| 273 |
+
- specific_cloud_ice_water_content_2
|
| 274 |
+
- specific_cloud_ice_water_content_3
|
| 275 |
+
- specific_cloud_ice_water_content_5
|
| 276 |
+
- specific_cloud_ice_water_content_7
|
| 277 |
+
- specific_cloud_ice_water_content_10
|
| 278 |
+
- specific_cloud_ice_water_content_20
|
| 279 |
+
- specific_cloud_ice_water_content_30
|
| 280 |
+
- specific_cloud_ice_water_content_50
|
| 281 |
+
- specific_cloud_ice_water_content_70
|
| 282 |
+
- specific_cloud_ice_water_content_100
|
| 283 |
+
- specific_cloud_ice_water_content_125
|
| 284 |
+
- specific_cloud_ice_water_content_150
|
| 285 |
+
- specific_cloud_ice_water_content_175
|
| 286 |
+
- specific_cloud_ice_water_content_200
|
| 287 |
+
- specific_cloud_ice_water_content_225
|
| 288 |
+
- specific_cloud_ice_water_content_250
|
| 289 |
+
- specific_cloud_ice_water_content_300
|
| 290 |
+
- specific_cloud_ice_water_content_350
|
| 291 |
+
- specific_cloud_ice_water_content_400
|
| 292 |
+
- specific_cloud_ice_water_content_450
|
| 293 |
+
- specific_cloud_ice_water_content_500
|
| 294 |
+
- specific_cloud_ice_water_content_550
|
| 295 |
+
- specific_cloud_ice_water_content_600
|
| 296 |
+
- specific_cloud_ice_water_content_650
|
| 297 |
+
- specific_cloud_ice_water_content_700
|
| 298 |
+
- specific_cloud_ice_water_content_750
|
| 299 |
+
- specific_cloud_ice_water_content_775
|
| 300 |
+
- specific_cloud_ice_water_content_800
|
| 301 |
+
- specific_cloud_ice_water_content_825
|
| 302 |
+
- specific_cloud_ice_water_content_850
|
| 303 |
+
- specific_cloud_ice_water_content_875
|
| 304 |
+
- specific_cloud_ice_water_content_900
|
| 305 |
+
- specific_cloud_ice_water_content_925
|
| 306 |
+
- specific_cloud_ice_water_content_950
|
| 307 |
+
- specific_cloud_ice_water_content_975
|
| 308 |
+
- specific_cloud_ice_water_content_1000
|
| 309 |
+
- specific_cloud_liquid_water_content_1
|
| 310 |
+
- specific_cloud_liquid_water_content_2
|
| 311 |
+
- specific_cloud_liquid_water_content_3
|
| 312 |
+
- specific_cloud_liquid_water_content_5
|
| 313 |
+
- specific_cloud_liquid_water_content_7
|
| 314 |
+
- specific_cloud_liquid_water_content_10
|
| 315 |
+
- specific_cloud_liquid_water_content_20
|
| 316 |
+
- specific_cloud_liquid_water_content_30
|
| 317 |
+
- specific_cloud_liquid_water_content_50
|
| 318 |
+
- specific_cloud_liquid_water_content_70
|
| 319 |
+
- specific_cloud_liquid_water_content_100
|
| 320 |
+
- specific_cloud_liquid_water_content_125
|
| 321 |
+
- specific_cloud_liquid_water_content_150
|
| 322 |
+
- specific_cloud_liquid_water_content_175
|
| 323 |
+
- specific_cloud_liquid_water_content_200
|
| 324 |
+
- specific_cloud_liquid_water_content_225
|
| 325 |
+
- specific_cloud_liquid_water_content_250
|
| 326 |
+
- specific_cloud_liquid_water_content_300
|
| 327 |
+
- specific_cloud_liquid_water_content_350
|
| 328 |
+
- specific_cloud_liquid_water_content_400
|
| 329 |
+
- specific_cloud_liquid_water_content_450
|
| 330 |
+
- specific_cloud_liquid_water_content_500
|
| 331 |
+
- specific_cloud_liquid_water_content_550
|
| 332 |
+
- specific_cloud_liquid_water_content_600
|
| 333 |
+
- specific_cloud_liquid_water_content_650
|
| 334 |
+
- specific_cloud_liquid_water_content_700
|
| 335 |
+
- specific_cloud_liquid_water_content_750
|
| 336 |
+
- specific_cloud_liquid_water_content_775
|
| 337 |
+
- specific_cloud_liquid_water_content_800
|
| 338 |
+
- specific_cloud_liquid_water_content_825
|
| 339 |
+
- specific_cloud_liquid_water_content_850
|
| 340 |
+
- specific_cloud_liquid_water_content_875
|
| 341 |
+
- specific_cloud_liquid_water_content_900
|
| 342 |
+
- specific_cloud_liquid_water_content_925
|
| 343 |
+
- specific_cloud_liquid_water_content_950
|
| 344 |
+
- specific_cloud_liquid_water_content_975
|
| 345 |
+
- specific_cloud_liquid_water_content_1000
|
| 346 |
+
- sea_ice_cover
|
| 347 |
+
- sea_surface_temperature
|
| 348 |
+
|
| 349 |
+
training:
|
| 350 |
+
mode: weather_forecast
|
| 351 |
+
max_steps: 3
|
| 352 |
+
trajectory_length: 2
|
| 353 |
+
# Global batch size. For --devices N it is rounded up to a multiple of N;
|
| 354 |
+
# each replica then receives distinct trajectories.
|
| 355 |
+
samples_per_step: 1
|
| 356 |
+
# Number of local JAX devices for optional synchronous data parallelism.
|
| 357 |
+
devices: 1
|
| 358 |
+
shuffle: true
|
| 359 |
+
drop_last: true
|
| 360 |
+
# OneScience ERA5Dataset samples are prefetched on host threads while the
|
| 361 |
+
# current DCU step runs. Keep the queue shallow for full 721x1440 fields.
|
| 362 |
+
data_num_workers: 2
|
| 363 |
+
prefetch_batches: 1
|
| 364 |
+
# Full params/EMA/optimizer/reader state is always saved on clean exit. Set a
|
| 365 |
+
# positive interval for periodic resumable checkpoints during long runs.
|
| 366 |
+
checkpoint_interval: 0
|
| 367 |
+
learning_rate: 0.0001
|
| 368 |
+
optimizer:
|
| 369 |
+
name: adam
|
| 370 |
+
schedule: constant
|
| 371 |
+
b1: 0.9
|
| 372 |
+
b2: 0.95
|
| 373 |
+
eps: 1.0e-6
|
| 374 |
+
# Optional piecewise constant schedule. Empty boundaries use base LR.
|
| 375 |
+
rates: []
|
| 376 |
+
boundaries: []
|
| 377 |
+
# Public Experiment tracks an EMA for evaluation/checkpointing. Set to 0 to
|
| 378 |
+
# disable; otherwise this is the effective average window in optimizer steps.
|
| 379 |
+
ema_num_steps: 1000
|
| 380 |
+
rollout_schedule: []
|
| 381 |
+
# Public NeuralGCM uses transformed trajectory losses. The private job loss
|
| 382 |
+
# bindings and complete normalization tables are unavailable, so every
|
| 383 |
+
# published coefficient and every auditable fallback remain explicit here.
|
| 384 |
+
gradient_clip_norm: 1.0
|
| 385 |
+
loss:
|
| 386 |
+
backend: official
|
| 387 |
+
# Supplementary G.4 deterministic objective coefficients:
|
| 388 |
+
# 20*data MSE + 0.1*data spectrum MSE + 1*model MSE
|
| 389 |
+
# + 0.1*model spectrum MSE + 2*batch spectral bias MSE.
|
| 390 |
+
data_weight: 20.0
|
| 391 |
+
data_spectrum_weight: 0.1
|
| 392 |
+
model_weight: 1.0
|
| 393 |
+
model_spectrum_weight: 0.1
|
| 394 |
+
bias_weight: 2.0
|
| 395 |
+
accuracy_time_scale_hours: 24.0
|
| 396 |
+
spectral_time_scale_hours: 40.0
|
| 397 |
+
spectral_cutoff_by_mode:
|
| 398 |
+
weather_forecast: 120
|
| 399 |
+
climate_scale: 80
|
| 400 |
+
forecast_2_8_deg: 42
|
| 401 |
+
# Optional exact PerVariableRescaling weights. Each value multiplies the
|
| 402 |
+
# squared error. When null, factor/scale below multiplies the error.
|
| 403 |
+
variable_weights: null
|
| 404 |
+
# The paper uses ERA5 24-hour difference standard deviations, but does not
|
| 405 |
+
# publish the complete numerical tables. These auditable fallbacks keep
|
| 406 |
+
# physical variables balanced; replace them with statistics calculated
|
| 407 |
+
# from the exact ERA5 training vintage for a precision reproduction.
|
| 408 |
+
time_rescaling: legacy
|
| 409 |
+
spectral_weight: 0.0
|
| 410 |
+
variable_scales:
|
| 411 |
+
z: 10000.0
|
| 412 |
+
t: 30.0
|
| 413 |
+
u: 30.0
|
| 414 |
+
v: 30.0
|
| 415 |
+
specific_humidity: 0.01
|
| 416 |
+
specific_cloud_ice_water_content: 1.0e-5
|
| 417 |
+
specific_cloud_liquid_water_content: 2.0e-5
|
| 418 |
+
divergence: 0.1
|
| 419 |
+
vorticity: 0.1
|
| 420 |
+
log_surface_pressure: 0.1
|
| 421 |
+
default: 1.0
|
| 422 |
+
# Additional balancing factors stated explicitly in Supplementary G.3.
|
| 423 |
+
variable_factors:
|
| 424 |
+
z: 2.0
|
| 425 |
+
specific_humidity: 0.66
|
| 426 |
+
log_surface_pressure: 5.0
|
| 427 |
+
specific_cloud_ice_water_content: 0.05
|
| 428 |
+
specific_cloud_liquid_water_content: 0.05
|
| 429 |
+
default: 1.0
|
| 430 |
+
# Order 12 is exact. Absolute half-power cutoffs below are digitized from
|
| 431 |
+
# Supplementary Fig. 8 because the underlying numeric table was not
|
| 432 |
+
# released. Interpolation is performed at the configured output times.
|
| 433 |
+
predictability_filter:
|
| 434 |
+
enabled: true
|
| 435 |
+
order: 12
|
| 436 |
+
lead_hours: [0, 6, 12, 24, 36, 48, 60, 72]
|
| 437 |
+
cutoffs:
|
| 438 |
+
temperature: [80, 120, 120, 95, 45, 35, 30, 25]
|
| 439 |
+
wind: [80, 120, 115, 82, 48, 36, 29, 24]
|
| 440 |
+
moisture: [80, 120, 110, 52, 34, 28, 24, 21]
|
| 441 |
+
divergence: [80, 120, 105, 43, 24, 19, 16, 14]
|
| 442 |
+
default: [80, 120, 115, 82, 48, 36, 29, 24]
|
| 443 |
+
# Optional multiplicative weights for pressure levels, ordered as the
|
| 444 |
+
# configured ERA5 pressure-level list. Empty means uniform weighting.
|
| 445 |
+
level_weights: []
|
| 446 |
+
# Long-run reproduction settings inferred from the public paper description
|
| 447 |
+
# and released training pseudocode. The paper's private job bindings are not
|
| 448 |
+
# available, so these are explicit project settings rather than exact claims.
|
| 449 |
+
# They are enabled only by --paper-defaults; CLI values remain highest priority.
|
| 450 |
+
profiles:
|
| 451 |
+
weather_forecast:
|
| 452 |
+
max_steps: 25000
|
| 453 |
+
learning_rate: 0.001
|
| 454 |
+
optimizer: &paper_optimizer
|
| 455 |
+
schedule: neuralgcm
|
| 456 |
+
warmup_steps: 2000
|
| 457 |
+
decay_start: 15000
|
| 458 |
+
decay_steps: 10000
|
| 459 |
+
decay_rate: 0.5
|
| 460 |
+
rollout_schedule:
|
| 461 |
+
- {trajectory_length: 2, until_step: 0} # 6 h
|
| 462 |
+
- {trajectory_length: 3, until_step: 500} # 12 h
|
| 463 |
+
- {trajectory_length: 4, until_step: 2000} # 18 h
|
| 464 |
+
- {trajectory_length: 5, until_step: 4500} # 24 h
|
| 465 |
+
- {trajectory_length: 7, until_step: 8000} # 36 h
|
| 466 |
+
- {trajectory_length: 9, until_step: 12500} # 48 h
|
| 467 |
+
- {trajectory_length: 11, until_step: 18000} # 60 h
|
| 468 |
+
climate_scale:
|
| 469 |
+
max_steps: 26000
|
| 470 |
+
learning_rate: 0.002
|
| 471 |
+
optimizer: *paper_optimizer
|
| 472 |
+
rollout_schedule: &coarse_rollout_schedule
|
| 473 |
+
- {trajectory_length: 3, until_step: 0} # 12 h
|
| 474 |
+
- {trajectory_length: 5, until_step: 2000} # 24 h
|
| 475 |
+
- {trajectory_length: 7, until_step: 5656} # 36 h
|
| 476 |
+
- {trajectory_length: 9, until_step: 10392} # 48 h
|
| 477 |
+
- {trajectory_length: 11, until_step: 16000} # 60 h
|
| 478 |
+
- {trajectory_length: 13, until_step: 22360} # 72 h
|
| 479 |
+
forecast_2_8_deg:
|
| 480 |
+
max_steps: 38000
|
| 481 |
+
learning_rate: 0.002
|
| 482 |
+
optimizer: *paper_optimizer
|
| 483 |
+
rollout_schedule: *coarse_rollout_schedule
|
| 484 |
+
stochastic_1_4_deg:
|
| 485 |
+
max_steps: 43000
|
| 486 |
+
learning_rate: 0.001
|
| 487 |
+
ensemble_size: 2
|
| 488 |
+
optimizer: *paper_optimizer
|
| 489 |
+
rollout_schedule:
|
| 490 |
+
- {trajectory_length: 2, until_step: 0} # 6 h
|
| 491 |
+
- {trajectory_length: 3, until_step: 500} # 12 h
|
| 492 |
+
- {trajectory_length: 4, until_step: 2000} # 18 h
|
| 493 |
+
- {trajectory_length: 5, until_step: 4500} # 24 h
|
| 494 |
+
- {trajectory_length: 7, until_step: 8000} # 36 h
|
| 495 |
+
- {trajectory_length: 9, until_step: 12500} # 48 h
|
| 496 |
+
- {trajectory_length: 11, until_step: 18000} # 60 h
|
| 497 |
+
- {trajectory_length: 13, until_step: 24500} # 72 h
|
| 498 |
+
- {trajectory_length: 17, until_step: 32000} # 96 h
|
| 499 |
+
- {trajectory_length: 21, until_step: 40500} # 120 h
|
| 500 |
+
loss:
|
| 501 |
+
backend: crps
|
| 502 |
+
variable_weights: null
|
| 503 |
+
variable_scale: 1.0
|
| 504 |
+
nodal_time_scale_hours: 24.0
|
| 505 |
+
spectral_time_scale_hours: 40.0
|
| 506 |
+
spectral_max_wavenumber: 80
|
| 507 |
+
checkpoint: null
|
| 508 |
+
gin_config: null
|
| 509 |
+
train_dataset: null
|
| 510 |
+
eval_dataset: null
|
| 511 |
+
|
| 512 |
+
inference:
|
| 513 |
+
mode: weather_forecast
|
| 514 |
+
# Used by stochastic profiles; deterministic checkpoints ignore the key.
|
| 515 |
+
seed: 20260904
|
| 516 |
+
# Memory-conscious default: eight 6-hour outputs reach forecast day 2.
|
| 517 |
+
# Set --steps 60 to exercise the model's full 15-day capability.
|
| 518 |
+
prediction_steps: 8
|
| 519 |
+
output_interval_hours: 6
|
| 520 |
+
# An official-format local model_bak.pkl takes precedence when present;
|
| 521 |
+
# otherwise inference selects the profile's bundled checkpoint.
|
| 522 |
+
checkpoint: data/checkpoint/model_bak.pkl
|
| 523 |
+
output: results/predictions.nc
|
| 524 |
+
|
| 525 |
+
runtime:
|
| 526 |
+
platform: auto
|
| 527 |
+
dcu_device: 0
|
config.json
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "NeuralGCM",
|
| 3 |
+
"model_type": "neuralgcm",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"PressureLevelModel"
|
| 6 |
+
],
|
| 7 |
+
"framework": "JAX with Haiku and Gin",
|
| 8 |
+
"domain": "atmosphere-and-climate",
|
| 9 |
+
"task": "global-weather-forecasting-and-climate-simulation",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/NeuralGCM.py",
|
| 12 |
+
"scope": "vendored NeuralGCM pressure-level inference and training facade with differentiable dynamics and neural parameterizations"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "hybrid differentiable general circulation model",
|
| 16 |
+
"dynamical_core": "spectral atmospheric dynamics",
|
| 17 |
+
"learned_components": [
|
| 18 |
+
"encoder",
|
| 19 |
+
"decoder",
|
| 20 |
+
"subgrid physical parameterizations"
|
| 21 |
+
],
|
| 22 |
+
"input_format": "xarray pressure-level fields",
|
| 23 |
+
"grid_shape_order": [
|
| 24 |
+
"longitude",
|
| 25 |
+
"latitude"
|
| 26 |
+
],
|
| 27 |
+
"time_step_hours": 6,
|
| 28 |
+
"pressure_level_count": 37,
|
| 29 |
+
"dynamic_variable_count": 7,
|
| 30 |
+
"forcing_variable_count": 2,
|
| 31 |
+
"flattened_input_channels": 261,
|
| 32 |
+
"profiles": {
|
| 33 |
+
"weather_forecast": {
|
| 34 |
+
"type": "deterministic weather forecast",
|
| 35 |
+
"resolution_degrees": 0.7,
|
| 36 |
+
"grid_shape": [
|
| 37 |
+
512,
|
| 38 |
+
256
|
| 39 |
+
],
|
| 40 |
+
"checkpoint": "weight/models_v1_deterministic_0_7_deg.pkl"
|
| 41 |
+
},
|
| 42 |
+
"climate_scale": {
|
| 43 |
+
"type": "deterministic climate simulation",
|
| 44 |
+
"resolution_degrees": 1.4,
|
| 45 |
+
"grid_shape": [
|
| 46 |
+
256,
|
| 47 |
+
128
|
| 48 |
+
],
|
| 49 |
+
"checkpoint": "weight/models_v1_deterministic_1_4_deg.pkl"
|
| 50 |
+
},
|
| 51 |
+
"forecast_2_8_deg": {
|
| 52 |
+
"type": "deterministic weather forecast",
|
| 53 |
+
"resolution_degrees": 2.8,
|
| 54 |
+
"grid_shape": [
|
| 55 |
+
128,
|
| 56 |
+
64
|
| 57 |
+
],
|
| 58 |
+
"checkpoint": "weight/models_v1_deterministic_2_8_deg.pkl"
|
| 59 |
+
},
|
| 60 |
+
"stochastic_1_4_deg": {
|
| 61 |
+
"type": "stochastic weather forecast",
|
| 62 |
+
"resolution_degrees": 1.4,
|
| 63 |
+
"grid_shape": [
|
| 64 |
+
256,
|
| 65 |
+
128
|
| 66 |
+
],
|
| 67 |
+
"checkpoint": "weight/models_v1_stochastic_1_4_deg.pkl"
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"data": {
|
| 72 |
+
"dataset": "ERA5",
|
| 73 |
+
"source_grid_shape": [
|
| 74 |
+
721,
|
| 75 |
+
1440
|
| 76 |
+
],
|
| 77 |
+
"time_step_hours": 6,
|
| 78 |
+
"input_steps": 1,
|
| 79 |
+
"pressure_levels_hpa": [
|
| 80 |
+
1,
|
| 81 |
+
2,
|
| 82 |
+
3,
|
| 83 |
+
5,
|
| 84 |
+
7,
|
| 85 |
+
10,
|
| 86 |
+
20,
|
| 87 |
+
30,
|
| 88 |
+
50,
|
| 89 |
+
70,
|
| 90 |
+
100,
|
| 91 |
+
125,
|
| 92 |
+
150,
|
| 93 |
+
175,
|
| 94 |
+
200,
|
| 95 |
+
225,
|
| 96 |
+
250,
|
| 97 |
+
300,
|
| 98 |
+
350,
|
| 99 |
+
400,
|
| 100 |
+
450,
|
| 101 |
+
500,
|
| 102 |
+
550,
|
| 103 |
+
600,
|
| 104 |
+
650,
|
| 105 |
+
700,
|
| 106 |
+
750,
|
| 107 |
+
775,
|
| 108 |
+
800,
|
| 109 |
+
825,
|
| 110 |
+
850,
|
| 111 |
+
875,
|
| 112 |
+
900,
|
| 113 |
+
925,
|
| 114 |
+
950,
|
| 115 |
+
975,
|
| 116 |
+
1000
|
| 117 |
+
],
|
| 118 |
+
"input_variables": [
|
| 119 |
+
"geopotential",
|
| 120 |
+
"specific_humidity",
|
| 121 |
+
"temperature",
|
| 122 |
+
"u_component_of_wind",
|
| 123 |
+
"v_component_of_wind"
|
| 124 |
+
],
|
| 125 |
+
"optional_input_variables": [
|
| 126 |
+
"specific_cloud_ice_water_content",
|
| 127 |
+
"specific_cloud_liquid_water_content"
|
| 128 |
+
],
|
| 129 |
+
"forcing_variables": [
|
| 130 |
+
"sea_ice_cover",
|
| 131 |
+
"sea_surface_temperature"
|
| 132 |
+
],
|
| 133 |
+
"protocol": "era5_37_pressure_levels_261_channel"
|
| 134 |
+
},
|
| 135 |
+
"weights": {
|
| 136 |
+
"license": "CC BY-SA 4.0",
|
| 137 |
+
"source": "gs://neuralgcm/models/v1/"
|
| 138 |
+
},
|
| 139 |
+
"configuration_sources": [
|
| 140 |
+
"conf/config.yaml",
|
| 141 |
+
"configuration.json",
|
| 142 |
+
"model/NeuralGCM.py",
|
| 143 |
+
"model/legacy",
|
| 144 |
+
"model/reference_code",
|
| 145 |
+
"scripts/common.py"
|
| 146 |
+
]
|
| 147 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"framework": "JAX",
|
| 3 |
+
"task": "weather_and_climate_simulation",
|
| 4 |
+
"model": "NeuralGCM",
|
| 5 |
+
"input_format": "xarray_pressure_level",
|
| 6 |
+
"protocol": "era5_37_pressure_levels_261_channel",
|
| 7 |
+
"default_config": "conf/config.yaml",
|
| 8 |
+
"train": "scripts/train.py",
|
| 9 |
+
"inference": "scripts/inference.py",
|
| 10 |
+
"evaluation": "scripts/result.py",
|
| 11 |
+
"visualization": "scripts/result.py"
|
| 12 |
+
}
|
model/NeuralGCM.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Official NeuralGCM implementation facade.
|
| 2 |
+
|
| 3 |
+
The upstream legacy model, encoders, decoders, dynamical core and reference
|
| 4 |
+
training utilities are vendored directly under this project's ``model``
|
| 5 |
+
namespace (``model/legacy`` and ``model/reference_code``). This file is the
|
| 6 |
+
single project-facing entry point; no external ``neuralgcm`` source directory
|
| 7 |
+
is required at runtime.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import pickle
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
PROFILE_GIN = {
|
| 18 |
+
"weather_forecast": "deterministic_0_7_deg.gin",
|
| 19 |
+
"climate_scale": "deterministic_1_4_deg.gin",
|
| 20 |
+
"forecast_2_8_deg": "deterministic_2_8_deg.gin",
|
| 21 |
+
"stochastic_1_4_deg": "stochastic_1_4_deg.gin",
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
MODE_ALIASES = {
|
| 25 |
+
"forecast": "weather_forecast",
|
| 26 |
+
"weather_forecast": "weather_forecast",
|
| 27 |
+
"climate": "climate_scale",
|
| 28 |
+
"climate_scale": "climate_scale",
|
| 29 |
+
"forecast_2_8_deg": "forecast_2_8_deg",
|
| 30 |
+
"stochastic_1_4_deg": "stochastic_1_4_deg",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class OfficialNeuralGCMUnavailable(RuntimeError):
|
| 35 |
+
"""Raised when the official runtime package is not available."""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class CheckpointFormatError(ValueError):
|
| 39 |
+
"""Raised when a file is not an official NeuralGCM checkpoint."""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def checkpoint_mode(payload: object) -> str | None:
|
| 43 |
+
"""Infer the project profile declared by an official-format checkpoint."""
|
| 44 |
+
if not isinstance(payload, dict):
|
| 45 |
+
return None
|
| 46 |
+
if payload.get("mode"):
|
| 47 |
+
value = str(payload["mode"])
|
| 48 |
+
return MODE_ALIASES.get(value, value)
|
| 49 |
+
text = str(payload.get("model_config_str", ""))
|
| 50 |
+
if "GridTL255" in text:
|
| 51 |
+
return "weather_forecast"
|
| 52 |
+
if "GridTL63" in text:
|
| 53 |
+
return "forecast_2_8_deg"
|
| 54 |
+
if "GridTL127" in text:
|
| 55 |
+
return "stochastic_1_4_deg" if "FIELD_SUBSET" in text else "climate_scale"
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def validate_checkpoint_mode(payload: object, mode: str, path: str | Path) -> None:
|
| 60 |
+
"""Reject a checkpoint whose grid/profile differs from the requested mode."""
|
| 61 |
+
stored_mode = checkpoint_mode(payload)
|
| 62 |
+
if stored_mode and stored_mode != mode:
|
| 63 |
+
raise ValueError(
|
| 64 |
+
f"Checkpoint {path} is for mode={stored_mode!r}, but mode={mode!r} "
|
| 65 |
+
"was requested. Select the matching mode or checkpoint."
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def parameter_summary(params: Any) -> dict[str, Any]:
|
| 70 |
+
"""Return reproducible parameter count, storage size and dtype statistics."""
|
| 71 |
+
import jax
|
| 72 |
+
|
| 73 |
+
leaves = jax.tree_util.tree_leaves(params)
|
| 74 |
+
array_leaves = [leaf for leaf in leaves if hasattr(leaf, "shape") and hasattr(leaf, "dtype")]
|
| 75 |
+
count = sum(int(np.prod(leaf.shape, dtype=np.int64)) for leaf in array_leaves)
|
| 76 |
+
nbytes = sum(
|
| 77 |
+
int(np.prod(leaf.shape, dtype=np.int64)) * np.dtype(leaf.dtype).itemsize
|
| 78 |
+
for leaf in array_leaves
|
| 79 |
+
)
|
| 80 |
+
dtype_counts: dict[str, int] = {}
|
| 81 |
+
for leaf in array_leaves:
|
| 82 |
+
dtype = str(np.dtype(leaf.dtype))
|
| 83 |
+
dtype_counts[dtype] = dtype_counts.get(dtype, 0) + int(
|
| 84 |
+
np.prod(leaf.shape, dtype=np.int64)
|
| 85 |
+
)
|
| 86 |
+
return {
|
| 87 |
+
"count": count,
|
| 88 |
+
"nbytes": nbytes,
|
| 89 |
+
"leaves": len(array_leaves),
|
| 90 |
+
"dtypes": dtype_counts,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def format_parameter_summary(params: Any) -> str:
|
| 95 |
+
"""Format a compact ``params.count``-style model summary."""
|
| 96 |
+
summary = parameter_summary(params)
|
| 97 |
+
dtype_text = ",".join(
|
| 98 |
+
f"{dtype}:{count:,}" for dtype, count in sorted(summary["dtypes"].items())
|
| 99 |
+
)
|
| 100 |
+
return (
|
| 101 |
+
f"params.count={summary['count']:,} "
|
| 102 |
+
f"params.bytes={summary['nbytes']:,} "
|
| 103 |
+
f"params.mib={summary['nbytes'] / 2**20:.2f} "
|
| 104 |
+
f"params.leaves={summary['leaves']} dtypes={dtype_text}"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def load_checkpoint(path: str | Path):
|
| 109 |
+
"""Load an official checkpoint through the vendored PressureLevelModel."""
|
| 110 |
+
try:
|
| 111 |
+
from model.legacy.api import PressureLevelModel
|
| 112 |
+
except Exception as exc: # pragma: no cover - runtime-dependent
|
| 113 |
+
raise OfficialNeuralGCMUnavailable(
|
| 114 |
+
"Unable to import the vendored NeuralGCM implementation. Check "
|
| 115 |
+
"JAX, Haiku, Gin and Dinosaur dependencies in develop_base."
|
| 116 |
+
) from exc
|
| 117 |
+
path = Path(path)
|
| 118 |
+
if not path.exists():
|
| 119 |
+
raise FileNotFoundError(path)
|
| 120 |
+
with path.open("rb") as handle:
|
| 121 |
+
checkpoint = pickle.load(handle)
|
| 122 |
+
required = {"model_config_str", "aux_ds_dict", "params"}
|
| 123 |
+
if not isinstance(checkpoint, dict) or not required.issubset(checkpoint):
|
| 124 |
+
keys = sorted(checkpoint) if isinstance(checkpoint, dict) else type(checkpoint).__name__
|
| 125 |
+
raise CheckpointFormatError(
|
| 126 |
+
f"{path} is not an official checkpoint; expected keys "
|
| 127 |
+
f"{sorted(required)}, got {keys}"
|
| 128 |
+
)
|
| 129 |
+
return PressureLevelModel.from_checkpoint(checkpoint)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def official_runtime_available() -> bool:
|
| 133 |
+
try:
|
| 134 |
+
from model.legacy.api import PressureLevelModel # noqa: F401
|
| 135 |
+
except Exception:
|
| 136 |
+
return False
|
| 137 |
+
return True
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def build_from_scratch(dataset, mode: str):
|
| 141 |
+
"""Build the public WhirlModel used for random parameter initialization.
|
| 142 |
+
|
| 143 |
+
Parameter initialization itself needs a concrete trajectory and is performed
|
| 144 |
+
by ``scripts/train.py`` through the returned model's Haiku rollout function.
|
| 145 |
+
This compatibility facade deliberately does not import the unreleased Google
|
| 146 |
+
experiment runner.
|
| 147 |
+
"""
|
| 148 |
+
return build_training_model(dataset, mode)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def build_training_model(dataset, mode: str):
|
| 152 |
+
"""Build an official ``WhirlModel`` from the fused Gin profile."""
|
| 153 |
+
if mode not in PROFILE_GIN:
|
| 154 |
+
raise ValueError(f"Unknown NeuralGCM mode {mode!r}")
|
| 155 |
+
import gin
|
| 156 |
+
from model.legacy import model_builder
|
| 157 |
+
|
| 158 |
+
config_path = Path(__file__).resolve().parent / "reference_code" / "paper_configs" / PROFILE_GIN[mode]
|
| 159 |
+
gin_text = config_path.read_text(encoding="utf-8")
|
| 160 |
+
# The released Gin profiles use ``orography_data_path = None`` and rely on
|
| 161 |
+
# the official xarray auxiliary-dataset escape hatch for static fields.
|
| 162 |
+
# ``get_whirl_model`` normally obtains this from dataset metadata; supply it
|
| 163 |
+
# explicitly for synthetic/OneScience datasets that have no metadata attrs.
|
| 164 |
+
from dinosaur import xarray_utils
|
| 165 |
+
try:
|
| 166 |
+
aux_features = xarray_utils.aux_features_from_xarray(dataset)
|
| 167 |
+
except (KeyError, AttributeError):
|
| 168 |
+
aux_features = {}
|
| 169 |
+
aux_features[xarray_utils.XARRAY_DS_KEY] = dataset
|
| 170 |
+
dataset = dataset.copy()
|
| 171 |
+
dataset.attrs = dict(dataset.attrs)
|
| 172 |
+
dataset.attrs[xarray_utils.XR_AUX_FEATURES_LIST_KEY] = ",".join(
|
| 173 |
+
key for key in aux_features if key != xarray_utils.XARRAY_DS_KEY
|
| 174 |
+
)
|
| 175 |
+
# get_whirl_model reads serializable aux variables from attrs. Injecting the
|
| 176 |
+
# xarray dataset directly is handled below through a temporary wrapper.
|
| 177 |
+
original = model_builder.xarray_utils.aux_features_from_xarray
|
| 178 |
+
model_builder.xarray_utils.aux_features_from_xarray = lambda _: aux_features
|
| 179 |
+
try:
|
| 180 |
+
model = model_builder.get_whirl_model(dataset, gin_text)
|
| 181 |
+
finally:
|
| 182 |
+
model_builder.xarray_utils.aux_features_from_xarray = original
|
| 183 |
+
# The profile's xarray conversion callbacks are configured through Gin;
|
| 184 |
+
# get_whirl_model returns the fully bound model object.
|
| 185 |
+
return model, gin_text
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def make_rollout_functions(
|
| 189 |
+
whirl_model, trajectory_length: int, *, inner_steps: int = 1
|
| 190 |
+
):
|
| 191 |
+
"""Return Haiku init/apply functions using the official rollout helpers."""
|
| 192 |
+
import haiku as hk
|
| 193 |
+
from model.legacy import model_utils
|
| 194 |
+
|
| 195 |
+
@hk.transform
|
| 196 |
+
def rollout_fn(target, forcing):
|
| 197 |
+
model = whirl_model.model_cls()
|
| 198 |
+
trajectory_fn = model_utils.trajectory_with_inputs_and_forcing(
|
| 199 |
+
model, num_init_frames=1, start_with_input=True
|
| 200 |
+
)
|
| 201 |
+
_, predicted = trajectory_fn(
|
| 202 |
+
target,
|
| 203 |
+
forcing,
|
| 204 |
+
outer_steps=trajectory_length,
|
| 205 |
+
inner_steps=inner_steps,
|
| 206 |
+
)
|
| 207 |
+
return model_utils.compute_prediction_and_target_representations(
|
| 208 |
+
predicted, target, forcing, model
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
return rollout_fn
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def save_official_checkpoint(path: str | Path, params: Any, dataset, model_config_str: str, *, metadata: dict[str, Any] | None = None):
|
| 215 |
+
"""Write a checkpoint consumable by ``PressureLevelModel.from_checkpoint``."""
|
| 216 |
+
path = Path(path)
|
| 217 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 218 |
+
payload = {
|
| 219 |
+
"model_config_str": model_config_str,
|
| 220 |
+
"aux_ds_dict": dataset.to_dict(),
|
| 221 |
+
"params": params,
|
| 222 |
+
}
|
| 223 |
+
if metadata:
|
| 224 |
+
payload.update(metadata)
|
| 225 |
+
with path.open("wb") as handle:
|
| 226 |
+
pickle.dump(payload, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
| 227 |
+
return path
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
NeuralGCMAdapter = load_checkpoint
|
model/NeuralGCM_demo.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import importlib.resources
|
| 15 |
+
import pickle
|
| 16 |
+
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import horizontal_interpolation
|
| 19 |
+
from dinosaur import spherical_harmonic
|
| 20 |
+
import model as neuralgcm
|
| 21 |
+
import numpy as np
|
| 22 |
+
import xarray
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _horizontal_regrid(
|
| 26 |
+
regridder: horizontal_interpolation.Regridder, dataset: xarray.Dataset
|
| 27 |
+
) -> xarray.Dataset:
|
| 28 |
+
"""Horizontally regrid an xarray Dataset."""
|
| 29 |
+
# TODO(shoyer): consider moving to public API
|
| 30 |
+
regridded = xarray.apply_ufunc(
|
| 31 |
+
regridder,
|
| 32 |
+
dataset,
|
| 33 |
+
input_core_dims=[['longitude', 'latitude']],
|
| 34 |
+
output_core_dims=[['longitude', 'latitude']],
|
| 35 |
+
exclude_dims={'longitude', 'latitude'},
|
| 36 |
+
vectorize=True, # loops over level, for lower memory usage
|
| 37 |
+
)
|
| 38 |
+
regridded.coords['longitude'] = np.rad2deg(regridder.target_grid.longitudes)
|
| 39 |
+
regridded.coords['latitude'] = np.rad2deg(regridder.target_grid.latitudes)
|
| 40 |
+
return regridded
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_checkpoint_tl63_stochastic():
|
| 44 |
+
"""Load a checkpoint for a toy TL63 stochastic model."""
|
| 45 |
+
package = importlib.resources.files(neuralgcm)
|
| 46 |
+
file = package.joinpath('data/tl63_stochastic_mini.pkl')
|
| 47 |
+
return pickle.loads(file.read_bytes())
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def load_data(coords: coordinate_systems.CoordinateSystem) -> xarray.Dataset:
|
| 51 |
+
"""Load demo data for the given coordinate system."""
|
| 52 |
+
if coords.vertical.layers != 37:
|
| 53 |
+
raise ValueError('can only load demo data for 37 pressure levels')
|
| 54 |
+
package = importlib.resources.files(neuralgcm)
|
| 55 |
+
with package.joinpath('data/era5_tl31_19590102T00.nc').open('rb') as f:
|
| 56 |
+
ds = xarray.load_dataset(f).expand_dims('time')
|
| 57 |
+
regridder = horizontal_interpolation.ConservativeRegridder(
|
| 58 |
+
spherical_harmonic.Grid.TL31(), coords.horizontal
|
| 59 |
+
)
|
| 60 |
+
return _horizontal_regrid(regridder, ds)
|
model/data/era5_tl31_19590102T00.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:18f66e795af9f564a2b6e0d861b9a51e74ce831a82675b47ff957be709554a5e
|
| 3 |
+
size 2141788
|
model/data/tl63_stochastic_mini.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:de0f8e8cb31ecca59469104440808a3ea8e31418d5a8a48220e3c1fa64303baa
|
| 3 |
+
size 1030438
|
model/legacy/api.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Public API for NeuralGCM models."""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from collections import abc
|
| 18 |
+
import datetime
|
| 19 |
+
import functools
|
| 20 |
+
from typing import Any, Callable
|
| 21 |
+
|
| 22 |
+
from dinosaur import coordinate_systems
|
| 23 |
+
from dinosaur import scales
|
| 24 |
+
from dinosaur import time_integration
|
| 25 |
+
from dinosaur import typing
|
| 26 |
+
from dinosaur import xarray_utils
|
| 27 |
+
import jax
|
| 28 |
+
from jax import tree_util
|
| 29 |
+
import jax.numpy as jnp
|
| 30 |
+
from model.legacy import gin_utils
|
| 31 |
+
from model.legacy import model_builder
|
| 32 |
+
from model.legacy import physics_specifications
|
| 33 |
+
import numpy as np
|
| 34 |
+
import pandas as pd
|
| 35 |
+
import xarray
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
ArrayLike = float | np.ndarray | jax.Array
|
| 39 |
+
Params = dict[str, dict[str, ArrayLike]]
|
| 40 |
+
TimedeltaLike = str | np.timedelta64 | pd.Timestamp | datetime.timedelta
|
| 41 |
+
Numeric = float | np.ndarray | jax.Array | xarray.DataArray
|
| 42 |
+
|
| 43 |
+
# TODO(shoyer): make these types more precise
|
| 44 |
+
Inputs = dict[str, ArrayLike]
|
| 45 |
+
Forcings = dict[str, ArrayLike]
|
| 46 |
+
TemporalForcings = dict[str, ArrayLike]
|
| 47 |
+
Outputs = dict[str, jax.Array]
|
| 48 |
+
BatchedOutputs = dict[str, jax.Array]
|
| 49 |
+
State = Any
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _sim_time_from_state(state: State) -> jax.Array:
|
| 53 |
+
"""Extract sim_time from model state."""
|
| 54 |
+
# TODO(shoyer): eliminate whichever of these two cases is no longer needed!
|
| 55 |
+
# TODO(shoyer): consider renaming `sim_time` to `time`?
|
| 56 |
+
if isinstance(state, typing.ModelState):
|
| 57 |
+
sim_time = getattr(state.state, 'sim_time', None)
|
| 58 |
+
else:
|
| 59 |
+
sim_time = getattr(state, 'sim_time', None)
|
| 60 |
+
return sim_time # pyrefly: ignore[bad-return]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _calculate_sub_steps(
|
| 64 |
+
timestep: np.timedelta64, duration: TimedeltaLike
|
| 65 |
+
) -> int:
|
| 66 |
+
"""Calculate the number of time-steps required to simulate a time interval."""
|
| 67 |
+
duration = pd.Timedelta(duration)
|
| 68 |
+
time_step_ratio = duration / timestep
|
| 69 |
+
if abs(time_step_ratio - round(time_step_ratio)) > 1e-6:
|
| 70 |
+
raise ValueError(
|
| 71 |
+
f'non-integral time-step ratio: {duration=} is not a multiple of '
|
| 72 |
+
f'the internal model timestep {timestep}'
|
| 73 |
+
)
|
| 74 |
+
return round(time_step_ratio)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _prepend_dummy_time_axis(state: typing.Pytree) -> typing.Pytree:
|
| 78 |
+
return tree_util.tree_map(lambda x: jnp.expand_dims(x, axis=0), state)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _static_gin_config(method):
|
| 82 |
+
"""Decorator to add static gin config to a method."""
|
| 83 |
+
|
| 84 |
+
@functools.wraps(method)
|
| 85 |
+
def _method(self, *args, **kwargs):
|
| 86 |
+
with gin_utils.specific_config(self.gin_config):
|
| 87 |
+
return method(self, *args, **kwargs)
|
| 88 |
+
|
| 89 |
+
return _method
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _check_variables(
|
| 93 |
+
dataset: xarray.Dataset,
|
| 94 |
+
desired_level_variables: abc.Sequence[str] = (),
|
| 95 |
+
desired_surface_variables: abc.Sequence[str] = (),
|
| 96 |
+
):
|
| 97 |
+
"""Checks that a dataset has the desired variables."""
|
| 98 |
+
T, Z, X, Y = ('time', 'level', 'longitude', 'latitude') # pylint: disable=invalid-name
|
| 99 |
+
|
| 100 |
+
for k in desired_level_variables:
|
| 101 |
+
if k not in dataset.data_vars:
|
| 102 |
+
raise ValueError(f'expected variable {k} not found')
|
| 103 |
+
dims = dataset[k].dims
|
| 104 |
+
if not (set(dims) == {Z, X, Y} or set(dims) == {T, Z, X, Y}):
|
| 105 |
+
raise ValueError(
|
| 106 |
+
f'expected variable {k} to have dims {(Z, X, Y)} or {(T, Z, X, Y)},'
|
| 107 |
+
f' but got {dims}'
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
for k in desired_surface_variables:
|
| 111 |
+
if k not in dataset.data_vars:
|
| 112 |
+
raise ValueError(f'expected variable {k} not found')
|
| 113 |
+
dims = dataset[k].dims
|
| 114 |
+
if not (set(dims) == {X, Y} or set(dims) == {T, X, Y}):
|
| 115 |
+
raise ValueError(
|
| 116 |
+
f'expected variable {k} to have dims {(X, Y)} or {(T, X, Y)},'
|
| 117 |
+
f' but got {dims}'
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _check_coords(
|
| 122 |
+
actual_coords: coordinate_systems.CoordinateSystem,
|
| 123 |
+
desired_coords: coordinate_systems.CoordinateSystem,
|
| 124 |
+
) -> None:
|
| 125 |
+
"""Checks that a dataset has the desired coordinates."""
|
| 126 |
+
if not np.allclose(
|
| 127 |
+
actual := actual_coords.horizontal.longitudes,
|
| 128 |
+
desired := desired_coords.horizontal.longitudes,
|
| 129 |
+
atol=1e-3,
|
| 130 |
+
):
|
| 131 |
+
raise ValueError(f'longitude coordinate mismatch: {actual=}, {desired=}')
|
| 132 |
+
|
| 133 |
+
if not np.allclose(
|
| 134 |
+
actual := actual_coords.horizontal.latitudes,
|
| 135 |
+
desired := desired_coords.horizontal.latitudes,
|
| 136 |
+
atol=1e-3,
|
| 137 |
+
):
|
| 138 |
+
raise ValueError(f'latitude coordinate mismatch: {actual=}, {desired=}')
|
| 139 |
+
|
| 140 |
+
if actual_coords.vertical is not None and not np.allclose(
|
| 141 |
+
actual := actual_coords.vertical.centers,
|
| 142 |
+
desired := desired_coords.vertical.centers,
|
| 143 |
+
atol=1e-3,
|
| 144 |
+
):
|
| 145 |
+
raise ValueError(
|
| 146 |
+
f'pressure level coordinate mismatch: {actual=}, {desired=}'
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _rename_if_found(
|
| 151 |
+
dataset: xarray.Dataset, names: dict[str, str]
|
| 152 |
+
) -> xarray.Dataset:
|
| 153 |
+
return dataset.rename({k: v for k, v in names.items() if k in dataset})
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
_ABBREVIATED_NAMES = {
|
| 157 |
+
'u_component_of_wind': 'u',
|
| 158 |
+
'v_component_of_wind': 'v',
|
| 159 |
+
'geopotential': 'z',
|
| 160 |
+
'temperature': 't',
|
| 161 |
+
'longitude': 'lon',
|
| 162 |
+
'latitude': 'lat',
|
| 163 |
+
}
|
| 164 |
+
_FULL_NAMES = {v: k for k, v in _ABBREVIATED_NAMES.items()}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _expand_tracers(inputs: dict) -> dict:
|
| 168 |
+
inputs = inputs.copy()
|
| 169 |
+
inputs.update(inputs.pop('tracers'))
|
| 170 |
+
assert not inputs['diagnostics']
|
| 171 |
+
del inputs['diagnostics']
|
| 172 |
+
return inputs
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@tree_util.register_pytree_node_class
|
| 176 |
+
class PressureLevelModel:
|
| 177 |
+
"""Inference-only API for models that predict dense data on pressure levels.
|
| 178 |
+
|
| 179 |
+
These models are trained on ECMWF ERA5 data on pressure-levels as stored in
|
| 180 |
+
the Copernicus Data Store.
|
| 181 |
+
|
| 182 |
+
This class encapsulates the details of defining models (e.g., with Haiku) and
|
| 183 |
+
hence should remain stable even for future NeuralGCM models.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
def __init__(
|
| 187 |
+
self,
|
| 188 |
+
structure: model_builder.WhirlModel,
|
| 189 |
+
params: Params,
|
| 190 |
+
gin_config: str,
|
| 191 |
+
):
|
| 192 |
+
self._structure = structure
|
| 193 |
+
self._params = params
|
| 194 |
+
self.gin_config = gin_config
|
| 195 |
+
|
| 196 |
+
self._tracer_variables = [
|
| 197 |
+
'specific_humidity',
|
| 198 |
+
]
|
| 199 |
+
self._input_variables = [
|
| 200 |
+
'geopotential',
|
| 201 |
+
'specific_humidity',
|
| 202 |
+
'temperature',
|
| 203 |
+
'u_component_of_wind',
|
| 204 |
+
'v_component_of_wind',
|
| 205 |
+
]
|
| 206 |
+
# Some old model versions do not use cloud variables.
|
| 207 |
+
# TODO(shoyer): remove this once all integration tests are updated.
|
| 208 |
+
cloud_variables = [
|
| 209 |
+
'specific_cloud_ice_water_content',
|
| 210 |
+
'specific_cloud_liquid_water_content',
|
| 211 |
+
]
|
| 212 |
+
for variable in cloud_variables:
|
| 213 |
+
if variable in self.gin_config:
|
| 214 |
+
self._tracer_variables.append(variable)
|
| 215 |
+
self._input_variables.append(variable)
|
| 216 |
+
|
| 217 |
+
self._forcing_variables = [
|
| 218 |
+
'sea_ice_cover',
|
| 219 |
+
'sea_surface_temperature',
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
def __repr__(self):
|
| 223 |
+
return (
|
| 224 |
+
f'{self.__class__.__name__}(structure={self._structure},'
|
| 225 |
+
f' params={self._params})'
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
@property
|
| 229 |
+
def params(self) -> Params:
|
| 230 |
+
return self._params
|
| 231 |
+
|
| 232 |
+
def tree_flatten(self):
|
| 233 |
+
leaves, params_def = tree_util.tree_flatten(self.params)
|
| 234 |
+
return (leaves, (params_def, self._structure, self.gin_config))
|
| 235 |
+
|
| 236 |
+
@classmethod
|
| 237 |
+
def tree_unflatten(cls, aux_data, leaves):
|
| 238 |
+
params_def, structure, gin_config = aux_data
|
| 239 |
+
params = tree_util.tree_unflatten(params_def, leaves)
|
| 240 |
+
return cls(structure, params, gin_config)
|
| 241 |
+
|
| 242 |
+
@property
|
| 243 |
+
def input_variables(self) -> list[str]:
|
| 244 |
+
"""List of variable names required in `inputs` by this model."""
|
| 245 |
+
return list(self._input_variables)
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
def forcing_variables(self) -> list[str]:
|
| 249 |
+
"""List of variable names required in `forcings` by this model."""
|
| 250 |
+
return list(self._forcing_variables)
|
| 251 |
+
|
| 252 |
+
@property
|
| 253 |
+
def timestep(self) -> np.timedelta64:
|
| 254 |
+
"""Spacing between internal model timesteps."""
|
| 255 |
+
to_timedelta = (
|
| 256 |
+
self._structure.specs.physics_specs.dimensionalize_timedelta64
|
| 257 |
+
)
|
| 258 |
+
return to_timedelta(self._structure.specs.dt)
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def data_coords(self) -> coordinate_systems.CoordinateSystem:
|
| 262 |
+
"""Coordinate system for input and output data."""
|
| 263 |
+
return self._structure.data_coords
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def model_coords(self) -> coordinate_systems.CoordinateSystem:
|
| 267 |
+
"""Coordinate system for internal model state."""
|
| 268 |
+
return self._structure.coords
|
| 269 |
+
|
| 270 |
+
def _check_coords(self, dataset: xarray.Dataset):
|
| 271 |
+
dataset_coords = model_builder.coordinate_system_from_dataset(dataset)
|
| 272 |
+
_check_coords(dataset_coords, self.data_coords)
|
| 273 |
+
|
| 274 |
+
def _dataset_with_sim_time(self, dataset: xarray.Dataset) -> xarray.Dataset:
|
| 275 |
+
ref_datetime = self._structure.specs.aux_features['reference_datetime']
|
| 276 |
+
return xarray_utils.ds_with_sim_time(
|
| 277 |
+
dataset,
|
| 278 |
+
self._structure.specs.physics_specs,
|
| 279 |
+
reference_datetime=ref_datetime,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
def _to_abbreviated_names_and_tracers(self, inputs: dict) -> dict:
|
| 283 |
+
inputs = {_ABBREVIATED_NAMES.get(k, k): v for k, v in inputs.items()}
|
| 284 |
+
inputs['tracers'] = {
|
| 285 |
+
k: inputs.pop(k) for k in self._tracer_variables if k in inputs
|
| 286 |
+
}
|
| 287 |
+
inputs['diagnostics'] = {}
|
| 288 |
+
return inputs
|
| 289 |
+
|
| 290 |
+
def _from_abbreviated_names_and_tracers(self, outputs: dict) -> dict:
|
| 291 |
+
outputs = {_FULL_NAMES.get(k, k): v for k, v in outputs.items()}
|
| 292 |
+
outputs |= outputs.pop('tracers')
|
| 293 |
+
outputs |= outputs.pop('diagnostics')
|
| 294 |
+
return outputs
|
| 295 |
+
|
| 296 |
+
def to_nondim_units(self, value: Numeric, units: str) -> Numeric:
|
| 297 |
+
"""Scale a value to the model's internal non-dimensional units."""
|
| 298 |
+
scale_ = self._structure.specs.physics_specs.scale
|
| 299 |
+
units_ = scales.parse_units(units)
|
| 300 |
+
return scale_.nondimensionalize(value * units_)
|
| 301 |
+
|
| 302 |
+
def from_nondim_units(self, value: Numeric, units: str) -> Numeric:
|
| 303 |
+
"""Scale a value from the model's internal non-dimensional units."""
|
| 304 |
+
scale_ = self._structure.specs.physics_specs.scale
|
| 305 |
+
units_ = scales.parse_units(units)
|
| 306 |
+
return scale_.dimensionalize(value, units_).magnitude
|
| 307 |
+
|
| 308 |
+
def datetime64_to_sim_time(self, datetime64: np.ndarray) -> np.ndarray:
|
| 309 |
+
"""Converts a datetime64 array to sim_time."""
|
| 310 |
+
ref_datetime = self._structure.specs.aux_features['reference_datetime']
|
| 311 |
+
return xarray_utils.datetime64_to_nondim_time(
|
| 312 |
+
datetime64,
|
| 313 |
+
self._structure.specs.physics_specs,
|
| 314 |
+
reference_datetime=ref_datetime,
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
def sim_time_to_datetime64(self, sim_time: np.ndarray) -> np.ndarray:
|
| 318 |
+
"""Converts a sim_time array to datetime64."""
|
| 319 |
+
ref_datetime = self._structure.specs.aux_features['reference_datetime']
|
| 320 |
+
return xarray_utils.nondim_time_to_datetime64(
|
| 321 |
+
sim_time,
|
| 322 |
+
self._structure.specs.physics_specs,
|
| 323 |
+
reference_datetime=ref_datetime,
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
def _data_from_xarray(
|
| 327 |
+
self, dataset: xarray.Dataset, variables: list[str]
|
| 328 |
+
) -> dict[str, np.ndarray]:
|
| 329 |
+
self._check_coords(dataset)
|
| 330 |
+
dataset = dataset[variables]
|
| 331 |
+
dataset = self._dataset_with_sim_time(dataset)
|
| 332 |
+
dataset = _rename_if_found(dataset, {'longitude': 'lon', 'latitude': 'lat'})
|
| 333 |
+
return xarray_utils.xarray_to_data_dict(dataset)
|
| 334 |
+
|
| 335 |
+
def inputs_from_xarray(
|
| 336 |
+
self, dataset: xarray.Dataset
|
| 337 |
+
) -> dict[str, np.ndarray]:
|
| 338 |
+
"""Extract inputs from an xarray.Dataset."""
|
| 339 |
+
_check_variables(dataset, desired_level_variables=self._input_variables)
|
| 340 |
+
return self._data_from_xarray(dataset, self._input_variables)
|
| 341 |
+
|
| 342 |
+
def forcings_from_xarray(
|
| 343 |
+
self, dataset: xarray.Dataset
|
| 344 |
+
) -> dict[str, np.ndarray]:
|
| 345 |
+
"""Extract forcings from an xarray.Dataset."""
|
| 346 |
+
_check_variables(dataset, desired_surface_variables=self._forcing_variables)
|
| 347 |
+
return self._data_from_xarray(dataset, self._forcing_variables)
|
| 348 |
+
|
| 349 |
+
def data_from_xarray(
|
| 350 |
+
self, dataset: xarray.Dataset
|
| 351 |
+
) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]:
|
| 352 |
+
"""Extracts data and forcings from an xarray.Dataset."""
|
| 353 |
+
inputs = self.inputs_from_xarray(dataset)
|
| 354 |
+
forcings = self.forcings_from_xarray(dataset)
|
| 355 |
+
return (inputs, forcings)
|
| 356 |
+
|
| 357 |
+
def data_to_xarray(
|
| 358 |
+
self,
|
| 359 |
+
data: dict[str, ArrayLike],
|
| 360 |
+
times: np.ndarray | None,
|
| 361 |
+
decoded: bool = True,
|
| 362 |
+
) -> xarray.Dataset:
|
| 363 |
+
"""Converts decoded model predictions to xarray.Dataset format.
|
| 364 |
+
|
| 365 |
+
Args:
|
| 366 |
+
data: dict of arrays with shapes matching input/outputs or encoded model
|
| 367 |
+
state for this model, i.e., with shape `([time,] level, longitude,
|
| 368 |
+
latitude)`, where `[time,]` indicates an optional leading time
|
| 369 |
+
dimension.
|
| 370 |
+
times: either `None` indicating no leading time dimension on any
|
| 371 |
+
variables, or a coordinate array of times with shape `(time,)`.
|
| 372 |
+
decoded: if `True`, use `self.data_coords` to determine the output
|
| 373 |
+
coordinates; otherwise use `self.model_coords`.
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
An xarray.Dataset with appropriate coordinates and dimensions.
|
| 377 |
+
"""
|
| 378 |
+
coords = self.data_coords if decoded else self.model_coords
|
| 379 |
+
dataset = xarray_utils.data_to_xarray(data, coords=coords, times=times)
|
| 380 |
+
dataset = _rename_if_found(dataset, {'lon': 'longitude', 'lat': 'latitude'})
|
| 381 |
+
return dataset
|
| 382 |
+
|
| 383 |
+
def _squeeze_level_from_forcings(self, forcings: Forcings) -> Forcings:
|
| 384 |
+
# Due to a bug in xarray_to_dynamic_covariate_data, we were accidentally
|
| 385 |
+
# not inserting a level dimension in forcings.
|
| 386 |
+
forcings = dict(forcings)
|
| 387 |
+
for k in self._forcing_variables:
|
| 388 |
+
if k in forcings:
|
| 389 |
+
assert isinstance(forcings[k], (np.ndarray, jax.Array))
|
| 390 |
+
forcings[k] = forcings[k].squeeze(axis=-3) # pyrefly: ignore[missing-attribute]
|
| 391 |
+
return forcings
|
| 392 |
+
|
| 393 |
+
@jax.jit
|
| 394 |
+
@_static_gin_config
|
| 395 |
+
def encode(
|
| 396 |
+
self,
|
| 397 |
+
inputs: Inputs,
|
| 398 |
+
forcings: Forcings,
|
| 399 |
+
rng_key: typing.PRNGKeyArray | None = None,
|
| 400 |
+
) -> State:
|
| 401 |
+
"""Encode from pressure-level inputs & forcings to model state.
|
| 402 |
+
|
| 403 |
+
Args:
|
| 404 |
+
inputs: input data on pressure-levels, as a dict where each entry is an
|
| 405 |
+
array with shape `[level, longitude, latitude]` matching `data_coords`.
|
| 406 |
+
forcings: forcing data on pressure-levels, as a dict where each entry is
|
| 407 |
+
an array with shape `[level, longitude, latitude]` matching
|
| 408 |
+
`data_coords`. Single level data (e.g., sea surface temperature) should
|
| 409 |
+
have a `level` dimension of size 1.
|
| 410 |
+
rng_key: optional JAX RNG key to use for encoding the state. Required if
|
| 411 |
+
using stochastic models, otherwise ignored.
|
| 412 |
+
|
| 413 |
+
Returns:
|
| 414 |
+
Dynamical core state on sigma levels, where all arrays have dimensions
|
| 415 |
+
`[level, zonal_wavenumber, total_wavenumber]` matching `model_coords`.
|
| 416 |
+
"""
|
| 417 |
+
sim_time = inputs['sim_time']
|
| 418 |
+
inputs = self._to_abbreviated_names_and_tracers(inputs)
|
| 419 |
+
inputs = _prepend_dummy_time_axis(inputs)
|
| 420 |
+
forcings = self._squeeze_level_from_forcings(forcings)
|
| 421 |
+
forcings = _prepend_dummy_time_axis(forcings)
|
| 422 |
+
f = self._structure.forcing_fn(self.params, None, forcings, sim_time)
|
| 423 |
+
return self._structure.encode_fn(self.params, rng_key, inputs, f)
|
| 424 |
+
|
| 425 |
+
@jax.jit
|
| 426 |
+
@_static_gin_config
|
| 427 |
+
def advance(self, state: State, forcings: Forcings) -> State:
|
| 428 |
+
"""Advance model state one timestep forward.
|
| 429 |
+
|
| 430 |
+
Args:
|
| 431 |
+
state: dynamical core state on sigma levels, where all arrays have
|
| 432 |
+
dimensions `[level, zonal_wavenumber, total_wavenumber]` matching
|
| 433 |
+
`model_coords`
|
| 434 |
+
forcings: forcing data on pressure-levels, as a dict where each entry is
|
| 435 |
+
an array with shape `[level, longitude, latitude]` matching
|
| 436 |
+
`data_coords`. Single level data (e.g., sea surface temperature) should
|
| 437 |
+
have a `level` dimension of size 1.
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
State advanced one time-step forward.
|
| 441 |
+
"""
|
| 442 |
+
sim_time = _sim_time_from_state(state)
|
| 443 |
+
forcings = self._squeeze_level_from_forcings(forcings)
|
| 444 |
+
forcings = _prepend_dummy_time_axis(forcings)
|
| 445 |
+
f = self._structure.forcing_fn(self.params, None, forcings, sim_time)
|
| 446 |
+
state = self._structure.advance_fn(self.params, None, state, f)
|
| 447 |
+
return state
|
| 448 |
+
|
| 449 |
+
@jax.jit
|
| 450 |
+
@_static_gin_config
|
| 451 |
+
def decode(self, state: State, forcings: Forcings) -> Outputs:
|
| 452 |
+
"""Decode from model state to pressure-level outputs.
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
state: dynamical core state on sigma levels, where all arrays have
|
| 456 |
+
dimensions `[level, zonal_wavenumber, total_wavenumber]` matching
|
| 457 |
+
`model_coords`.
|
| 458 |
+
forcings: forcing data on pressure-levels, as a dict where each entry is
|
| 459 |
+
an array with shape `[level, longitude, latitude]` matching
|
| 460 |
+
`data_coords`. Single level data (e.g., sea surface temperature) should
|
| 461 |
+
have a `level` dimension of size 1.
|
| 462 |
+
|
| 463 |
+
Returns:
|
| 464 |
+
Outputs on pressure-levels, as a dict where each entry is an array with
|
| 465 |
+
shape `[level, longitude, latitude]` matching `data_coords`.
|
| 466 |
+
"""
|
| 467 |
+
sim_time = _sim_time_from_state(state)
|
| 468 |
+
forcings = self._squeeze_level_from_forcings(forcings)
|
| 469 |
+
forcings = _prepend_dummy_time_axis(forcings)
|
| 470 |
+
f = self._structure.forcing_fn(self.params, None, forcings, sim_time)
|
| 471 |
+
outputs = self._structure.decode_fn(self.params, None, state, f)
|
| 472 |
+
outputs = self._from_abbreviated_names_and_tracers(outputs)
|
| 473 |
+
return outputs
|
| 474 |
+
|
| 475 |
+
@functools.partial(
|
| 476 |
+
jax.jit,
|
| 477 |
+
static_argnames=[
|
| 478 |
+
'steps',
|
| 479 |
+
'timedelta',
|
| 480 |
+
'start_with_input',
|
| 481 |
+
'post_process_fn',
|
| 482 |
+
],
|
| 483 |
+
)
|
| 484 |
+
@_static_gin_config
|
| 485 |
+
def unroll(
|
| 486 |
+
self,
|
| 487 |
+
state: State,
|
| 488 |
+
forcings: TemporalForcings,
|
| 489 |
+
*,
|
| 490 |
+
steps: int,
|
| 491 |
+
timedelta: TimedeltaLike | None = None,
|
| 492 |
+
start_with_input: bool = False,
|
| 493 |
+
post_process_fn: Callable[[State], Any] | None = None,
|
| 494 |
+
) -> tuple[State, BatchedOutputs]:
|
| 495 |
+
"""Unroll predictions over many time-steps.
|
| 496 |
+
|
| 497 |
+
Usage:
|
| 498 |
+
|
| 499 |
+
advanced_state, outputs = model.unroll(state, forcings, steps=N)
|
| 500 |
+
|
| 501 |
+
where ``advanced_state`` is the advanced model state after ``N`` steps and
|
| 502 |
+
``outputs`` is a trajectory of decoded states on pressure-levels with a
|
| 503 |
+
leading dimension of size ``N``.
|
| 504 |
+
|
| 505 |
+
Args:
|
| 506 |
+
state: initial model state.
|
| 507 |
+
forcings: forcing data over the time-period spanned by the desired output
|
| 508 |
+
trajectory. Should include a leading time-axis, but times can be at any
|
| 509 |
+
desired granularity (e.g., it should be fine to supply daily forcing
|
| 510 |
+
data, even if producing hourly outputs). The nearest forcing in time
|
| 511 |
+
will be used for each internal ``advance()`` and ``decode()`` call.
|
| 512 |
+
steps: number of time-steps to take.
|
| 513 |
+
timedelta: size of each time-step to take, which must be a multiple of the
|
| 514 |
+
internal model timestep. By default uses the internal model timestep.
|
| 515 |
+
start_with_input: if ``True``, outputs are at times ``[0, ..., (steps - 1)
|
| 516 |
+
* timestep]`` relative to the initial time; if ``False``, outputs are at
|
| 517 |
+
times ``[timestep, ..., steps * timestep]``.
|
| 518 |
+
post_process_fn: optional function to apply to each advanced state and
|
| 519 |
+
current forcings to create outputs like ``post_process_fn(state,
|
| 520 |
+
forcings)``, where ``forcings`` does not include a time axis. By
|
| 521 |
+
default, uses ``model.decode``.
|
| 522 |
+
|
| 523 |
+
Returns:
|
| 524 |
+
A tuple of the advanced state at time ``steps * timestamp``, and outputs
|
| 525 |
+
with a leading ``time`` axis at the time-steps specified by ``steps``,
|
| 526 |
+
``timedelta`` and ``start_with_input``.
|
| 527 |
+
"""
|
| 528 |
+
if timedelta is None:
|
| 529 |
+
timedelta = self.timestep
|
| 530 |
+
|
| 531 |
+
def get_nearest_forcings(sim_time):
|
| 532 |
+
times = forcings['sim_time']
|
| 533 |
+
assert isinstance(times, jax.Array)
|
| 534 |
+
approx_index = jnp.interp(sim_time, times, jnp.arange(times.size))
|
| 535 |
+
index = jnp.round(approx_index).astype(jnp.int32)
|
| 536 |
+
return jax.tree.map(lambda x: x[index, ...], forcings)
|
| 537 |
+
|
| 538 |
+
def with_nearest_forcings(func):
|
| 539 |
+
def wrapped(state):
|
| 540 |
+
sim_time = _sim_time_from_state(state)
|
| 541 |
+
forcings = get_nearest_forcings(sim_time)
|
| 542 |
+
return func(state, forcings)
|
| 543 |
+
|
| 544 |
+
return wrapped
|
| 545 |
+
|
| 546 |
+
if post_process_fn is None:
|
| 547 |
+
post_process_fn = self.decode
|
| 548 |
+
|
| 549 |
+
inner_steps = _calculate_sub_steps(self.timestep, timedelta)
|
| 550 |
+
trajectory_func = time_integration.trajectory_from_step(
|
| 551 |
+
with_nearest_forcings(self.advance),
|
| 552 |
+
outer_steps=steps,
|
| 553 |
+
inner_steps=inner_steps,
|
| 554 |
+
start_with_input=start_with_input,
|
| 555 |
+
post_process_fn=with_nearest_forcings(post_process_fn),
|
| 556 |
+
)
|
| 557 |
+
state, outputs = trajectory_func(state)
|
| 558 |
+
return state, outputs
|
| 559 |
+
|
| 560 |
+
@classmethod
|
| 561 |
+
def from_checkpoint(cls, checkpoint: Any) -> PressureLevelModel:
|
| 562 |
+
"""Creates a PressureLevelModel from a checkpoint.
|
| 563 |
+
|
| 564 |
+
Args:
|
| 565 |
+
checkpoint: dictionary with keys "model_config_str", "aux_ds_dict" and
|
| 566 |
+
"params" that specifies model gin configuration, supplemental xarray
|
| 567 |
+
dataset with model-specific static features, and model parameters.
|
| 568 |
+
|
| 569 |
+
Returns:
|
| 570 |
+
Instance of a `PressureLevelModel` with weights and configuration
|
| 571 |
+
specified by the checkpoint.
|
| 572 |
+
"""
|
| 573 |
+
# Hard code radius=1.0 to enable breaking changes in Dinosaur.
|
| 574 |
+
model_config_str = (
|
| 575 |
+
checkpoint['model_config_str'].replace(
|
| 576 |
+
'GridWithWavenumbers.radius = None',
|
| 577 |
+
'GridWithWavenumbers.radius = 1.0',
|
| 578 |
+
)
|
| 579 |
+
+ '\n\n'
|
| 580 |
+
+ '\n'.join([
|
| 581 |
+
'GridTL63.radius = 1.0',
|
| 582 |
+
'GridTL127.radius = 1.0',
|
| 583 |
+
'GridTL255.radius = 1.0',
|
| 584 |
+
])
|
| 585 |
+
)
|
| 586 |
+
with gin_utils.specific_config(model_config_str):
|
| 587 |
+
physics_specs = physics_specifications.get_physics_specs()
|
| 588 |
+
aux_ds = xarray.Dataset.from_dict(checkpoint['aux_ds_dict'])
|
| 589 |
+
data_coords = model_builder.coordinate_system_from_dataset(aux_ds)
|
| 590 |
+
model_specs = model_builder.get_model_specs(
|
| 591 |
+
data_coords, physics_specs, {xarray_utils.XARRAY_DS_KEY: aux_ds}
|
| 592 |
+
)
|
| 593 |
+
whirl_model = model_builder.WhirlModel(
|
| 594 |
+
coords=model_specs.coords,
|
| 595 |
+
dt=model_specs.dt,
|
| 596 |
+
physics_specs=model_specs.physics_specs,
|
| 597 |
+
aux_features=model_specs.aux_features,
|
| 598 |
+
input_coords=data_coords,
|
| 599 |
+
output_coords=data_coords,
|
| 600 |
+
)
|
| 601 |
+
return cls(whirl_model, checkpoint['params'], model_config_str)
|
model/legacy/correctors.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules that predict refinement or updates of time-advanced states."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
from typing import Any, Callable, Optional
|
| 18 |
+
from dinosaur import coordinate_systems
|
| 19 |
+
from dinosaur import time_integration
|
| 20 |
+
from dinosaur import typing
|
| 21 |
+
import gin
|
| 22 |
+
import haiku as hk
|
| 23 |
+
import jax
|
| 24 |
+
from model.legacy import equations
|
| 25 |
+
from model.legacy import features
|
| 26 |
+
from model.legacy import filters
|
| 27 |
+
from model.legacy import integrators
|
| 28 |
+
from model.legacy import mappings
|
| 29 |
+
|
| 30 |
+
Pytree = typing.Pytree
|
| 31 |
+
PyTreeState = typing.PyTreeState
|
| 32 |
+
Forcing = typing.Forcing
|
| 33 |
+
|
| 34 |
+
CorrectorFn = typing.CorrectorFn
|
| 35 |
+
CorrectorModule = typing.CorrectorModule
|
| 36 |
+
EquationModule = equations.EquationModule
|
| 37 |
+
FeaturesModule = features.FeaturesModule
|
| 38 |
+
MappingModule = mappings.MappingModule
|
| 39 |
+
StepModule = typing.StepModule
|
| 40 |
+
StepFilterModule = Callable[..., typing.PyTreeStepFilterFn]
|
| 41 |
+
TimeIntegrator = integrators.TimeIntegrator
|
| 42 |
+
TransformModule = typing.TransformModule
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@gin.register
|
| 46 |
+
class PredictorEulerCorrector(hk.Module):
|
| 47 |
+
"""Corrector that takes Euler step ontop of a predictor step."""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 52 |
+
dt: float,
|
| 53 |
+
physics_specs: Any,
|
| 54 |
+
aux_features: typing.AuxFeatures,
|
| 55 |
+
predictor_module: StepModule,
|
| 56 |
+
filter_module: StepFilterModule = filters.NoFilter,
|
| 57 |
+
name: Optional[str] = None,
|
| 58 |
+
):
|
| 59 |
+
super().__init__(name=name)
|
| 60 |
+
self.dt = dt
|
| 61 |
+
self.step_fn = predictor_module(coords, dt, physics_specs, aux_features)
|
| 62 |
+
self.filter_fn = filter_module(coords, dt, physics_specs, aux_features)
|
| 63 |
+
|
| 64 |
+
def __call__(
|
| 65 |
+
self,
|
| 66 |
+
state: typing.PyTreeState,
|
| 67 |
+
tendencies: typing.PyTreeState,
|
| 68 |
+
forcing: Optional[Forcing] = None,
|
| 69 |
+
) -> typing.PyTreeState:
|
| 70 |
+
state = self.step_fn(state, forcing)
|
| 71 |
+
euler_add_fn = lambda x, y: x + self.dt * y if y is not None else x
|
| 72 |
+
result = jax.tree_util.tree_map(euler_add_fn, state, tendencies)
|
| 73 |
+
return self.filter_fn(state, result)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@gin.register
|
| 77 |
+
class DycoreWithPhysicsCorrector(hk.Module):
|
| 78 |
+
"""Corrector that runs dycore with physics tendencies added to explicit terms.
|
| 79 |
+
|
| 80 |
+
This corrector treats predicted physics tendencies constant at each time
|
| 81 |
+
interval and includes them to all substeps of the dycore step. To achieve this
|
| 82 |
+
the dycore in this module is specified by the governing equation, rather than
|
| 83 |
+
an `EquationStep`.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 89 |
+
dt: float,
|
| 90 |
+
physics_specs: Any,
|
| 91 |
+
aux_features: typing.AuxFeatures,
|
| 92 |
+
dycore_equation_module: EquationModule = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 93 |
+
dycore_substeps: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 94 |
+
time_integrator: TimeIntegrator = integrators.imex_rk_sil3,
|
| 95 |
+
filter_module: StepFilterModule = filters.NoFilter,
|
| 96 |
+
checkpoint_explicit_terms: bool = True,
|
| 97 |
+
name: Optional[str] = None,
|
| 98 |
+
):
|
| 99 |
+
super().__init__(name=name)
|
| 100 |
+
dycore_equation = dycore_equation_module(
|
| 101 |
+
coords, dt, physics_specs, aux_features)
|
| 102 |
+
if checkpoint_explicit_terms:
|
| 103 |
+
dycore_equation = time_integration.ImplicitExplicitODE.from_functions(
|
| 104 |
+
hk.remat(dycore_equation.explicit_terms),
|
| 105 |
+
dycore_equation.implicit_terms,
|
| 106 |
+
dycore_equation.implicit_inverse) # pyrefly: ignore[bad-argument-type]
|
| 107 |
+
self.coords = coords
|
| 108 |
+
self.dycore_equation = dycore_equation
|
| 109 |
+
self.dycore_substeps = dycore_substeps
|
| 110 |
+
self.inner_dt = dt / dycore_substeps
|
| 111 |
+
self.dt = dt
|
| 112 |
+
self.time_integrator = time_integrator
|
| 113 |
+
self.filter_fn = filter_module(coords, dt, physics_specs, aux_features)
|
| 114 |
+
|
| 115 |
+
def __call__(
|
| 116 |
+
self,
|
| 117 |
+
state: typing.PyTreeState,
|
| 118 |
+
tendencies: typing.PyTreeState,
|
| 119 |
+
forcing: Optional[Forcing] = None,
|
| 120 |
+
) -> typing.PyTreeState:
|
| 121 |
+
state, tendencies = self.coords.with_dycore_sharding((state, tendencies))
|
| 122 |
+
physics_parametrization_eq = time_integration.ExplicitODE.from_functions(
|
| 123 |
+
lambda state: tendencies)
|
| 124 |
+
all_equations = (self.dycore_equation, physics_parametrization_eq)
|
| 125 |
+
equation = time_integration.compose_equations(all_equations)
|
| 126 |
+
step_fn = self.time_integrator(equation, self.inner_dt)
|
| 127 |
+
# TODO(dkochkov) make step_with_filters work with single filter.
|
| 128 |
+
step_fn = time_integration.step_with_filters(step_fn, [self.filter_fn])
|
| 129 |
+
step_fn = time_integration.repeated(step_fn, self.dycore_substeps, hk.scan)
|
| 130 |
+
state = time_integration.maybe_fix_sim_time_roundoff(
|
| 131 |
+
step_fn(state), self.dt
|
| 132 |
+
)
|
| 133 |
+
state = self.coords.with_dycore_sharding(state)
|
| 134 |
+
return state
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@gin.register
|
| 138 |
+
class CustomCoordsCorrector(hk.Module):
|
| 139 |
+
"""Corrector module that uses gin-configured coordinates instead of coords.
|
| 140 |
+
|
| 141 |
+
This class currently supports model states in spectral representation. It
|
| 142 |
+
could be easily extended to nodal-state models by converting to modal space
|
| 143 |
+
prior to spectral interpolation and back after the timestep if performed.
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
def __init__(
|
| 147 |
+
self,
|
| 148 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 149 |
+
dt: float,
|
| 150 |
+
physics_specs: Any,
|
| 151 |
+
aux_features: typing.AuxFeatures,
|
| 152 |
+
corrector_module: CorrectorModule = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 153 |
+
custom_coords: coordinate_systems.CoordinateSystem = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 154 |
+
name: Optional[str] = None,
|
| 155 |
+
):
|
| 156 |
+
super().__init__(name=name)
|
| 157 |
+
custom_coords = dataclasses.replace(
|
| 158 |
+
custom_coords, spmd_mesh=coords.spmd_mesh
|
| 159 |
+
)
|
| 160 |
+
self.corrector_fn = corrector_module(
|
| 161 |
+
custom_coords, dt, physics_specs, aux_features)
|
| 162 |
+
self.to_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
|
| 163 |
+
coords, custom_coords)
|
| 164 |
+
self.from_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
|
| 165 |
+
custom_coords, coords)
|
| 166 |
+
|
| 167 |
+
def __call__(
|
| 168 |
+
self,
|
| 169 |
+
state: typing.PyTreeState,
|
| 170 |
+
tendencies: typing.PyTreeState,
|
| 171 |
+
forcing: Optional[Forcing] = None,
|
| 172 |
+
) -> typing.PyTreeState:
|
| 173 |
+
state = self.to_custom_coords_fn(state)
|
| 174 |
+
tendencies = self.to_custom_coords_fn(tendencies)
|
| 175 |
+
# TODO(dkochkov) Consider adding forcing interpolated to custom coords.
|
| 176 |
+
custom_out = self.corrector_fn(state, tendencies, None)
|
| 177 |
+
return self.from_custom_coords_fn(custom_out)
|
model/legacy/decoders.py
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines `decoder` modules that map model state to output data format."""
|
| 15 |
+
|
| 16 |
+
import functools
|
| 17 |
+
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
|
| 18 |
+
import zlib
|
| 19 |
+
|
| 20 |
+
from dinosaur import coordinate_systems
|
| 21 |
+
from dinosaur import primitive_equations
|
| 22 |
+
from dinosaur import pytree_utils
|
| 23 |
+
from dinosaur import scales
|
| 24 |
+
from dinosaur import spherical_harmonic
|
| 25 |
+
from dinosaur import typing
|
| 26 |
+
from dinosaur import vertical_interpolation
|
| 27 |
+
from dinosaur import weatherbench_utils
|
| 28 |
+
from dinosaur import xarray_utils
|
| 29 |
+
import gin
|
| 30 |
+
import haiku as hk
|
| 31 |
+
import jax
|
| 32 |
+
import jax.numpy as jnp
|
| 33 |
+
from model.legacy import diagnostics
|
| 34 |
+
from model.legacy import features
|
| 35 |
+
from model.legacy import filters
|
| 36 |
+
from model.legacy import mappings
|
| 37 |
+
from model.legacy import orographies
|
| 38 |
+
from model.legacy import perturbations
|
| 39 |
+
from model.legacy import stochastic
|
| 40 |
+
from model.legacy import transforms
|
| 41 |
+
import numpy as np
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# long lines are better than splitting argument definitions onto two lines
|
| 45 |
+
# pylint: disable=line-too-long
|
| 46 |
+
|
| 47 |
+
# We ♥ λ's
|
| 48 |
+
# pylint: disable=g-long-lambda
|
| 49 |
+
|
| 50 |
+
DataState = typing.DataState
|
| 51 |
+
DiagnosticModule = diagnostics.DiagnosticModule
|
| 52 |
+
FeaturesModule = features.FeaturesModule
|
| 53 |
+
FilterModule = Callable[..., typing.PyTreeFilterFn]
|
| 54 |
+
Forcing = typing.Forcing
|
| 55 |
+
MappingModule = mappings.MappingModule
|
| 56 |
+
PyTreeState = typing.PyTreeState
|
| 57 |
+
ModelState = typing.ModelState
|
| 58 |
+
TransformModule = typing.TransformModule
|
| 59 |
+
OrographyModule = orographies.OrographyModule
|
| 60 |
+
PerturbationModule = perturbations.PerturbationModule
|
| 61 |
+
RandomnessModule = stochastic.RandomnessModule
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@gin.register
|
| 65 |
+
class DecoderIdentityTransform(hk.Module):
|
| 66 |
+
"""Transformation that returns inputs without modification."""
|
| 67 |
+
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 71 |
+
dt: float,
|
| 72 |
+
physics_specs: Any,
|
| 73 |
+
aux_features: Dict[str, Any],
|
| 74 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 75 |
+
name: Optional[str] = None,
|
| 76 |
+
):
|
| 77 |
+
super().__init__(name=name)
|
| 78 |
+
del coords, dt, physics_specs, aux_features, output_coords
|
| 79 |
+
|
| 80 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 81 |
+
return inputs
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@gin.register
|
| 85 |
+
class DecoderFilterTransform(hk.Module):
|
| 86 |
+
"""Transformation that returns truncated and filtered modal inputs."""
|
| 87 |
+
|
| 88 |
+
def __init__(
|
| 89 |
+
self,
|
| 90 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 91 |
+
dt: float,
|
| 92 |
+
physics_specs: Any,
|
| 93 |
+
aux_features: Dict[str, Any],
|
| 94 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 95 |
+
filter_module: FilterModule = filters.DataNoFilter,
|
| 96 |
+
return_nodal: bool = True,
|
| 97 |
+
name: Optional[str] = None,
|
| 98 |
+
):
|
| 99 |
+
super().__init__(name=name)
|
| 100 |
+
self.output_coords = output_coords
|
| 101 |
+
self.filter_fn = filter_module(coords, dt, physics_specs, aux_features)
|
| 102 |
+
self.return_nodal = return_nodal
|
| 103 |
+
|
| 104 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 105 |
+
modal_inputs = coordinate_systems.maybe_to_modal(inputs, self.output_coords)
|
| 106 |
+
filtered_inputs = self.filter_fn(modal_inputs)
|
| 107 |
+
if self.return_nodal:
|
| 108 |
+
return self.output_coords.horizontal.to_nodal(filtered_inputs)
|
| 109 |
+
return filtered_inputs
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@gin.register
|
| 113 |
+
class OutputModalToModalTransform(hk.Module):
|
| 114 |
+
"""Transformation that truncates modal state to output coords."""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 119 |
+
dt: float,
|
| 120 |
+
physics_specs: Any,
|
| 121 |
+
aux_features: Dict[str, Any],
|
| 122 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 123 |
+
name: Optional[str] = None,
|
| 124 |
+
):
|
| 125 |
+
super().__init__(name=name)
|
| 126 |
+
self.coords = coords
|
| 127 |
+
self.output_coords = output_coords
|
| 128 |
+
|
| 129 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 130 |
+
downsample_fn = coordinate_systems.get_spectral_downsample_fn(
|
| 131 |
+
self.coords, self.output_coords
|
| 132 |
+
)
|
| 133 |
+
return downsample_fn(inputs)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@gin.register
|
| 137 |
+
class OutputModalToNodalTransform(hk.Module):
|
| 138 |
+
"""Transformation that converts modal state to nodal representation."""
|
| 139 |
+
|
| 140 |
+
def __init__(
|
| 141 |
+
self,
|
| 142 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 143 |
+
dt: float,
|
| 144 |
+
physics_specs: Any,
|
| 145 |
+
aux_features: Dict[str, Any],
|
| 146 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 147 |
+
name: Optional[str] = None,
|
| 148 |
+
):
|
| 149 |
+
super().__init__(name=name)
|
| 150 |
+
self.coords = coords
|
| 151 |
+
self.output_coords = output_coords
|
| 152 |
+
|
| 153 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 154 |
+
to_nodal_fn = self.output_coords.horizontal.to_nodal
|
| 155 |
+
downsample_fn = coordinate_systems.get_spectral_downsample_fn(
|
| 156 |
+
self.coords, self.output_coords
|
| 157 |
+
)
|
| 158 |
+
return jax.tree_util.tree_map(
|
| 159 |
+
lambda x: to_nodal_fn(downsample_fn(x)), inputs
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@gin.register
|
| 164 |
+
class OutputNodalToModalTransform(hk.Module):
|
| 165 |
+
"""Transformation that converts nodal state to modal representation."""
|
| 166 |
+
|
| 167 |
+
def __init__(
|
| 168 |
+
self,
|
| 169 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 170 |
+
dt: float,
|
| 171 |
+
physics_specs: Any,
|
| 172 |
+
aux_features: Dict[str, Any],
|
| 173 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 174 |
+
name: Optional[str] = None,
|
| 175 |
+
):
|
| 176 |
+
super().__init__(name=name)
|
| 177 |
+
self.output_coords = output_coords
|
| 178 |
+
|
| 179 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 180 |
+
return self.output_coords.horizontal.to_modal(inputs)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@gin.register
|
| 184 |
+
class ModalOutputLearnedAdaptorTransform(hk.Module):
|
| 185 |
+
"""Transformation using a tower to adapt modal outputs to the data domain."""
|
| 186 |
+
|
| 187 |
+
def __init__(
|
| 188 |
+
self,
|
| 189 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 190 |
+
dt: float,
|
| 191 |
+
physics_specs: Any,
|
| 192 |
+
aux_features: Dict[str, Any],
|
| 193 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 194 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 195 |
+
nodal_mapping_module: MappingModule,
|
| 196 |
+
output_transform_module: TransformModule,
|
| 197 |
+
name: Optional[str] = None,
|
| 198 |
+
):
|
| 199 |
+
del output_coords # unused.
|
| 200 |
+
super().__init__(name=name)
|
| 201 |
+
self.coords = coords
|
| 202 |
+
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
|
| 203 |
+
coords, dt, physics_specs, aux_features
|
| 204 |
+
)
|
| 205 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 206 |
+
self.output_transform_fn = output_transform_module(
|
| 207 |
+
coords, dt, physics_specs, aux_features
|
| 208 |
+
)
|
| 209 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 210 |
+
x, coords
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 214 |
+
"""Applies transform to modal inputs, returns modal outputs."""
|
| 215 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 216 |
+
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
|
| 217 |
+
# if `inputs` contain `sim_time` - remove it from corrections.
|
| 218 |
+
sim_time_shape = prediction_shapes.pop('sim_time', None)
|
| 219 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 220 |
+
nodal_input_features = self.modal_to_nodal_features_fn(inputs, None)
|
| 221 |
+
nodal_corrections = self.output_transform_fn(net(nodal_input_features))
|
| 222 |
+
corrections = self.coords.horizontal.to_modal(nodal_corrections)
|
| 223 |
+
if sim_time_shape is not None:
|
| 224 |
+
corrections['sim_time'] = 0.0
|
| 225 |
+
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
|
| 226 |
+
return from_dict_fn(outputs)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
@gin.register
|
| 230 |
+
class NodalOutputLearnedAdaptorTransform(hk.Module):
|
| 231 |
+
"""Transformation using a tower to adapt nodal outputs to the data domain."""
|
| 232 |
+
|
| 233 |
+
def __init__(
|
| 234 |
+
self,
|
| 235 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 236 |
+
dt: float,
|
| 237 |
+
physics_specs: Any,
|
| 238 |
+
aux_features: Dict[str, Any],
|
| 239 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 240 |
+
nodal_to_nodal_features_module: FeaturesModule,
|
| 241 |
+
nodal_mapping_module: MappingModule,
|
| 242 |
+
output_transform_module: TransformModule,
|
| 243 |
+
name: Optional[str] = None,
|
| 244 |
+
):
|
| 245 |
+
del output_coords # unused.
|
| 246 |
+
super().__init__(name=name)
|
| 247 |
+
self.coords = coords
|
| 248 |
+
self.nodal_to_nodal_features_fn = nodal_to_nodal_features_module(
|
| 249 |
+
coords, dt, physics_specs, aux_features
|
| 250 |
+
)
|
| 251 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 252 |
+
self.output_transform_fn = output_transform_module(
|
| 253 |
+
coords, dt, physics_specs, aux_features
|
| 254 |
+
)
|
| 255 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 256 |
+
x, coords
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 260 |
+
"""Applies transform to nodal inputs, returns nodal outputs."""
|
| 261 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 262 |
+
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
|
| 263 |
+
# if `inputs` contain `sim_time` - remove it from corrections.
|
| 264 |
+
sim_time_shape = prediction_shapes.pop('sim_time', None)
|
| 265 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 266 |
+
input_features = self.nodal_to_nodal_features_fn(inputs, None)
|
| 267 |
+
corrections = self.output_transform_fn(net(input_features))
|
| 268 |
+
if sim_time_shape is not None:
|
| 269 |
+
corrections['sim_time'] = 0.0
|
| 270 |
+
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
|
| 271 |
+
return from_dict_fn(outputs)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@gin.register
|
| 275 |
+
class DecoderCombinedTransform(hk.Module):
|
| 276 |
+
"""Module that applies multiple transformations sequentially."""
|
| 277 |
+
|
| 278 |
+
def __init__(
|
| 279 |
+
self,
|
| 280 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 281 |
+
dt: float,
|
| 282 |
+
physics_specs: Any,
|
| 283 |
+
aux_features: typing.AuxFeatures,
|
| 284 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 285 |
+
transforms: Tuple[TransformModule, ...], # pylint: disable=redefined-outer-name
|
| 286 |
+
name: Optional[str] = None,
|
| 287 |
+
):
|
| 288 |
+
super().__init__(name=name)
|
| 289 |
+
self.transform_fns = [
|
| 290 |
+
module(coords, dt, physics_specs, aux_features, output_coords)
|
| 291 |
+
for module in transforms
|
| 292 |
+
]
|
| 293 |
+
|
| 294 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 295 |
+
for transform_fn in self.transform_fns:
|
| 296 |
+
inputs = transform_fn(inputs)
|
| 297 |
+
return inputs
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@gin.register
|
| 301 |
+
class IdentityDecoder(hk.Module):
|
| 302 |
+
"""Decoder that returns model state unaltered."""
|
| 303 |
+
|
| 304 |
+
def __init__(
|
| 305 |
+
self,
|
| 306 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 307 |
+
dt: float,
|
| 308 |
+
physics_specs: Any,
|
| 309 |
+
aux_features: Dict[str, Any],
|
| 310 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 311 |
+
name: Optional[str] = None,
|
| 312 |
+
):
|
| 313 |
+
del coords, dt, physics_specs, aux_features, output_coords
|
| 314 |
+
super().__init__(name=name)
|
| 315 |
+
|
| 316 |
+
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
|
| 317 |
+
del forcing
|
| 318 |
+
return x.state
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
@gin.register
|
| 322 |
+
class StateToDictDecoder(hk.Module):
|
| 323 |
+
"""Decoder that returns a dict representation of a model state."""
|
| 324 |
+
|
| 325 |
+
def __init__(
|
| 326 |
+
self,
|
| 327 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 328 |
+
dt: float,
|
| 329 |
+
physics_specs: Any,
|
| 330 |
+
aux_features: Dict[str, Any],
|
| 331 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 332 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 333 |
+
name: Optional[str] = None,
|
| 334 |
+
):
|
| 335 |
+
super().__init__(name=name)
|
| 336 |
+
self.transform_fn = transform_module(
|
| 337 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
|
| 341 |
+
del forcing
|
| 342 |
+
state_dict, _ = pytree_utils.as_dict(x.state)
|
| 343 |
+
return self.transform_fn(state_dict)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
@gin.register
|
| 347 |
+
class LeapfrogSliceDecoder(hk.Module):
|
| 348 |
+
"""Decoder that returns one slice out of a leapfrog pair."""
|
| 349 |
+
|
| 350 |
+
def __init__(
|
| 351 |
+
self,
|
| 352 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 353 |
+
dt: float,
|
| 354 |
+
physics_specs: Any,
|
| 355 |
+
aux_features: Dict[str, Any],
|
| 356 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 357 |
+
slice_id: int = 0,
|
| 358 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 359 |
+
name: Optional[str] = None,
|
| 360 |
+
):
|
| 361 |
+
super().__init__(name=name)
|
| 362 |
+
self.slice_id = slice_id
|
| 363 |
+
self.transform_fn = transform_module(
|
| 364 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
|
| 368 |
+
del forcing
|
| 369 |
+
return self.transform_fn(x.state[self.slice_id])
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
@gin.register
|
| 373 |
+
class LeapfrogSliceDictDecoder(hk.Module):
|
| 374 |
+
"""Decoder that returns one slice out of a leapfrog pair as dictionary."""
|
| 375 |
+
|
| 376 |
+
def __init__(
|
| 377 |
+
self,
|
| 378 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 379 |
+
dt: float,
|
| 380 |
+
physics_specs: Any,
|
| 381 |
+
aux_features: Dict[str, Any],
|
| 382 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 383 |
+
slice_id: int = 0,
|
| 384 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 385 |
+
name: Optional[str] = None,
|
| 386 |
+
):
|
| 387 |
+
super().__init__(name=name)
|
| 388 |
+
self.slice_id = slice_id
|
| 389 |
+
self.transform_fn = transform_module(
|
| 390 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
|
| 394 |
+
del forcing
|
| 395 |
+
state_dict, _ = pytree_utils.as_dict(x.state[self.slice_id])
|
| 396 |
+
return self.transform_fn(state_dict)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
@gin.configurable
|
| 400 |
+
class PrimitiveToWeatherbenchDecoder(hk.Module):
|
| 401 |
+
"""Decoder that converts `StateWithTime` to `weatherbench.State`."""
|
| 402 |
+
|
| 403 |
+
def __init__(
|
| 404 |
+
self,
|
| 405 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 406 |
+
dt: float,
|
| 407 |
+
physics_specs: Any,
|
| 408 |
+
aux_features: Dict[str, Any],
|
| 409 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 410 |
+
time_axis: int = 0,
|
| 411 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 412 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 413 |
+
name: Optional[str] = None,
|
| 414 |
+
):
|
| 415 |
+
super().__init__(name=name)
|
| 416 |
+
ref_temps = aux_features[xarray_utils.REF_TEMP_KEY]
|
| 417 |
+
self.ref_temps = ref_temps[..., np.newaxis, np.newaxis]
|
| 418 |
+
self.output_coords = output_coords
|
| 419 |
+
self.coords = coords
|
| 420 |
+
self.physics_specs = physics_specs
|
| 421 |
+
self.velocity_fn = functools.partial(
|
| 422 |
+
spherical_harmonic.vor_div_to_uv_nodal,
|
| 423 |
+
output_coords.horizontal,
|
| 424 |
+
)
|
| 425 |
+
modal_orography_init_fn = orography_module(
|
| 426 |
+
coords, dt, physics_specs, aux_features
|
| 427 |
+
)
|
| 428 |
+
orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 429 |
+
self.nodal_orography = coords.horizontal.to_nodal(orography)
|
| 430 |
+
self.geopotential_fn = functools.partial(
|
| 431 |
+
primitive_equations.get_geopotential_with_moisture,
|
| 432 |
+
nodal_orography=self.nodal_orography,
|
| 433 |
+
coordinates=coords.vertical,
|
| 434 |
+
gravity_acceleration=physics_specs.gravity_acceleration,
|
| 435 |
+
ideal_gas_constant=physics_specs.ideal_gas_constant,
|
| 436 |
+
water_vapor_gas_constant=physics_specs.water_vapor_gas_constant,
|
| 437 |
+
)
|
| 438 |
+
self.transform_fn = transform_module(
|
| 439 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
def primitive_to_weatherbench(
|
| 443 |
+
self,
|
| 444 |
+
inputs: primitive_equations.StateWithTime,
|
| 445 |
+
) -> weatherbench_utils.State:
|
| 446 |
+
"""Converts pe_state to weatherbench state on pressure levels."""
|
| 447 |
+
# output state is computed on output_coords.
|
| 448 |
+
to_nodal_fn = self.output_coords.horizontal.to_nodal
|
| 449 |
+
u, v = self.velocity_fn( # returned in nodal space.
|
| 450 |
+
vorticity=inputs.vorticity, divergence=inputs.divergence
|
| 451 |
+
)
|
| 452 |
+
t = self.ref_temps + to_nodal_fn(inputs.temperature_variation)
|
| 453 |
+
tracers = to_nodal_fn(inputs.tracers)
|
| 454 |
+
z = self.geopotential_fn(t, tracers['specific_humidity'])
|
| 455 |
+
surface_pressure = jnp.exp(to_nodal_fn(inputs.log_surface_pressure))
|
| 456 |
+
u, v, t, z, tracers, surface_pressure = (
|
| 457 |
+
self.coords.dycore_to_physics_sharding(
|
| 458 |
+
(u, v, t, z, tracers, surface_pressure)
|
| 459 |
+
)
|
| 460 |
+
)
|
| 461 |
+
interpolate_with_linear_extrap_fn = (
|
| 462 |
+
vertical_interpolation.vectorize_vertical_interpolation(
|
| 463 |
+
vertical_interpolation.linear_interp_with_linear_extrap
|
| 464 |
+
)
|
| 465 |
+
)
|
| 466 |
+
interpolate_with_constant_extrap_fn = (
|
| 467 |
+
vertical_interpolation.vectorize_vertical_interpolation(
|
| 468 |
+
vertical_interpolation.vertical_interpolation
|
| 469 |
+
)
|
| 470 |
+
)
|
| 471 |
+
regrid_with_linear_fn = functools.partial(
|
| 472 |
+
vertical_interpolation.interp_sigma_to_pressure,
|
| 473 |
+
pressure_coords=self.output_coords.vertical,
|
| 474 |
+
sigma_coords=self.coords.vertical,
|
| 475 |
+
surface_pressure=surface_pressure,
|
| 476 |
+
interpolate_fn=interpolate_with_linear_extrap_fn,
|
| 477 |
+
)
|
| 478 |
+
regrid_with_constant_fn = functools.partial(
|
| 479 |
+
vertical_interpolation.interp_sigma_to_pressure,
|
| 480 |
+
pressure_coords=self.output_coords.vertical,
|
| 481 |
+
sigma_coords=self.coords.vertical,
|
| 482 |
+
surface_pressure=surface_pressure,
|
| 483 |
+
interpolate_fn=interpolate_with_constant_extrap_fn,
|
| 484 |
+
)
|
| 485 |
+
# closes regridding options based on http://shortn/_X09ZAU1jsx.
|
| 486 |
+
# use constant extrapolation for `u, v, tracers`.
|
| 487 |
+
# use linear extrapolation for `z, t`.
|
| 488 |
+
return weatherbench_utils.State(
|
| 489 |
+
u=regrid_with_constant_fn(u), # pyrefly: ignore[unexpected-keyword]
|
| 490 |
+
v=regrid_with_constant_fn(v), # pyrefly: ignore[unexpected-keyword]
|
| 491 |
+
t=regrid_with_linear_fn(t), # pyrefly: ignore[unexpected-keyword]
|
| 492 |
+
z=regrid_with_linear_fn(z), # pyrefly: ignore[unexpected-keyword]
|
| 493 |
+
sim_time=inputs.sim_time, # pyrefly: ignore[unexpected-keyword]
|
| 494 |
+
tracers=regrid_with_constant_fn(tracers), # pyrefly: ignore[unexpected-keyword]
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
def __call__(
|
| 498 |
+
self, inputs: ModelState, forcing: Forcing
|
| 499 |
+
) -> DataState:
|
| 500 |
+
del forcing
|
| 501 |
+
wb_on_sigma = self.primitive_to_weatherbench(inputs.state)
|
| 502 |
+
return self.transform_fn(wb_on_sigma.asdict()) # pyrefly: ignore[missing-attribute]
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
_DECODER_SALT = zlib.crc32(b'decoder') # arbitrary uint32 value
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def _decoder_prng_key(
|
| 509 |
+
randomness: typing.RandomnessState,
|
| 510 |
+
) -> typing.PRNGKeyArray | None:
|
| 511 |
+
"""Get a PRNG Key suitable for decoder randomness."""
|
| 512 |
+
if randomness.prng_key is None:
|
| 513 |
+
return None
|
| 514 |
+
salt = jnp.uint32(_DECODER_SALT) + jnp.uint32(randomness.prng_step)
|
| 515 |
+
return jax.random.fold_in(randomness.prng_key, salt)
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
@gin.register
|
| 519 |
+
class LearnedPrimitiveToWeatherbenchDecoder(PrimitiveToWeatherbenchDecoder):
|
| 520 |
+
"""Similar to `PrimitiveToWeatherbenchDecoder` with learned interpolation."""
|
| 521 |
+
|
| 522 |
+
def __init__(
|
| 523 |
+
self,
|
| 524 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 525 |
+
dt: float,
|
| 526 |
+
physics_specs: Any,
|
| 527 |
+
aux_features: Dict[str, Any],
|
| 528 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 529 |
+
modal_to_nodal_model_features_module: FeaturesModule,
|
| 530 |
+
modal_to_nodal_data_features_module: FeaturesModule,
|
| 531 |
+
correction_transform_module: TransformModule,
|
| 532 |
+
nodal_mapping_module: MappingModule,
|
| 533 |
+
prediction_mask: typing.Pytree,
|
| 534 |
+
time_axis: int = 0,
|
| 535 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 536 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 537 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 538 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 539 |
+
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
|
| 540 |
+
name: Optional[str] = None,
|
| 541 |
+
):
|
| 542 |
+
super().__init__(
|
| 543 |
+
coords=coords,
|
| 544 |
+
dt=dt,
|
| 545 |
+
physics_specs=physics_specs,
|
| 546 |
+
aux_features=aux_features,
|
| 547 |
+
output_coords=output_coords,
|
| 548 |
+
time_axis=time_axis,
|
| 549 |
+
orography_module=orography_module,
|
| 550 |
+
name=name,
|
| 551 |
+
) # don't pass the transform, as we apply it at the end.
|
| 552 |
+
self.prediction_mask = prediction_mask
|
| 553 |
+
# features are computed on both coordinate systems.
|
| 554 |
+
self.model_features_fn = modal_to_nodal_model_features_module(
|
| 555 |
+
coords, dt, physics_specs, aux_features
|
| 556 |
+
)
|
| 557 |
+
self.data_features_fn = modal_to_nodal_data_features_module(
|
| 558 |
+
output_coords, dt, physics_specs, aux_features
|
| 559 |
+
)
|
| 560 |
+
self.corrections_transform_fn = correction_transform_module(
|
| 561 |
+
coords, dt, physics_specs, aux_features
|
| 562 |
+
)
|
| 563 |
+
# corrections are computed in real space on output coordinates.
|
| 564 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 565 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 566 |
+
x, output_coords
|
| 567 |
+
)
|
| 568 |
+
self.transform_fn = transform_module(
|
| 569 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 570 |
+
)
|
| 571 |
+
self.randomness_fn = randomness_module(
|
| 572 |
+
coords, dt, physics_specs, aux_features
|
| 573 |
+
)
|
| 574 |
+
self.perturbation_fn = perturbation_module(
|
| 575 |
+
coords, dt, physics_specs, aux_features
|
| 576 |
+
)
|
| 577 |
+
self.diagnostic_fn = diagnostics_module(
|
| 578 |
+
coords, dt, physics_specs, aux_features
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
def __call__(
|
| 582 |
+
self, inputs: ModelState, forcing: Forcing
|
| 583 |
+
) -> DataState:
|
| 584 |
+
randomness = self.randomness_fn.unconditional_sample(
|
| 585 |
+
_decoder_prng_key(inputs.randomness)
|
| 586 |
+
)
|
| 587 |
+
prognostics = self.perturbation_fn(
|
| 588 |
+
inputs=self.coords.with_dycore_sharding(inputs.state),
|
| 589 |
+
state=None,
|
| 590 |
+
randomness=self.coords.with_dycore_sharding(randomness.nodal_value),
|
| 591 |
+
)
|
| 592 |
+
inputs.state = prognostics # compute diagnostics from the perturbed state.
|
| 593 |
+
|
| 594 |
+
# TODO(dkochkov) Could we pass physics_tendencies here?
|
| 595 |
+
# TODO(janniyuval) Consider using evaporation diagnostics for training.
|
| 596 |
+
decoder_diagnostics = self.diagnostic_fn(inputs, None)
|
| 597 |
+
wb_on_pressure_dict = self.primitive_to_weatherbench(prognostics).asdict() # pyrefly: ignore[missing-attribute]
|
| 598 |
+
wb_on_pressure_modal = coordinate_systems.maybe_to_modal(
|
| 599 |
+
self.coords.physics_to_dycore_sharding(wb_on_pressure_dict),
|
| 600 |
+
self.output_coords,
|
| 601 |
+
)
|
| 602 |
+
wb_on_pressure_dict['diagnostics'] = decoder_diagnostics
|
| 603 |
+
prediction_mask = pytree_utils.replace_with_matching_or_default(
|
| 604 |
+
wb_on_pressure_dict, self.prediction_mask, default=False)
|
| 605 |
+
prediction_shapes = jax.tree_util.tree_map(
|
| 606 |
+
lambda x, y: self.get_nodal_shape_fn(x) if y else None,
|
| 607 |
+
wb_on_pressure_dict,
|
| 608 |
+
prediction_mask,
|
| 609 |
+
)
|
| 610 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 611 |
+
model_features = self.model_features_fn(
|
| 612 |
+
prognostics.asdict(), forcing=forcing,
|
| 613 |
+
randomness=randomness.nodal_value
|
| 614 |
+
)
|
| 615 |
+
data_features = self.data_features_fn(wb_on_pressure_modal, forcing=forcing)
|
| 616 |
+
data_features = transforms.add_prefix(data_features, 'data_')
|
| 617 |
+
model_features = transforms.add_prefix(model_features, 'model_')
|
| 618 |
+
all_features = self.coords.dycore_to_physics_sharding(
|
| 619 |
+
data_features | model_features
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
nodal_outputs = self.corrections_transform_fn(net(all_features))
|
| 623 |
+
add_fn = lambda x, y: x + y if y is not None else x
|
| 624 |
+
wb_on_pressure_dict = jax.tree_util.tree_map(
|
| 625 |
+
add_fn, wb_on_pressure_dict, nodal_outputs
|
| 626 |
+
)
|
| 627 |
+
return self.transform_fn(wb_on_pressure_dict)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
@gin.register
|
| 631 |
+
class DimensionalPrimitiveToWeatherbenchDecoder(PrimitiveToWeatherbenchDecoder):
|
| 632 |
+
"""Same as PrimitiveToWeatherbenchDecoder, but with dimensional output."""
|
| 633 |
+
|
| 634 |
+
def __init__(
|
| 635 |
+
self,
|
| 636 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 637 |
+
dt: float,
|
| 638 |
+
physics_specs: Any,
|
| 639 |
+
aux_features: Dict[str, Any],
|
| 640 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 641 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 642 |
+
time_axis: int = 0,
|
| 643 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 644 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 645 |
+
name: Optional[str] = None,
|
| 646 |
+
):
|
| 647 |
+
nondim_pressure_centers = physics_specs.nondimensionalize(
|
| 648 |
+
output_coords.vertical.centers * scales.units.millibar
|
| 649 |
+
)
|
| 650 |
+
nondim_output_coords = coordinate_systems.CoordinateSystem(
|
| 651 |
+
output_coords.horizontal,
|
| 652 |
+
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
|
| 653 |
+
spmd_mesh=output_coords.spmd_mesh,
|
| 654 |
+
)
|
| 655 |
+
super().__init__(
|
| 656 |
+
coords,
|
| 657 |
+
dt,
|
| 658 |
+
physics_specs,
|
| 659 |
+
aux_features,
|
| 660 |
+
output_coords=nondim_output_coords,
|
| 661 |
+
time_axis=time_axis,
|
| 662 |
+
orography_module=orography_module,
|
| 663 |
+
transform_module=transform_module,
|
| 664 |
+
name=name,
|
| 665 |
+
)
|
| 666 |
+
self.redimensionalize_fn = transforms.RedimensionalizeTransform(
|
| 667 |
+
coords,
|
| 668 |
+
dt,
|
| 669 |
+
physics_specs,
|
| 670 |
+
aux_features,
|
| 671 |
+
output_coords=output_coords,
|
| 672 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
def __call__(
|
| 676 |
+
self, inputs: ModelState, forcing: Forcing
|
| 677 |
+
) -> DataState:
|
| 678 |
+
return self.redimensionalize_fn(super().__call__(inputs, forcing))
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
@gin.configurable
|
| 682 |
+
class DimensionalLearnedPrimitiveToWeatherbenchDecoder(
|
| 683 |
+
LearnedPrimitiveToWeatherbenchDecoder
|
| 684 |
+
):
|
| 685 |
+
"""Same as LearnedPrimitiveToWeatherbenchDecoder, but with dimensional output."""
|
| 686 |
+
|
| 687 |
+
def __init__(
|
| 688 |
+
self,
|
| 689 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 690 |
+
dt: float,
|
| 691 |
+
physics_specs: Any,
|
| 692 |
+
aux_features: Dict[str, Any],
|
| 693 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 694 |
+
modal_to_nodal_model_features_module: FeaturesModule,
|
| 695 |
+
modal_to_nodal_data_features_module: FeaturesModule,
|
| 696 |
+
nodal_mapping_module: MappingModule,
|
| 697 |
+
correction_transform_module: TransformModule,
|
| 698 |
+
prediction_mask: typing.Pytree,
|
| 699 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 700 |
+
time_axis: int = 0,
|
| 701 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 702 |
+
transform_module: TransformModule = DecoderIdentityTransform,
|
| 703 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 704 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 705 |
+
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
|
| 706 |
+
name: Optional[str] = None,
|
| 707 |
+
):
|
| 708 |
+
nondim_pressure_centers = physics_specs.nondimensionalize(
|
| 709 |
+
output_coords.vertical.centers * scales.units.millibar
|
| 710 |
+
)
|
| 711 |
+
nondim_output_coords = coordinate_systems.CoordinateSystem(
|
| 712 |
+
output_coords.horizontal,
|
| 713 |
+
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
|
| 714 |
+
spmd_mesh=output_coords.spmd_mesh,
|
| 715 |
+
)
|
| 716 |
+
super().__init__(
|
| 717 |
+
coords,
|
| 718 |
+
dt,
|
| 719 |
+
physics_specs,
|
| 720 |
+
aux_features,
|
| 721 |
+
output_coords=nondim_output_coords,
|
| 722 |
+
modal_to_nodal_model_features_module=(
|
| 723 |
+
modal_to_nodal_model_features_module
|
| 724 |
+
),
|
| 725 |
+
modal_to_nodal_data_features_module=modal_to_nodal_data_features_module,
|
| 726 |
+
nodal_mapping_module=nodal_mapping_module,
|
| 727 |
+
correction_transform_module=correction_transform_module,
|
| 728 |
+
prediction_mask=prediction_mask,
|
| 729 |
+
time_axis=time_axis,
|
| 730 |
+
orography_module=orography_module,
|
| 731 |
+
transform_module=transform_module,
|
| 732 |
+
randomness_module=randomness_module,
|
| 733 |
+
perturbation_module=perturbation_module,
|
| 734 |
+
diagnostics_module=diagnostics_module,
|
| 735 |
+
name=name,
|
| 736 |
+
)
|
| 737 |
+
self.redimensionalize_fn = transforms.RedimensionalizeTransform(
|
| 738 |
+
coords,
|
| 739 |
+
dt,
|
| 740 |
+
physics_specs,
|
| 741 |
+
aux_features,
|
| 742 |
+
output_coords=output_coords,
|
| 743 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
def __call__(
|
| 747 |
+
self, inputs: ModelState, forcing: Forcing
|
| 748 |
+
) -> DataState:
|
| 749 |
+
return self.redimensionalize_fn(super().__call__(inputs, forcing))
|
model/legacy/diagnostics.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines `diagnostic` modules that compute diagnostic predictions."""
|
| 15 |
+
|
| 16 |
+
from collections import abc
|
| 17 |
+
from typing import Any, Callable, Optional, Protocol
|
| 18 |
+
|
| 19 |
+
from dinosaur import coordinate_systems
|
| 20 |
+
from dinosaur import scales
|
| 21 |
+
from dinosaur import sigma_coordinates
|
| 22 |
+
from dinosaur import typing
|
| 23 |
+
|
| 24 |
+
import gin
|
| 25 |
+
import haiku as hk
|
| 26 |
+
import jax
|
| 27 |
+
import jax.numpy as jnp
|
| 28 |
+
import numpy as np
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
TransformModule = typing.TransformModule
|
| 32 |
+
|
| 33 |
+
PRECIPITATION = 'precipitation'
|
| 34 |
+
EVAPORATION = 'evaporation'
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class DiagnosticFn(Protocol):
|
| 38 |
+
"""Implements initialization and computation of model diagnostic fields."""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 43 |
+
dt: float,
|
| 44 |
+
physics_specs: Any,
|
| 45 |
+
aux_features: dict[str, Any],
|
| 46 |
+
):
|
| 47 |
+
del coords, dt, physics_specs, aux_features
|
| 48 |
+
|
| 49 |
+
def __call__(
|
| 50 |
+
self,
|
| 51 |
+
model_state: typing.ModelState,
|
| 52 |
+
physics_tendencies: typing.Pytree,
|
| 53 |
+
forcing: typing.Forcing | None = None,
|
| 54 |
+
) -> dict[str, jax.Array]:
|
| 55 |
+
"""Computes diagnostic field from `model_state` and `physics_tendencies`."""
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
DiagnosticModule = Callable[..., DiagnosticFn]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@gin.register
|
| 63 |
+
class NoDiagnostics:
|
| 64 |
+
"""Diagnostic module that computes no diagnostics."""
|
| 65 |
+
|
| 66 |
+
def __init__(
|
| 67 |
+
self,
|
| 68 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 69 |
+
dt: float,
|
| 70 |
+
physics_specs: Any,
|
| 71 |
+
aux_features: dict[str, Any],
|
| 72 |
+
):
|
| 73 |
+
del coords, dt, physics_specs, aux_features
|
| 74 |
+
|
| 75 |
+
def __call__(
|
| 76 |
+
self,
|
| 77 |
+
model_state: typing.ModelState,
|
| 78 |
+
physics_tendencies: typing.Pytree,
|
| 79 |
+
forcing: typing.Forcing | None = None,
|
| 80 |
+
) -> dict[str, jax.Array]:
|
| 81 |
+
return {}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@gin.register
|
| 85 |
+
class CombinedDiagnostics:
|
| 86 |
+
"""Computes a combination of multiple diagnostics."""
|
| 87 |
+
|
| 88 |
+
def __init__(
|
| 89 |
+
self,
|
| 90 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 91 |
+
dt: float,
|
| 92 |
+
physics_specs: Any,
|
| 93 |
+
aux_features: dict[str, Any],
|
| 94 |
+
diagnostic_modules: abc.Sequence[DiagnosticModule] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 95 |
+
):
|
| 96 |
+
self.diagnostic_fns = [
|
| 97 |
+
module(coords, dt, physics_specs, aux_features)
|
| 98 |
+
for module in diagnostic_modules
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
def __call__(
|
| 102 |
+
self,
|
| 103 |
+
model_state: typing.ModelState,
|
| 104 |
+
physics_tendencies: typing.Pytree,
|
| 105 |
+
forcing: typing.Forcing | None = None,
|
| 106 |
+
) -> dict[str, jax.Array]:
|
| 107 |
+
diagnostics = {}
|
| 108 |
+
for fn in self.diagnostic_fns:
|
| 109 |
+
new_diagnostics = fn(model_state, physics_tendencies, forcing)
|
| 110 |
+
if any(k in diagnostics for k in new_diagnostics):
|
| 111 |
+
raise ValueError(
|
| 112 |
+
f'{new_diagnostics.keys()} overlaps with {diagnostics.keys()}'
|
| 113 |
+
)
|
| 114 |
+
diagnostics.update(new_diagnostics)
|
| 115 |
+
return diagnostics
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@gin.register
|
| 119 |
+
class PrecipitationMinusEvaporationDiagnostics:
|
| 120 |
+
"""Computes `P-E` by integrating physics_tendencies.
|
| 121 |
+
|
| 122 |
+
Depending on the `method` computes either precipitation minus evaporation
|
| 123 |
+
rate, which in ERA5 has units `kg m**-2 s**-1` or time-accumulated value
|
| 124 |
+
in `kg m**-2` if `method == cumulative`.
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
def __init__(
|
| 128 |
+
self,
|
| 129 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 130 |
+
dt: float,
|
| 131 |
+
physics_specs: Any,
|
| 132 |
+
aux_features: dict[str, Any],
|
| 133 |
+
moisture_species: tuple[str, ...] = (
|
| 134 |
+
'specific_humidity',
|
| 135 |
+
'specific_cloud_ice_water_content',
|
| 136 |
+
'specific_cloud_liquid_water_content',
|
| 137 |
+
),
|
| 138 |
+
method: str = 'rate',
|
| 139 |
+
):
|
| 140 |
+
del aux_features
|
| 141 |
+
self.coords = coords
|
| 142 |
+
self.dt = dt
|
| 143 |
+
self.physics_specs = physics_specs
|
| 144 |
+
self.moisture_species = moisture_species
|
| 145 |
+
self.method = method
|
| 146 |
+
self.to_nodal_fn = coords.horizontal.to_nodal
|
| 147 |
+
|
| 148 |
+
def _compute_evaporation_minus_precipitation(
|
| 149 |
+
self, model_state: typing.ModelState, physics_tendencies: typing.Pytree
|
| 150 |
+
) -> typing.Array:
|
| 151 |
+
"""Computes evaporation minus precipitation."""
|
| 152 |
+
lsp = model_state.state.log_surface_pressure
|
| 153 |
+
p_surface = jnp.squeeze(jnp.exp(self.to_nodal_fn(lsp)), axis=0)
|
| 154 |
+
moisture_tendencies = [
|
| 155 |
+
v
|
| 156 |
+
for tracer, v in physics_tendencies.tracers.items()
|
| 157 |
+
if tracer in self.moisture_species
|
| 158 |
+
]
|
| 159 |
+
moisture_tendencies = sum(self.to_nodal_fn(moisture_tendencies))
|
| 160 |
+
scale = p_surface / self.physics_specs.g
|
| 161 |
+
e_minus_p = scale * sigma_coordinates.sigma_integral(
|
| 162 |
+
moisture_tendencies, self.coords.vertical, keepdims=False
|
| 163 |
+
)
|
| 164 |
+
return e_minus_p
|
| 165 |
+
|
| 166 |
+
def __call__(
|
| 167 |
+
self,
|
| 168 |
+
model_state: typing.ModelState,
|
| 169 |
+
physics_tendencies: typing.Pytree,
|
| 170 |
+
forcing: typing.Forcing | None = None,
|
| 171 |
+
) -> typing.Pytree:
|
| 172 |
+
"""Computes precipitation minus evaporation."""
|
| 173 |
+
del forcing # unused
|
| 174 |
+
e_minus_p = self._compute_evaporation_minus_precipitation(
|
| 175 |
+
model_state, physics_tendencies
|
| 176 |
+
)
|
| 177 |
+
if self.method == 'rate':
|
| 178 |
+
return {'P_minus_E_rate': -e_minus_p}
|
| 179 |
+
elif self.method == 'cumulative':
|
| 180 |
+
# TODO(dkochkov) Address possible precision loss due to small deltas.
|
| 181 |
+
surface_nodal_shape = self.coords.horizontal.nodal_shape
|
| 182 |
+
previous = model_state.diagnostics.get(
|
| 183 |
+
'P_minus_E_cumulative',
|
| 184 |
+
jnp.zeros(surface_nodal_shape))
|
| 185 |
+
return {'P_minus_E_cumulative': previous - (e_minus_p * self.dt)}
|
| 186 |
+
else:
|
| 187 |
+
raise ValueError(f'Unknown {self.method=}, must be `rate`/`cumulative`')
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@gin.register
|
| 191 |
+
class PrecipitableWaterDiagnostics:
|
| 192 |
+
"""Computes cumulative preciptable water in the state."""
|
| 193 |
+
|
| 194 |
+
def __init__(
|
| 195 |
+
self,
|
| 196 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 197 |
+
dt: float,
|
| 198 |
+
physics_specs: Any,
|
| 199 |
+
aux_features: dict[str, Any],
|
| 200 |
+
moisture_species: tuple[str, ...] = (
|
| 201 |
+
'specific_humidity',
|
| 202 |
+
'specific_cloud_ice_water_content',
|
| 203 |
+
'specific_cloud_liquid_water_content',
|
| 204 |
+
),
|
| 205 |
+
):
|
| 206 |
+
del dt, aux_features
|
| 207 |
+
self.coords = coords
|
| 208 |
+
self.physics_specs = physics_specs
|
| 209 |
+
self.moisture_species = moisture_species
|
| 210 |
+
self.to_nodal_fn = coords.horizontal.to_nodal
|
| 211 |
+
|
| 212 |
+
def __call__(
|
| 213 |
+
self,
|
| 214 |
+
model_state: typing.ModelState,
|
| 215 |
+
physics_tendencies: typing.Pytree,
|
| 216 |
+
forcing: typing.Forcing | None = None,
|
| 217 |
+
) -> typing.Pytree:
|
| 218 |
+
"""Computes preciptable water."""
|
| 219 |
+
del physics_tendencies, forcing # unused
|
| 220 |
+
lsp = model_state.state.log_surface_pressure
|
| 221 |
+
p_surface = jnp.squeeze(jnp.exp(self.to_nodal_fn(lsp)), axis=0)
|
| 222 |
+
moisture_tracers = [
|
| 223 |
+
v
|
| 224 |
+
for tracer, v in model_state.tracers.items() # pyrefly: ignore[missing-attribute]
|
| 225 |
+
if tracer in self.moisture_species
|
| 226 |
+
]
|
| 227 |
+
moisture = sum(self.to_nodal_fn(moisture_tracers))
|
| 228 |
+
water_density = self.physics_specs.nondimensionalize(scales.WATER_DENSITY)
|
| 229 |
+
scale = p_surface / (self.physics_specs.g * water_density)
|
| 230 |
+
water = scale * sigma_coordinates.sigma_integral(
|
| 231 |
+
moisture, self.coords.vertical, keepdims=False
|
| 232 |
+
)
|
| 233 |
+
return {'precipitable_water': water}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
@gin.register
|
| 237 |
+
class NodalModelDiagnosticsDecoder:
|
| 238 |
+
"""Diagnostics decoder that returns elements from model_state.diagnostics."""
|
| 239 |
+
|
| 240 |
+
def __init__(
|
| 241 |
+
self,
|
| 242 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 243 |
+
dt: float,
|
| 244 |
+
physics_specs: Any,
|
| 245 |
+
aux_features: dict[str, Any],
|
| 246 |
+
):
|
| 247 |
+
del dt, aux_features
|
| 248 |
+
self.coords = coords
|
| 249 |
+
self.physics_specs = physics_specs
|
| 250 |
+
|
| 251 |
+
def __call__(
|
| 252 |
+
self,
|
| 253 |
+
model_state: typing.ModelState,
|
| 254 |
+
physics_tendencies: typing.Pytree,
|
| 255 |
+
forcing: typing.Forcing | None = None,
|
| 256 |
+
) -> typing.Pytree:
|
| 257 |
+
"""Computes precipitation minus evaporation."""
|
| 258 |
+
del physics_tendencies, forcing # unused.
|
| 259 |
+
nodal_diagnostics = coordinate_systems.maybe_to_nodal(
|
| 260 |
+
model_state.diagnostics, self.coords
|
| 261 |
+
)
|
| 262 |
+
return nodal_diagnostics
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# TODO(janniyuval) add a decoder that can add some Gaussian noise to evap/precip
|
| 266 |
+
@gin.register
|
| 267 |
+
class PrecipitationDiagnosticsConstrained(
|
| 268 |
+
hk.Module, PrecipitationMinusEvaporationDiagnostics
|
| 269 |
+
):
|
| 270 |
+
"""Predict evaporation and computes cumulative precipitation.
|
| 271 |
+
|
| 272 |
+
Calculation is based on calculating `P-E` by integrating physics_tendencies.
|
| 273 |
+
Depending on the `method` computes either precipitation
|
| 274 |
+
rate, (which in ERA5 has units `kg m**-2 s**-1`) or time-accumulated value
|
| 275 |
+
in `Length` units (GPCP uses mm/day) if `method == cumulative`.
|
| 276 |
+
Evaporation has the units of `kg m**-2 s**-1` in ERA5.
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
def __init__(
|
| 280 |
+
self,
|
| 281 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 282 |
+
dt: float,
|
| 283 |
+
physics_specs: Any,
|
| 284 |
+
aux_features: dict[str, Any],
|
| 285 |
+
embedding_module: typing.EmbeddingModule,
|
| 286 |
+
moisture_species: tuple[str, ...] = (
|
| 287 |
+
'specific_humidity',
|
| 288 |
+
'specific_cloud_ice_water_content',
|
| 289 |
+
'specific_cloud_liquid_water_content',
|
| 290 |
+
),
|
| 291 |
+
is_precipitation: bool = True,
|
| 292 |
+
method_precipitation: str = 'cumulative',
|
| 293 |
+
method_evaporation: str = 'rate',
|
| 294 |
+
name: Optional[str] = None,
|
| 295 |
+
field_name: str = 'total_precipitation',
|
| 296 |
+
):
|
| 297 |
+
# del aux_features
|
| 298 |
+
super().__init__(name=name)
|
| 299 |
+
self.coords = coords
|
| 300 |
+
self.dt = dt
|
| 301 |
+
self.physics_specs = physics_specs
|
| 302 |
+
self.moisture_species = moisture_species
|
| 303 |
+
self.method_precipitation = method_precipitation
|
| 304 |
+
self.method_evaporation = method_evaporation
|
| 305 |
+
self.to_nodal_fn = coords.horizontal.to_nodal
|
| 306 |
+
self.is_precipitation = is_precipitation
|
| 307 |
+
if self.is_precipitation:
|
| 308 |
+
predicted_name = PRECIPITATION
|
| 309 |
+
diagnosed_name = EVAPORATION
|
| 310 |
+
else:
|
| 311 |
+
predicted_name = EVAPORATION
|
| 312 |
+
diagnosed_name = PRECIPITATION
|
| 313 |
+
|
| 314 |
+
self.predicted_name = predicted_name
|
| 315 |
+
self.diagnosed_name = diagnosed_name
|
| 316 |
+
|
| 317 |
+
output_shapes = {
|
| 318 |
+
f'{predicted_name}': np.asarray(coords.surface_nodal_shape)
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
self.embedding_fn = embedding_module(
|
| 322 |
+
coords, dt, physics_specs, aux_features, output_shapes=output_shapes
|
| 323 |
+
)
|
| 324 |
+
self.water_density = self.physics_specs.nondimensionalize(
|
| 325 |
+
scales.WATER_DENSITY
|
| 326 |
+
)
|
| 327 |
+
self.field_name = field_name
|
| 328 |
+
|
| 329 |
+
def __call__(
|
| 330 |
+
self,
|
| 331 |
+
model_state: typing.ModelState,
|
| 332 |
+
physics_tendencies: typing.Pytree,
|
| 333 |
+
forcing: typing.Forcing | None = None,
|
| 334 |
+
) -> typing.Pytree:
|
| 335 |
+
"""Computes precipitation minus evaporation."""
|
| 336 |
+
e_minus_p = self._compute_evaporation_minus_precipitation(
|
| 337 |
+
model_state, physics_tendencies
|
| 338 |
+
)
|
| 339 |
+
water_budget = self.embedding_fn(
|
| 340 |
+
model_state.state,
|
| 341 |
+
model_state.memory,
|
| 342 |
+
model_state.diagnostics,
|
| 343 |
+
model_state.randomness,
|
| 344 |
+
forcing,
|
| 345 |
+
)
|
| 346 |
+
water_budget[self.diagnosed_name] = (
|
| 347 |
+
-e_minus_p - water_budget[self.predicted_name]
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
# Note: In ERA5 mean_evaporation_rate (kg m**-2 s**-1)
|
| 351 |
+
# is negative for evaporation.
|
| 352 |
+
# In GPCP precipitation is positive (mm/day).
|
| 353 |
+
# Here e_minus_p is positive for evaporation.
|
| 354 |
+
output_dict = {}
|
| 355 |
+
surface_nodal_shape = self.coords.horizontal.nodal_shape
|
| 356 |
+
if self.method_precipitation == 'rate': # units: length/time
|
| 357 |
+
output_dict[PRECIPITATION + '_rate'] = (
|
| 358 |
+
water_budget[PRECIPITATION]
|
| 359 |
+
) / self.water_density
|
| 360 |
+
elif self.method_precipitation == 'cumulative': # units: length
|
| 361 |
+
previous = model_state.diagnostics.get(
|
| 362 |
+
self.field_name, jnp.zeros(surface_nodal_shape)
|
| 363 |
+
)
|
| 364 |
+
# TODO(janniyuval) remove precipitation_cumulative_mean once no models
|
| 365 |
+
# use it.
|
| 366 |
+
assert self.field_name in [
|
| 367 |
+
'total_precipitation',
|
| 368 |
+
'precipitation_cumulative_mean',
|
| 369 |
+
], self.field_name
|
| 370 |
+
output_dict[self.field_name] = previous + (
|
| 371 |
+
(water_budget[PRECIPITATION] / self.water_density) * self.dt
|
| 372 |
+
)
|
| 373 |
+
else:
|
| 374 |
+
raise ValueError(
|
| 375 |
+
f'Precipitation method is {self.method_precipitation=}, but it must'
|
| 376 |
+
' be `rate`/`cumulative`'
|
| 377 |
+
)
|
| 378 |
+
if self.method_evaporation == 'rate': # units: mass length**-2 time**-1
|
| 379 |
+
output_dict[EVAPORATION] = water_budget[EVAPORATION]
|
| 380 |
+
elif self.method_evaporation == 'cumulative': # units: length
|
| 381 |
+
previous_evap = model_state.diagnostics.get(
|
| 382 |
+
EVAPORATION + '_cumulative', jnp.zeros(surface_nodal_shape)
|
| 383 |
+
)
|
| 384 |
+
output_dict[EVAPORATION + '_cumulative'] = (
|
| 385 |
+
previous_evap
|
| 386 |
+
+ (water_budget[EVAPORATION] / self.water_density) * self.dt
|
| 387 |
+
)
|
| 388 |
+
else:
|
| 389 |
+
raise ValueError(
|
| 390 |
+
f'Evaporation method is {self.method_evaporation=}, but it must be'
|
| 391 |
+
' `rate`/`cumulative`'
|
| 392 |
+
)
|
| 393 |
+
return output_dict
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
@gin.register
|
| 397 |
+
class SurfacePressureDiagnostics:
|
| 398 |
+
"""Getting the surface pressure of the state."""
|
| 399 |
+
|
| 400 |
+
def __init__(
|
| 401 |
+
self,
|
| 402 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 403 |
+
dt: float,
|
| 404 |
+
physics_specs: Any,
|
| 405 |
+
aux_features: dict[str, Any],
|
| 406 |
+
):
|
| 407 |
+
del dt, aux_features, physics_specs
|
| 408 |
+
self.to_nodal_fn = coords.horizontal.to_nodal
|
| 409 |
+
|
| 410 |
+
def __call__(
|
| 411 |
+
self,
|
| 412 |
+
model_state: typing.ModelState,
|
| 413 |
+
physics_tendencies: typing.Pytree,
|
| 414 |
+
forcing: typing.Forcing | None = None,
|
| 415 |
+
) -> typing.Pytree:
|
| 416 |
+
"""Computes surface pressure."""
|
| 417 |
+
del physics_tendencies, forcing # unused
|
| 418 |
+
lsp = model_state.state.log_surface_pressure
|
| 419 |
+
surface_pressure = jnp.squeeze(jnp.exp(self.to_nodal_fn(lsp)), axis=0)
|
| 420 |
+
return {'surface_pressure': surface_pressure}
|
model/legacy/embeddings.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules that predict an embedding from the model state."""
|
| 15 |
+
from typing import Any, Optional
|
| 16 |
+
from dinosaur import coordinate_systems
|
| 17 |
+
from dinosaur import pytree_utils
|
| 18 |
+
from dinosaur import scales
|
| 19 |
+
from dinosaur import typing
|
| 20 |
+
from dinosaur import xarray_utils
|
| 21 |
+
import gin
|
| 22 |
+
import haiku as hk
|
| 23 |
+
import jax
|
| 24 |
+
import jax.numpy as jnp
|
| 25 |
+
from model.legacy import features
|
| 26 |
+
from model.legacy import mappings
|
| 27 |
+
from model.legacy import transforms
|
| 28 |
+
|
| 29 |
+
EmbeddingFn = typing.EmbeddingFn
|
| 30 |
+
EmbeddingModule = typing.EmbeddingModule
|
| 31 |
+
Forcing = typing.Forcing
|
| 32 |
+
TransformModule = typing.TransformModule
|
| 33 |
+
|
| 34 |
+
units = scales.units
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@gin.register
|
| 38 |
+
class ModalToNodalEmbedding(hk.Module):
|
| 39 |
+
"""Embedding that expects modal state input and returns nodal output."""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 44 |
+
dt: float,
|
| 45 |
+
physics_specs: Any,
|
| 46 |
+
aux_features: typing.AuxFeatures,
|
| 47 |
+
output_shapes: typing.Pytree,
|
| 48 |
+
modal_to_nodal_features_module: features.FeaturesModule,
|
| 49 |
+
nodal_mapping_module: mappings.MappingModule,
|
| 50 |
+
output_transform_module: TransformModule = transforms.IdentityTransform,
|
| 51 |
+
name: Optional[str] = None,
|
| 52 |
+
):
|
| 53 |
+
super().__init__(name=name)
|
| 54 |
+
self.coords = coords
|
| 55 |
+
self.output_shapes = output_shapes
|
| 56 |
+
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
|
| 57 |
+
coords, dt, physics_specs, aux_features
|
| 58 |
+
)
|
| 59 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 60 |
+
self.output_transform_fn = output_transform_module(
|
| 61 |
+
coords, dt, physics_specs, aux_features
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def __call__(
|
| 65 |
+
self,
|
| 66 |
+
state: typing.Pytree,
|
| 67 |
+
memory: Optional[typing.Pytree] = None,
|
| 68 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 69 |
+
randomness: Optional[typing.Pytree] = None,
|
| 70 |
+
forcing: Optional[typing.Forcing] = None,
|
| 71 |
+
) -> typing.Pytree:
|
| 72 |
+
"""Returns the embedding output on nodal locations."""
|
| 73 |
+
net = self.nodal_mapping_module(self.output_shapes)
|
| 74 |
+
# Need to check if dict when embedding is not within the parameterization
|
| 75 |
+
# (e.g., for diagnostic NN)
|
| 76 |
+
state, _ = pytree_utils.as_dict(state)
|
| 77 |
+
nodal_inputs = self.modal_to_nodal_features_fn(
|
| 78 |
+
state, memory, diagnostics, randomness, forcing
|
| 79 |
+
)
|
| 80 |
+
nodal_outputs = self.output_transform_fn(net(nodal_inputs))
|
| 81 |
+
return nodal_outputs
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# TODO(pnorgaard) Refactor default embeddings to separate object
|
| 85 |
+
@gin.register
|
| 86 |
+
class NodalSurfaceModelEmbedding(hk.Module):
|
| 87 |
+
"""Embedding to represent a nodal space surface model."""
|
| 88 |
+
|
| 89 |
+
def __init__(
|
| 90 |
+
self,
|
| 91 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 92 |
+
dt: float,
|
| 93 |
+
physics_specs: Any,
|
| 94 |
+
aux_features: typing.AuxFeatures,
|
| 95 |
+
output_shapes: typing.Pytree,
|
| 96 |
+
static_vars_ds_path: str,
|
| 97 |
+
land_embedding: Optional[EmbeddingModule] = None,
|
| 98 |
+
sea_embedding: Optional[EmbeddingModule] = None,
|
| 99 |
+
sea_ice_embedding: Optional[EmbeddingModule] = None,
|
| 100 |
+
snow_embedding: Optional[EmbeddingModule] = None,
|
| 101 |
+
output_transform_module: TransformModule = transforms.IdentityTransform,
|
| 102 |
+
name: Optional[str] = None,
|
| 103 |
+
):
|
| 104 |
+
super().__init__(name=name)
|
| 105 |
+
self.coords = coords
|
| 106 |
+
self.output_shapes = output_shapes
|
| 107 |
+
|
| 108 |
+
# Basic surface embedding settings
|
| 109 |
+
self.feature_axis = -3
|
| 110 |
+
param_init = hk.initializers.TruncatedNormal()
|
| 111 |
+
output_size = sum([x[self.feature_axis]
|
| 112 |
+
for x in jax.tree_util.tree_leaves(output_shapes)])
|
| 113 |
+
param_shape = (output_size, 1, 1) # uniform across lon, lat
|
| 114 |
+
surface_nodal_shape = self.coords.surface_nodal_shape
|
| 115 |
+
|
| 116 |
+
if land_embedding is not None:
|
| 117 |
+
self.land_embedding_fn = land_embedding(
|
| 118 |
+
coords=coords,
|
| 119 |
+
dt=dt,
|
| 120 |
+
physics_specs=physics_specs,
|
| 121 |
+
aux_features=aux_features,
|
| 122 |
+
output_shapes=output_shapes,
|
| 123 |
+
)
|
| 124 |
+
else:
|
| 125 |
+
self.land_parameters = hk.get_parameter(
|
| 126 |
+
'land_params', param_shape,
|
| 127 |
+
jnp.float32, init=param_init)
|
| 128 |
+
def land_embedding_fn(state, memory, randomness, forcing):
|
| 129 |
+
del state, memory, randomness, forcing # unused
|
| 130 |
+
outputs = self.land_parameters * jnp.ones(surface_nodal_shape)
|
| 131 |
+
return pytree_utils.unpack_to_pytree(
|
| 132 |
+
outputs, self.output_shapes, self.feature_axis
|
| 133 |
+
)
|
| 134 |
+
self.land_embedding_fn = land_embedding_fn
|
| 135 |
+
|
| 136 |
+
if sea_embedding is not None:
|
| 137 |
+
self.sea_embedding_fn = sea_embedding(
|
| 138 |
+
coords=coords,
|
| 139 |
+
dt=dt,
|
| 140 |
+
physics_specs=physics_specs,
|
| 141 |
+
aux_features=aux_features,
|
| 142 |
+
output_shapes=output_shapes,
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
self.sea_parameters = hk.get_parameter(
|
| 146 |
+
'sea_params', param_shape,
|
| 147 |
+
jnp.float32, init=param_init)
|
| 148 |
+
def sea_embedding_fn(state, memory, randomness, forcing):
|
| 149 |
+
del state, memory, randomness, forcing # unused
|
| 150 |
+
outputs = self.sea_parameters * jnp.ones(surface_nodal_shape)
|
| 151 |
+
return pytree_utils.unpack_to_pytree(
|
| 152 |
+
outputs, self.output_shapes, self.feature_axis
|
| 153 |
+
)
|
| 154 |
+
self.sea_embedding_fn = sea_embedding_fn
|
| 155 |
+
|
| 156 |
+
if sea_ice_embedding is not None:
|
| 157 |
+
self.sea_ice_embedding_fn = sea_ice_embedding(
|
| 158 |
+
coords=coords,
|
| 159 |
+
dt=dt,
|
| 160 |
+
physics_specs=physics_specs,
|
| 161 |
+
aux_features=aux_features,
|
| 162 |
+
output_shapes=output_shapes,
|
| 163 |
+
)
|
| 164 |
+
else:
|
| 165 |
+
self.sea_ice_parameters = hk.get_parameter(
|
| 166 |
+
'sea_ice_params', param_shape,
|
| 167 |
+
jnp.float32, init=param_init)
|
| 168 |
+
def sea_ice_embedding_fn(state, memory, randomness, forcing):
|
| 169 |
+
del state, memory, randomness, forcing # unused
|
| 170 |
+
outputs = self.sea_ice_parameters * jnp.ones(surface_nodal_shape)
|
| 171 |
+
return pytree_utils.unpack_to_pytree(
|
| 172 |
+
outputs, self.output_shapes, self.feature_axis
|
| 173 |
+
)
|
| 174 |
+
self.sea_ice_embedding_fn = sea_ice_embedding_fn
|
| 175 |
+
|
| 176 |
+
if snow_embedding is not None:
|
| 177 |
+
self.snow_embedding_fn = snow_embedding(
|
| 178 |
+
coords=coords,
|
| 179 |
+
dt=dt,
|
| 180 |
+
physics_specs=physics_specs,
|
| 181 |
+
aux_features=aux_features,
|
| 182 |
+
output_shapes=output_shapes,
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
self.snow_parameters = hk.get_parameter(
|
| 186 |
+
'snow_params', param_shape,
|
| 187 |
+
jnp.float32, init=param_init)
|
| 188 |
+
def snow_embedding_fn(state, memory, randomness, forcing):
|
| 189 |
+
del state, memory, randomness, forcing # unused
|
| 190 |
+
outputs = self.snow_parameters * jnp.ones(surface_nodal_shape)
|
| 191 |
+
return pytree_utils.unpack_to_pytree(
|
| 192 |
+
outputs, self.output_shapes, self.feature_axis
|
| 193 |
+
)
|
| 194 |
+
self.snow_embedding_fn = snow_embedding_fn
|
| 195 |
+
|
| 196 |
+
self.output_transform_fn = output_transform_module(
|
| 197 |
+
coords, dt, physics_specs, aux_features
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
ds = xarray_utils.ds_from_path_or_aux(static_vars_ds_path, aux_features)
|
| 201 |
+
self.land_sea_mask = xarray_utils.nodal_land_sea_mask_from_ds(ds)
|
| 202 |
+
|
| 203 |
+
# snow data is provided as depth (in meters). It is converted to snow_cover
|
| 204 |
+
# by choosing a threshold such that snow_cover = 0 below that value and
|
| 205 |
+
# snow cover = 1 above that value.
|
| 206 |
+
self.snow_cover_threshold = physics_specs.nondimensionalize(1 * units.meter) # pyrefly: ignore[unsupported-operation]
|
| 207 |
+
|
| 208 |
+
def __call__(
|
| 209 |
+
self,
|
| 210 |
+
state: typing.Pytree,
|
| 211 |
+
memory: Optional[typing.Pytree] = None,
|
| 212 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 213 |
+
randomness: Optional[typing.Pytree] = None,
|
| 214 |
+
forcing: Optional[typing.Forcing] = None,
|
| 215 |
+
) -> typing.Pytree:
|
| 216 |
+
"""Returns the embedding output on nodal locations."""
|
| 217 |
+
land_outputs = self.land_embedding_fn(
|
| 218 |
+
state, memory, diagnostics, randomness, forcing) # pyrefly: ignore[bad-argument-count]
|
| 219 |
+
sea_outputs = self.sea_embedding_fn(
|
| 220 |
+
state, memory, diagnostics, randomness, forcing) # pyrefly: ignore[bad-argument-count]
|
| 221 |
+
sea_ice_outputs = self.sea_ice_embedding_fn(
|
| 222 |
+
state, memory, diagnostics, randomness, forcing # pyrefly: ignore[bad-argument-count]
|
| 223 |
+
)
|
| 224 |
+
snow_outputs = self.snow_embedding_fn(
|
| 225 |
+
state, memory, diagnostics, randomness, forcing) # pyrefly: ignore[bad-argument-count]
|
| 226 |
+
|
| 227 |
+
# prepare masks with fractional values in [0, 1]
|
| 228 |
+
land_fraction = self.land_sea_mask
|
| 229 |
+
sea_fraction = 1 - land_fraction
|
| 230 |
+
sea_ice_fraction = forcing[xarray_utils.SEA_ICE_COVER] # pyrefly: ignore[unsupported-operation]
|
| 231 |
+
snow_fraction = forcing[xarray_utils.SNOW_DEPTH] > self.snow_cover_threshold # pyrefly: ignore[unsupported-operation]
|
| 232 |
+
|
| 233 |
+
# weight and combine outputs
|
| 234 |
+
snow_weight = snow_fraction * land_fraction # snow covered land
|
| 235 |
+
land_weight = (1 - snow_fraction) * land_fraction # land without snow
|
| 236 |
+
sea_ice_weight = sea_ice_fraction * sea_fraction # ice covered sea
|
| 237 |
+
sea_weight = (1 - sea_ice_fraction) * sea_fraction # sea without ice
|
| 238 |
+
|
| 239 |
+
def tree_scale(a, x):
|
| 240 |
+
# Multiply leaves of `x` by `a`.
|
| 241 |
+
return jax.tree_util.tree_map(lambda y: a * y, x)
|
| 242 |
+
|
| 243 |
+
surface_outputs = jax.tree_util.tree_map(
|
| 244 |
+
lambda a, b, c, d: a + b + c + d,
|
| 245 |
+
tree_scale(land_weight, land_outputs),
|
| 246 |
+
tree_scale(sea_weight, sea_outputs),
|
| 247 |
+
tree_scale(sea_ice_weight, sea_ice_outputs),
|
| 248 |
+
tree_scale(snow_weight, snow_outputs),
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
return self.output_transform_fn(surface_outputs)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
@gin.register
|
| 255 |
+
class NodalLandSeaIceEmbedding(hk.Module):
|
| 256 |
+
"""Embedding to represent a nodal land/sea/sea-ice surface."""
|
| 257 |
+
|
| 258 |
+
def __init__(
|
| 259 |
+
self,
|
| 260 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 261 |
+
dt: float,
|
| 262 |
+
physics_specs: Any,
|
| 263 |
+
aux_features: typing.AuxFeatures,
|
| 264 |
+
output_shapes: typing.Pytree,
|
| 265 |
+
static_vars_ds_path: str,
|
| 266 |
+
land_embedding: Optional[EmbeddingModule] = None,
|
| 267 |
+
sea_embedding: Optional[EmbeddingModule] = None,
|
| 268 |
+
sea_ice_embedding: Optional[EmbeddingModule] = None,
|
| 269 |
+
output_transform_module: TransformModule = transforms.IdentityTransform,
|
| 270 |
+
name: Optional[str] = None,
|
| 271 |
+
):
|
| 272 |
+
super().__init__(name=name)
|
| 273 |
+
self.coords = coords
|
| 274 |
+
self.output_shapes = output_shapes
|
| 275 |
+
|
| 276 |
+
# Basic surface embedding settings
|
| 277 |
+
self.feature_axis = -3
|
| 278 |
+
surface_nodal_shape = self.coords.surface_nodal_shape
|
| 279 |
+
param_init = hk.initializers.TruncatedNormal()
|
| 280 |
+
output_size = sum([x[self.feature_axis]
|
| 281 |
+
for x in jax.tree_util.tree_leaves(output_shapes)])
|
| 282 |
+
uniform_param_shape = (output_size, 1, 1) # uniform across lon, lat
|
| 283 |
+
# Alternative for lon,lat dependent parameters, e.g. for land model
|
| 284 |
+
# spatial_params_shape = (output_size, surface_nodal_shape[-2:])
|
| 285 |
+
|
| 286 |
+
def get_parameters_fn(
|
| 287 |
+
shape: tuple[int, int, int],
|
| 288 |
+
name: str = ''):
|
| 289 |
+
parameters = hk.get_parameter(
|
| 290 |
+
name + '_params', shape, jnp.float32, init=param_init
|
| 291 |
+
)
|
| 292 |
+
def parameters_fn(state, memory, diagnostics, randomness, forcing):
|
| 293 |
+
del state, memory, diagnostics, randomness, forcing # unused
|
| 294 |
+
outputs = parameters * jnp.ones(surface_nodal_shape)
|
| 295 |
+
return pytree_utils.unpack_to_pytree(
|
| 296 |
+
outputs, output_shapes, self.feature_axis,
|
| 297 |
+
)
|
| 298 |
+
return parameters_fn
|
| 299 |
+
|
| 300 |
+
if land_embedding is not None:
|
| 301 |
+
self.land_embedding_fn = land_embedding(
|
| 302 |
+
coords=coords,
|
| 303 |
+
dt=dt,
|
| 304 |
+
physics_specs=physics_specs,
|
| 305 |
+
aux_features=aux_features,
|
| 306 |
+
output_shapes=output_shapes,
|
| 307 |
+
)
|
| 308 |
+
else:
|
| 309 |
+
self.land_embedding_fn = get_parameters_fn(uniform_param_shape, 'land')
|
| 310 |
+
|
| 311 |
+
if sea_embedding is not None:
|
| 312 |
+
self.sea_embedding_fn = sea_embedding(
|
| 313 |
+
coords=coords,
|
| 314 |
+
dt=dt,
|
| 315 |
+
physics_specs=physics_specs,
|
| 316 |
+
aux_features=aux_features,
|
| 317 |
+
output_shapes=output_shapes,
|
| 318 |
+
)
|
| 319 |
+
else:
|
| 320 |
+
self.sea_embedding_fn = get_parameters_fn(uniform_param_shape, 'sea')
|
| 321 |
+
|
| 322 |
+
if sea_ice_embedding is not None:
|
| 323 |
+
self.sea_ice_embedding_fn = sea_ice_embedding(
|
| 324 |
+
coords=coords,
|
| 325 |
+
dt=dt,
|
| 326 |
+
physics_specs=physics_specs,
|
| 327 |
+
aux_features=aux_features,
|
| 328 |
+
output_shapes=output_shapes,
|
| 329 |
+
)
|
| 330 |
+
else:
|
| 331 |
+
self.sea_ice_embedding_fn = get_parameters_fn(
|
| 332 |
+
uniform_param_shape, 'sea_ice'
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
self.output_transform_fn = output_transform_module(
|
| 336 |
+
coords, dt, physics_specs, aux_features
|
| 337 |
+
)
|
| 338 |
+
ds = xarray_utils.ds_from_path_or_aux(static_vars_ds_path, aux_features)
|
| 339 |
+
self.land_sea_mask = xarray_utils.nodal_land_sea_mask_from_ds(ds)
|
| 340 |
+
|
| 341 |
+
def __call__(
|
| 342 |
+
self,
|
| 343 |
+
state: typing.Pytree,
|
| 344 |
+
memory: Optional[typing.Pytree] = None,
|
| 345 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 346 |
+
randomness: Optional[typing.Pytree] = None,
|
| 347 |
+
forcing: Optional[typing.Forcing] = None,
|
| 348 |
+
) -> typing.Pytree:
|
| 349 |
+
"""Returns the embedding output on nodal locations."""
|
| 350 |
+
# get outputs from each model
|
| 351 |
+
land_outputs = self.land_embedding_fn(
|
| 352 |
+
state, memory, diagnostics, randomness, forcing)
|
| 353 |
+
sea_outputs = self.sea_embedding_fn(
|
| 354 |
+
state, memory, diagnostics, randomness, forcing)
|
| 355 |
+
sea_ice_outputs = self.sea_ice_embedding_fn(
|
| 356 |
+
state, memory, diagnostics, randomness, forcing
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# prepare masks with fractional values in [0, 1]
|
| 360 |
+
land_fraction = self.land_sea_mask
|
| 361 |
+
sea_fraction = 1 - land_fraction
|
| 362 |
+
sea_ice_fraction = forcing[xarray_utils.SEA_ICE_COVER] # pyrefly: ignore[unsupported-operation]
|
| 363 |
+
|
| 364 |
+
# weight and combine outputs
|
| 365 |
+
land_weight = land_fraction
|
| 366 |
+
sea_ice_weight = sea_ice_fraction * sea_fraction # ice covered sea
|
| 367 |
+
sea_weight = (1 - sea_ice_fraction) * sea_fraction # sea without ice
|
| 368 |
+
|
| 369 |
+
def tree_scale(a, x):
|
| 370 |
+
# Multiply leaves of `x` by `a`.
|
| 371 |
+
return jax.tree_util.tree_map(lambda y: a * y, x)
|
| 372 |
+
|
| 373 |
+
surface_outputs = jax.tree_util.tree_map(
|
| 374 |
+
lambda a, b, c: a + b + c,
|
| 375 |
+
tree_scale(land_weight, land_outputs),
|
| 376 |
+
tree_scale(sea_weight, sea_outputs),
|
| 377 |
+
tree_scale(sea_ice_weight, sea_ice_outputs),
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
return self.output_transform_fn(surface_outputs)
|
model/legacy/encoders.py
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines `encoder` modules that map input trajectories to model states.
|
| 15 |
+
|
| 16 |
+
All encoder modules return the encoder-specific model state that represents the
|
| 17 |
+
state of the system at the latest time provided in the input trajectory.
|
| 18 |
+
The inputs are expected to consist of arrays with `time` as a leading axis.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# TODO(dkochkov) make all encoders take in trajectories and return ModelState.
|
| 22 |
+
|
| 23 |
+
import functools
|
| 24 |
+
from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union
|
| 25 |
+
|
| 26 |
+
from dinosaur import coordinate_systems
|
| 27 |
+
from dinosaur import primitive_equations
|
| 28 |
+
from dinosaur import pytree_utils
|
| 29 |
+
from dinosaur import scales
|
| 30 |
+
from dinosaur import shallow_water
|
| 31 |
+
from dinosaur import spherical_harmonic
|
| 32 |
+
from dinosaur import typing
|
| 33 |
+
from dinosaur import vertical_interpolation
|
| 34 |
+
from dinosaur import weatherbench_utils
|
| 35 |
+
from dinosaur import xarray_utils
|
| 36 |
+
import gin
|
| 37 |
+
import haiku as hk
|
| 38 |
+
import jax
|
| 39 |
+
import jax.numpy as jnp
|
| 40 |
+
from model.legacy import features
|
| 41 |
+
from model.legacy import mappings
|
| 42 |
+
from model.legacy import orographies
|
| 43 |
+
from model.legacy import perturbations
|
| 44 |
+
from model.legacy import stochastic
|
| 45 |
+
from model.legacy import transforms
|
| 46 |
+
import numpy as np
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
Array = Union[np.ndarray, jnp.ndarray]
|
| 50 |
+
DataState = typing.DataState
|
| 51 |
+
FeaturesModule = features.FeaturesModule
|
| 52 |
+
FilterModule = Callable[..., typing.PyTreeFilterFn]
|
| 53 |
+
Forcing = typing.Forcing
|
| 54 |
+
MappingModule = mappings.MappingModule
|
| 55 |
+
PyTreeState = typing.PyTreeState
|
| 56 |
+
ModelState = typing.ModelState
|
| 57 |
+
TransformModule = typing.TransformModule
|
| 58 |
+
OrographyModule = orographies.OrographyModule
|
| 59 |
+
PerturbationModule = perturbations.PerturbationModule
|
| 60 |
+
RandomnessModule = stochastic.RandomnessModule
|
| 61 |
+
|
| 62 |
+
# We ♥ λ's
|
| 63 |
+
# pylint: disable=g-long-lambda
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@gin.register
|
| 67 |
+
class EncoderIdentityTransform(hk.Module):
|
| 68 |
+
"""Transformation that returns inputs without modification."""
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 73 |
+
dt: float,
|
| 74 |
+
physics_specs: Any,
|
| 75 |
+
aux_features: Dict[str, Any],
|
| 76 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 77 |
+
name: Optional[str] = None,
|
| 78 |
+
):
|
| 79 |
+
super().__init__(name=name)
|
| 80 |
+
del coords, dt, physics_specs, aux_features, input_coords
|
| 81 |
+
|
| 82 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 83 |
+
return inputs
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@gin.register
|
| 87 |
+
class EncoderFilterTransform(hk.Module):
|
| 88 |
+
"""Transformation that returns truncated and filtered modal inputs."""
|
| 89 |
+
|
| 90 |
+
def __init__(
|
| 91 |
+
self,
|
| 92 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 93 |
+
dt: float,
|
| 94 |
+
physics_specs: Any,
|
| 95 |
+
aux_features: Dict[str, Any],
|
| 96 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 97 |
+
filter_modules: Sequence[FilterModule] = tuple(),
|
| 98 |
+
name: Optional[str] = None,
|
| 99 |
+
):
|
| 100 |
+
super().__init__(name=name)
|
| 101 |
+
self.filter_fns = [
|
| 102 |
+
module(coords, dt, physics_specs, aux_features)
|
| 103 |
+
for module in filter_modules
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 107 |
+
for filter_fn in self.filter_fns:
|
| 108 |
+
inputs = filter_fn(inputs)
|
| 109 |
+
return inputs
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@gin.register
|
| 113 |
+
class InputClipTransform(hk.Module):
|
| 114 |
+
"""Filter that clips highest total wavenumber the input state."""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 119 |
+
dt: float,
|
| 120 |
+
physics_specs: Any,
|
| 121 |
+
aux_features: Dict[str, Any],
|
| 122 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 123 |
+
wavenumbers_to_clip: int = 1,
|
| 124 |
+
name: Optional[str] = None,
|
| 125 |
+
):
|
| 126 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 127 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 128 |
+
super().__init__(name=name)
|
| 129 |
+
self.input_coords = input_coords
|
| 130 |
+
self.wavenumbers_to_clip = wavenumbers_to_clip
|
| 131 |
+
|
| 132 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 133 |
+
return self.input_coords.horizontal.clip_wavenumbers(
|
| 134 |
+
inputs, self.wavenumbers_to_clip
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@gin.register
|
| 139 |
+
class InputNodalToModalTransform(hk.Module):
|
| 140 |
+
"""Transformation that converts nodal inputs to modal representation."""
|
| 141 |
+
|
| 142 |
+
def __init__(
|
| 143 |
+
self,
|
| 144 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 145 |
+
dt: float,
|
| 146 |
+
physics_specs: Any,
|
| 147 |
+
aux_features: Dict[str, Any],
|
| 148 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 149 |
+
name: Optional[str] = None,
|
| 150 |
+
):
|
| 151 |
+
super().__init__(name=name)
|
| 152 |
+
self.coords = coords
|
| 153 |
+
self.input_coords = input_coords
|
| 154 |
+
|
| 155 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 156 |
+
to_modal_fn = self.input_coords.horizontal.to_modal
|
| 157 |
+
downsample_fn = coordinate_systems.get_spectral_downsample_fn(
|
| 158 |
+
self.input_coords, self.coords, expect_same_vertical=False
|
| 159 |
+
)
|
| 160 |
+
return jax.tree_util.tree_map(
|
| 161 |
+
lambda x: downsample_fn(to_modal_fn(x)), inputs
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@gin.register
|
| 166 |
+
class ModalInputLearnedAdaptorTransform(hk.Module):
|
| 167 |
+
"""Transformation using a tower to adapt modal inputs to the model domain."""
|
| 168 |
+
|
| 169 |
+
def __init__(
|
| 170 |
+
self,
|
| 171 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 172 |
+
dt: float,
|
| 173 |
+
physics_specs: Any,
|
| 174 |
+
aux_features: Dict[str, Any],
|
| 175 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 176 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 177 |
+
nodal_mapping_module: MappingModule,
|
| 178 |
+
output_transform_module: TransformModule,
|
| 179 |
+
name: Optional[str] = None,
|
| 180 |
+
):
|
| 181 |
+
del input_coords # unused.
|
| 182 |
+
super().__init__(name=name)
|
| 183 |
+
self.coords = coords
|
| 184 |
+
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
|
| 185 |
+
coords, dt, physics_specs, aux_features
|
| 186 |
+
)
|
| 187 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 188 |
+
self.output_transform_fn = output_transform_module(
|
| 189 |
+
coords, dt, physics_specs, aux_features
|
| 190 |
+
)
|
| 191 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 192 |
+
x, coords
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 196 |
+
"""Applies transform to modal inputs, returns modal outputs."""
|
| 197 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 198 |
+
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
|
| 199 |
+
# if `inputs` contain `sim_time` - remove it from corrections.
|
| 200 |
+
sim_time_shape = prediction_shapes.pop('sim_time', None)
|
| 201 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 202 |
+
nodal_input_features = self.modal_to_nodal_features_fn(inputs, None)
|
| 203 |
+
nodal_corrections = self.output_transform_fn(net(nodal_input_features))
|
| 204 |
+
corrections = self.coords.horizontal.to_modal(nodal_corrections)
|
| 205 |
+
if sim_time_shape is not None:
|
| 206 |
+
corrections['sim_time'] = 0.0
|
| 207 |
+
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
|
| 208 |
+
return from_dict_fn(outputs)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@gin.register
|
| 212 |
+
class NodalInputLearnedAdaptorTransform(hk.Module):
|
| 213 |
+
"""Transformation using a tower to adapt nodal inputs to the model domain."""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 218 |
+
dt: float,
|
| 219 |
+
physics_specs: Any,
|
| 220 |
+
aux_features: Dict[str, Any],
|
| 221 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 222 |
+
nodal_to_nodal_features_module: FeaturesModule,
|
| 223 |
+
nodal_mapping_module: MappingModule,
|
| 224 |
+
output_transform_module: TransformModule,
|
| 225 |
+
name: Optional[str] = None,
|
| 226 |
+
):
|
| 227 |
+
del input_coords # unused.
|
| 228 |
+
super().__init__(name=name)
|
| 229 |
+
self.coords = coords
|
| 230 |
+
self.nodal_to_nodal_features_fn = nodal_to_nodal_features_module(
|
| 231 |
+
coords, dt, physics_specs, aux_features
|
| 232 |
+
)
|
| 233 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 234 |
+
self.output_transform_fn = output_transform_module(
|
| 235 |
+
coords, dt, physics_specs, aux_features
|
| 236 |
+
)
|
| 237 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 238 |
+
x, coords
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 242 |
+
"""Applies transform to nodal inputs, returns nodal outputs."""
|
| 243 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 244 |
+
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
|
| 245 |
+
# if `inputs` contain `sim_time` - remove it from corrections.
|
| 246 |
+
sim_time_shape = prediction_shapes.pop('sim_time', None)
|
| 247 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 248 |
+
input_features = self.nodal_to_nodal_features_fn(inputs, None)
|
| 249 |
+
corrections = self.output_transform_fn(net(input_features))
|
| 250 |
+
if sim_time_shape is not None:
|
| 251 |
+
corrections['sim_time'] = 0.0
|
| 252 |
+
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
|
| 253 |
+
return from_dict_fn(outputs)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
@gin.register
|
| 257 |
+
class EncoderCombinedTransform(hk.Module):
|
| 258 |
+
"""Module that applies multiple transformations sequentially."""
|
| 259 |
+
|
| 260 |
+
def __init__(
|
| 261 |
+
self,
|
| 262 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 263 |
+
dt: float,
|
| 264 |
+
physics_specs: Any,
|
| 265 |
+
aux_features: typing.AuxFeatures,
|
| 266 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 267 |
+
transforms: Tuple[TransformModule, ...] = tuple(), # pylint: disable=redefined-outer-name
|
| 268 |
+
name: Optional[str] = None,
|
| 269 |
+
):
|
| 270 |
+
super().__init__(name=name)
|
| 271 |
+
self.transform_fns = [
|
| 272 |
+
module(coords, dt, physics_specs, aux_features, input_coords)
|
| 273 |
+
for module in transforms
|
| 274 |
+
]
|
| 275 |
+
|
| 276 |
+
def __call__(self, inputs: PyTreeState) -> PyTreeState:
|
| 277 |
+
for transform_fn in self.transform_fns:
|
| 278 |
+
inputs = transform_fn(inputs)
|
| 279 |
+
return inputs
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
@gin.register
|
| 283 |
+
class ShallowWaterStateEncoder(hk.Module):
|
| 284 |
+
"""Encoder that extracts shallow_water.State pair from inputs."""
|
| 285 |
+
|
| 286 |
+
def __init__(
|
| 287 |
+
self,
|
| 288 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 289 |
+
dt: float,
|
| 290 |
+
physics_specs: Any,
|
| 291 |
+
aux_features: Dict[str, Any],
|
| 292 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 293 |
+
time_axis: int = 0,
|
| 294 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 295 |
+
name: Optional[str] = None,
|
| 296 |
+
):
|
| 297 |
+
super().__init__(name=name)
|
| 298 |
+
self.slice_fn = functools.partial(
|
| 299 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=-1)
|
| 300 |
+
self.transform_fn = transform_module(
|
| 301 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
def __call__(
|
| 305 |
+
self, inputs: DataState, forcing: Forcing
|
| 306 |
+
) -> shallow_water.State:
|
| 307 |
+
del forcing
|
| 308 |
+
state = self.transform_fn(shallow_water.State(**self.slice_fn(inputs)))
|
| 309 |
+
return ModelState(state) # pyrefly: ignore[bad-argument-count, bad-return]
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
@gin.register
|
| 313 |
+
class ShallowWaterLeapfrogEncoder(hk.Module):
|
| 314 |
+
"""Encoder that extracts shallow_water.State pair from inputs."""
|
| 315 |
+
|
| 316 |
+
def __init__(
|
| 317 |
+
self,
|
| 318 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 319 |
+
dt: float,
|
| 320 |
+
physics_specs: Any,
|
| 321 |
+
aux_features: Dict[str, Any],
|
| 322 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 323 |
+
time_axis: int = 0,
|
| 324 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 325 |
+
name: Optional[str] = None,
|
| 326 |
+
):
|
| 327 |
+
super().__init__(name=name)
|
| 328 |
+
self.slice_fn = functools.partial(
|
| 329 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=slice(-2, None))
|
| 330 |
+
self.time_axis = time_axis
|
| 331 |
+
self.transform_fn = transform_module(
|
| 332 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
def __call__(
|
| 336 |
+
self, inputs: DataState, forcing: Forcing
|
| 337 |
+
) -> Tuple[shallow_water.State, ...]:
|
| 338 |
+
del forcing
|
| 339 |
+
last_two_frames = pytree_utils.split_axis(
|
| 340 |
+
self.slice_fn(inputs), self.time_axis
|
| 341 |
+
)
|
| 342 |
+
state = self.transform_fn(
|
| 343 |
+
tuple(shallow_water.State(**items) for items in last_two_frames)
|
| 344 |
+
)
|
| 345 |
+
return ModelState(state) # pyrefly: ignore[bad-argument-count, bad-return]
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
@gin.register
|
| 349 |
+
class PrimitiveEquationStateEncoder(hk.Module):
|
| 350 |
+
"""Encoder that extracts primitive_equations.State from inputs."""
|
| 351 |
+
|
| 352 |
+
def __init__(
|
| 353 |
+
self,
|
| 354 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 355 |
+
dt: float,
|
| 356 |
+
physics_specs: Any,
|
| 357 |
+
aux_features: Dict[str, Any],
|
| 358 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 359 |
+
time_axis: int = 0,
|
| 360 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 361 |
+
name: Optional[str] = None,
|
| 362 |
+
):
|
| 363 |
+
super().__init__(name=name)
|
| 364 |
+
self.slice_fn = functools.partial(
|
| 365 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=-1)
|
| 366 |
+
self.transform_fn = transform_module(
|
| 367 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
def __call__(
|
| 371 |
+
self, inputs: DataState, forcing: Forcing
|
| 372 |
+
) -> primitive_equations.State:
|
| 373 |
+
del forcing
|
| 374 |
+
state = self.transform_fn(
|
| 375 |
+
primitive_equations.State(**self.slice_fn(inputs)))
|
| 376 |
+
return ModelState(state) # pyrefly: ignore[bad-argument-count, bad-return]
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
@gin.register
|
| 380 |
+
class PrimitiveEquationLeapfrogEncoder(hk.Module):
|
| 381 |
+
"""Encoder that extracts primitive_equations.State pair from inputs."""
|
| 382 |
+
|
| 383 |
+
def __init__(
|
| 384 |
+
self,
|
| 385 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 386 |
+
dt: float,
|
| 387 |
+
physics_specs: Any,
|
| 388 |
+
aux_features: Dict[str, Any],
|
| 389 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 390 |
+
time_axis: int = 0,
|
| 391 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 392 |
+
name: Optional[str] = None,
|
| 393 |
+
):
|
| 394 |
+
super().__init__(name=name)
|
| 395 |
+
self.slice_fn = functools.partial(
|
| 396 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=slice(-2, None))
|
| 397 |
+
self.time_axis = time_axis
|
| 398 |
+
self.transform_fn = transform_module(
|
| 399 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
def __call__(
|
| 403 |
+
self, inputs: DataState, forcing: Forcing
|
| 404 |
+
) -> Tuple[primitive_equations.State, ...]:
|
| 405 |
+
del forcing
|
| 406 |
+
last_two_frames = pytree_utils.split_axis(
|
| 407 |
+
self.slice_fn(inputs), self.time_axis
|
| 408 |
+
)
|
| 409 |
+
state = self.transform_fn(
|
| 410 |
+
tuple(primitive_equations.State(**items) for items in last_two_frames)
|
| 411 |
+
)
|
| 412 |
+
return ModelState(state) # pyrefly: ignore[bad-argument-count, bad-return]
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
@gin.register
|
| 416 |
+
class PrimitiveEquationStateWithTimeEncoder(hk.Module):
|
| 417 |
+
"""Encoder that extracts primitive_equations.StateWithTime from inputs."""
|
| 418 |
+
|
| 419 |
+
def __init__(
|
| 420 |
+
self,
|
| 421 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 422 |
+
dt: float,
|
| 423 |
+
physics_specs: Any,
|
| 424 |
+
aux_features: Dict[str, Any],
|
| 425 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 426 |
+
time_axis: int = 0,
|
| 427 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 428 |
+
name: Optional[str] = None,
|
| 429 |
+
):
|
| 430 |
+
super().__init__(name=name)
|
| 431 |
+
self.slice_fn = functools.partial(
|
| 432 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=-1)
|
| 433 |
+
self.transform_fn = transform_module(
|
| 434 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
def __call__(
|
| 438 |
+
self, inputs: DataState, forcing: Forcing
|
| 439 |
+
) -> ModelState:
|
| 440 |
+
del forcing
|
| 441 |
+
sliced_inputs = self.slice_fn(inputs)
|
| 442 |
+
state = self.transform_fn(
|
| 443 |
+
primitive_equations.StateWithTime(**sliced_inputs))
|
| 444 |
+
return ModelState(state) # pyrefly: ignore[bad-argument-count]
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
@gin.register
|
| 448 |
+
class WeatherbenchToPrimitiveEncoder(hk.Module):
|
| 449 |
+
"""Encoder that extracts primitive_equations.StateWithTime from WB inputs."""
|
| 450 |
+
|
| 451 |
+
def __init__(
|
| 452 |
+
self,
|
| 453 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 454 |
+
dt: float,
|
| 455 |
+
physics_specs: Any,
|
| 456 |
+
aux_features: Dict[str, Any],
|
| 457 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 458 |
+
time_axis: int = 0,
|
| 459 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 460 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 461 |
+
name: Optional[str] = None,
|
| 462 |
+
):
|
| 463 |
+
super().__init__(name=name)
|
| 464 |
+
ref_temps = aux_features[xarray_utils.REF_TEMP_KEY]
|
| 465 |
+
self.ref_temps = ref_temps[..., np.newaxis, np.newaxis]
|
| 466 |
+
self.coords = coords
|
| 467 |
+
self.input_coords = input_coords
|
| 468 |
+
self.slice_fn = functools.partial(
|
| 469 |
+
pytree_utils.slice_along_axis, axis=time_axis, idx=-1)
|
| 470 |
+
modal_orography_init_fn = orography_module(
|
| 471 |
+
coords, dt, physics_specs, aux_features
|
| 472 |
+
)
|
| 473 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 474 |
+
self.surface_pressure_fn = functools.partial(
|
| 475 |
+
vertical_interpolation.get_surface_pressure,
|
| 476 |
+
input_coords.vertical,
|
| 477 |
+
orography=input_coords.horizontal.to_nodal(modal_orography),
|
| 478 |
+
gravity_acceleration=physics_specs.gravity_acceleration,
|
| 479 |
+
)
|
| 480 |
+
self.curl_and_div_fn = functools.partial(
|
| 481 |
+
spherical_harmonic.uv_nodal_to_vor_div_modal,
|
| 482 |
+
input_coords.horizontal,
|
| 483 |
+
)
|
| 484 |
+
self.modal_interpolate_fn = coordinate_systems.get_spectral_interpolate_fn(
|
| 485 |
+
input_coords, coords, expect_same_vertical=False
|
| 486 |
+
)
|
| 487 |
+
self.transform_fn = transform_module(
|
| 488 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
def weatherbench_to_primitive(
|
| 492 |
+
self,
|
| 493 |
+
wb_state_nodal: weatherbench_utils.State,
|
| 494 |
+
) -> ModelState:
|
| 495 |
+
"""Converts wb_state on pressure coordinates to primitive on sigma."""
|
| 496 |
+
# Note: the returned values have mixed nodal/modal representations.
|
| 497 |
+
surface_pressure = self.surface_pressure_fn(wb_state_nodal.z)
|
| 498 |
+
interpolate_fn = vertical_interpolation.vectorize_vertical_interpolation(
|
| 499 |
+
vertical_interpolation.vertical_interpolation
|
| 500 |
+
)
|
| 501 |
+
regrid_fn = functools.partial(
|
| 502 |
+
vertical_interpolation.interp_pressure_to_sigma,
|
| 503 |
+
pressure_coords=self.input_coords.vertical,
|
| 504 |
+
sigma_coords=self.coords.vertical,
|
| 505 |
+
surface_pressure=surface_pressure,
|
| 506 |
+
interpolate_fn=interpolate_fn,
|
| 507 |
+
)
|
| 508 |
+
wb_state_on_sigma = regrid_fn(wb_state_nodal)
|
| 509 |
+
u, v = self.coords.physics_to_dycore_sharding(
|
| 510 |
+
(wb_state_on_sigma.u, wb_state_on_sigma.v)
|
| 511 |
+
)
|
| 512 |
+
vorticity, divergence = self.coords.dycore_to_physics_sharding(
|
| 513 |
+
self.curl_and_div_fn(u, v)
|
| 514 |
+
)
|
| 515 |
+
pe_state_on_sigma = primitive_equations.StateWithTime(
|
| 516 |
+
divergence=divergence, # pyrefly: ignore[unexpected-keyword]
|
| 517 |
+
vorticity=vorticity, # pyrefly: ignore[unexpected-keyword]
|
| 518 |
+
temperature_variation=(wb_state_on_sigma.t - self.ref_temps), # pyrefly: ignore[unexpected-keyword]
|
| 519 |
+
log_surface_pressure=jnp.log(surface_pressure), # pyrefly: ignore[unexpected-keyword]
|
| 520 |
+
sim_time=wb_state_on_sigma.sim_time, # pyrefly: ignore[unexpected-keyword]
|
| 521 |
+
tracers=wb_state_on_sigma.tracers, # pyrefly: ignore[unexpected-keyword]
|
| 522 |
+
)
|
| 523 |
+
return pe_state_on_sigma # pyrefly: ignore[bad-return]
|
| 524 |
+
|
| 525 |
+
def __call__(
|
| 526 |
+
self,
|
| 527 |
+
inputs: DataState,
|
| 528 |
+
forcing: Forcing,
|
| 529 |
+
) -> ModelState:
|
| 530 |
+
del forcing
|
| 531 |
+
wb_state = weatherbench_utils.State(**self.slice_fn(inputs))
|
| 532 |
+
wb_state = coordinate_systems.maybe_to_nodal(wb_state, self.input_coords)
|
| 533 |
+
pe_state = self.weatherbench_to_primitive(wb_state)
|
| 534 |
+
pe_state = coordinate_systems.maybe_to_modal(pe_state, self.input_coords)
|
| 535 |
+
pe_state = self.modal_interpolate_fn(pe_state)
|
| 536 |
+
return ModelState(state=self.transform_fn(pe_state)) # pyrefly: ignore[unexpected-keyword]
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
@gin.register
|
| 540 |
+
class LearnedWeatherbenchToPrimitiveEncoder(WeatherbenchToPrimitiveEncoder):
|
| 541 |
+
"""Same as `WeatherbenchToPrimitiveEncoder`, but with learned corrections."""
|
| 542 |
+
|
| 543 |
+
def __init__(
|
| 544 |
+
self,
|
| 545 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 546 |
+
dt: float,
|
| 547 |
+
physics_specs: Any,
|
| 548 |
+
aux_features: Dict[str, Any],
|
| 549 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 550 |
+
modal_to_nodal_data_features_module: FeaturesModule,
|
| 551 |
+
modal_to_nodal_model_features_module: FeaturesModule,
|
| 552 |
+
nodal_mapping_module: MappingModule,
|
| 553 |
+
correction_transform_module: TransformModule,
|
| 554 |
+
prediction_mask: typing.Pytree,
|
| 555 |
+
time_axis: int = 0,
|
| 556 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 557 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 558 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 559 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 560 |
+
name: Optional[str] = None,
|
| 561 |
+
):
|
| 562 |
+
super().__init__(
|
| 563 |
+
coords,
|
| 564 |
+
dt,
|
| 565 |
+
physics_specs,
|
| 566 |
+
aux_features,
|
| 567 |
+
input_coords=input_coords,
|
| 568 |
+
time_axis=time_axis,
|
| 569 |
+
orography_module=orography_module,
|
| 570 |
+
name=name,
|
| 571 |
+
)
|
| 572 |
+
self.prediction_mask = prediction_mask
|
| 573 |
+
# data features are computed in real space on input coordinates.
|
| 574 |
+
self.data_features_fn = modal_to_nodal_data_features_module(
|
| 575 |
+
input_coords, dt, physics_specs, aux_features
|
| 576 |
+
)
|
| 577 |
+
self.model_features_fn = modal_to_nodal_model_features_module(
|
| 578 |
+
coords, dt, physics_specs, aux_features
|
| 579 |
+
)
|
| 580 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 581 |
+
self.output_transform_fn = correction_transform_module(
|
| 582 |
+
input_coords, dt, physics_specs, aux_features
|
| 583 |
+
)
|
| 584 |
+
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
|
| 585 |
+
x, coords
|
| 586 |
+
)
|
| 587 |
+
self.transform_fn = transform_module(
|
| 588 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 589 |
+
)
|
| 590 |
+
self.randomness_fn = randomness_module(
|
| 591 |
+
coords, dt, physics_specs, aux_features
|
| 592 |
+
)
|
| 593 |
+
self.perturbation_fn = perturbation_module(
|
| 594 |
+
coords, dt, physics_specs, aux_features
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
def __call__(
|
| 598 |
+
self,
|
| 599 |
+
inputs: DataState,
|
| 600 |
+
forcing: Forcing,
|
| 601 |
+
) -> ModelState:
|
| 602 |
+
randomness = self.randomness_fn.unconditional_sample(
|
| 603 |
+
hk.maybe_next_rng_key()
|
| 604 |
+
)
|
| 605 |
+
wb_state = self.coords.with_physics_sharding(
|
| 606 |
+
weatherbench_utils.State(**self.slice_fn(inputs))
|
| 607 |
+
)
|
| 608 |
+
wb_state_nodal = self.coords.with_physics_sharding(
|
| 609 |
+
coordinate_systems.maybe_to_nodal(wb_state, self.input_coords)
|
| 610 |
+
)
|
| 611 |
+
wb_state_modal = self.coords.with_physics_sharding(
|
| 612 |
+
coordinate_systems.maybe_to_modal(wb_state, self.input_coords)
|
| 613 |
+
)
|
| 614 |
+
pe_state = self.coords.physics_to_dycore_sharding(
|
| 615 |
+
self.weatherbench_to_primitive(wb_state_nodal)
|
| 616 |
+
)
|
| 617 |
+
# Computing corrections to the primitive_equations state.
|
| 618 |
+
pe_state_modal = coordinate_systems.maybe_to_modal(
|
| 619 |
+
pe_state, self.input_coords
|
| 620 |
+
)
|
| 621 |
+
# we need to interpolate `pe_state_modal` to self.coords to compute
|
| 622 |
+
# features in model space. In most cases this is no-op as grids match.
|
| 623 |
+
pe_state_modal = self.modal_interpolate_fn(pe_state_modal)
|
| 624 |
+
pe_state_nodal = coordinate_systems.maybe_to_nodal(
|
| 625 |
+
pe_state_modal, self.coords
|
| 626 |
+
)
|
| 627 |
+
prediction_shapes = jax.tree_util.tree_map(
|
| 628 |
+
lambda x, y: self.get_nodal_shape_fn(x) if y else None,
|
| 629 |
+
pe_state_nodal.asdict(),
|
| 630 |
+
self.prediction_mask,
|
| 631 |
+
)
|
| 632 |
+
prediction_shapes = primitive_equations.StateWithTime(**prediction_shapes)
|
| 633 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 634 |
+
# we need modal values to compute features for ML corrections.
|
| 635 |
+
data_features = self.data_features_fn(
|
| 636 |
+
wb_state_modal.asdict(), forcing=forcing,
|
| 637 |
+
)
|
| 638 |
+
model_features = self.model_features_fn(
|
| 639 |
+
pe_state_modal.asdict(), forcing=forcing,
|
| 640 |
+
randomness=randomness.nodal_value,
|
| 641 |
+
)
|
| 642 |
+
data_features = transforms.add_prefix(data_features, 'data_')
|
| 643 |
+
model_features = transforms.add_prefix(model_features, 'model_')
|
| 644 |
+
|
| 645 |
+
all_features = self.coords.with_physics_sharding(
|
| 646 |
+
data_features | model_features
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
nodal_corrections = self.coords.with_physics_sharding(
|
| 650 |
+
self.output_transform_fn(net(all_features))
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
perturbed_correction = self.perturbation_fn(
|
| 654 |
+
state=None, # Unused
|
| 655 |
+
inputs=nodal_corrections,
|
| 656 |
+
randomness=randomness.nodal_value,
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
add_fn = lambda x, y: x + y if y is not None else x
|
| 660 |
+
corrected_pe_state = self.coords.physics_to_dycore_sharding(
|
| 661 |
+
jax.tree_util.tree_map(
|
| 662 |
+
add_fn,
|
| 663 |
+
coordinate_systems.maybe_to_modal(pe_state_nodal, self.coords),
|
| 664 |
+
coordinate_systems.maybe_to_modal(
|
| 665 |
+
perturbed_correction, self.coords
|
| 666 |
+
),
|
| 667 |
+
)
|
| 668 |
+
)
|
| 669 |
+
return ModelState(state=self.transform_fn(corrected_pe_state)) # pyrefly: ignore[unexpected-keyword]
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
@gin.register
|
| 673 |
+
class DimensionalWeatherbenchToPrimitiveEncoder(WeatherbenchToPrimitiveEncoder):
|
| 674 |
+
"""Same as WeatherbenchToPrimitiveEncoder, but with dimensional inputs."""
|
| 675 |
+
|
| 676 |
+
def __init__(
|
| 677 |
+
self,
|
| 678 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 679 |
+
dt: float,
|
| 680 |
+
physics_specs: Any,
|
| 681 |
+
aux_features: Dict[str, Any],
|
| 682 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 683 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 684 |
+
time_axis: int = 0,
|
| 685 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 686 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 687 |
+
name: Optional[str] = None,
|
| 688 |
+
):
|
| 689 |
+
nondim_pressure_centers = physics_specs.nondimensionalize(
|
| 690 |
+
input_coords.vertical.centers * scales.units.millibar
|
| 691 |
+
)
|
| 692 |
+
nondim_input_coords = coordinate_systems.CoordinateSystem(
|
| 693 |
+
input_coords.horizontal,
|
| 694 |
+
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
|
| 695 |
+
spmd_mesh=input_coords.spmd_mesh,
|
| 696 |
+
)
|
| 697 |
+
super().__init__(
|
| 698 |
+
coords,
|
| 699 |
+
dt,
|
| 700 |
+
physics_specs,
|
| 701 |
+
aux_features,
|
| 702 |
+
input_coords=nondim_input_coords,
|
| 703 |
+
time_axis=time_axis,
|
| 704 |
+
orography_module=orography_module,
|
| 705 |
+
transform_module=transform_module,
|
| 706 |
+
name=name,
|
| 707 |
+
)
|
| 708 |
+
self.nondim_transform_fn = transforms.NondimensionalizeTransform(
|
| 709 |
+
coords,
|
| 710 |
+
dt,
|
| 711 |
+
physics_specs,
|
| 712 |
+
aux_features,
|
| 713 |
+
nondim_input_coords,
|
| 714 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 715 |
+
)
|
| 716 |
+
|
| 717 |
+
def __call__(
|
| 718 |
+
self,
|
| 719 |
+
inputs: DataState,
|
| 720 |
+
forcing: Forcing,
|
| 721 |
+
) -> primitive_equations.StateWithTime:
|
| 722 |
+
nondim_inputs = self.nondim_transform_fn(inputs)
|
| 723 |
+
return super().__call__(nondim_inputs, forcing) # pyrefly: ignore[bad-return]
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
@gin.register
|
| 727 |
+
class DimensionalLearnedWeatherbenchToPrimitiveEncoder(
|
| 728 |
+
LearnedWeatherbenchToPrimitiveEncoder
|
| 729 |
+
):
|
| 730 |
+
"""Same as LearnedWeatherbenchToPrimitiveEncoder, but with dimensional inputs."""
|
| 731 |
+
|
| 732 |
+
def __init__(
|
| 733 |
+
self,
|
| 734 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 735 |
+
dt: float,
|
| 736 |
+
physics_specs: Any,
|
| 737 |
+
aux_features: Dict[str, Any],
|
| 738 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 739 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 740 |
+
modal_to_nodal_data_features_module: FeaturesModule,
|
| 741 |
+
modal_to_nodal_model_features_module: FeaturesModule,
|
| 742 |
+
nodal_mapping_module: MappingModule,
|
| 743 |
+
correction_transform_module: TransformModule,
|
| 744 |
+
prediction_mask: typing.Pytree,
|
| 745 |
+
time_axis: int = 0,
|
| 746 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 747 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 748 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 749 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 750 |
+
name: Optional[str] = None,
|
| 751 |
+
):
|
| 752 |
+
nondim_pressure_centers = physics_specs.nondimensionalize(
|
| 753 |
+
input_coords.vertical.centers * scales.units.millibar
|
| 754 |
+
)
|
| 755 |
+
nondim_input_coords = coordinate_systems.CoordinateSystem(
|
| 756 |
+
input_coords.horizontal,
|
| 757 |
+
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
|
| 758 |
+
spmd_mesh=input_coords.spmd_mesh,
|
| 759 |
+
)
|
| 760 |
+
super().__init__(
|
| 761 |
+
coords,
|
| 762 |
+
dt,
|
| 763 |
+
physics_specs,
|
| 764 |
+
aux_features,
|
| 765 |
+
input_coords=nondim_input_coords,
|
| 766 |
+
modal_to_nodal_data_features_module=modal_to_nodal_data_features_module,
|
| 767 |
+
modal_to_nodal_model_features_module=(
|
| 768 |
+
modal_to_nodal_model_features_module
|
| 769 |
+
),
|
| 770 |
+
nodal_mapping_module=nodal_mapping_module,
|
| 771 |
+
correction_transform_module=correction_transform_module,
|
| 772 |
+
prediction_mask=prediction_mask,
|
| 773 |
+
time_axis=time_axis,
|
| 774 |
+
orography_module=orography_module,
|
| 775 |
+
transform_module=transform_module,
|
| 776 |
+
randomness_module=randomness_module,
|
| 777 |
+
perturbation_module=perturbation_module,
|
| 778 |
+
name=name,
|
| 779 |
+
)
|
| 780 |
+
self.nondim_transform_fn = transforms.NondimensionalizeTransform(
|
| 781 |
+
coords,
|
| 782 |
+
dt,
|
| 783 |
+
physics_specs,
|
| 784 |
+
aux_features,
|
| 785 |
+
nondim_input_coords,
|
| 786 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 787 |
+
)
|
| 788 |
+
|
| 789 |
+
def __call__(
|
| 790 |
+
self,
|
| 791 |
+
inputs: DataState,
|
| 792 |
+
forcing: Forcing,
|
| 793 |
+
) -> ModelState:
|
| 794 |
+
nondim_inputs = self.nondim_transform_fn(inputs)
|
| 795 |
+
return super().__call__(nondim_inputs, forcing)
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
@gin.register
|
| 799 |
+
class DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder(hk.Module):
|
| 800 |
+
"""Same as DimensionalLearnedWeatherbenchToPrimitiveEncoder, but with memory.
|
| 801 |
+
"""
|
| 802 |
+
|
| 803 |
+
def __init__(
|
| 804 |
+
self,
|
| 805 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 806 |
+
dt: float,
|
| 807 |
+
physics_specs: Any,
|
| 808 |
+
aux_features: Dict[str, Any],
|
| 809 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 810 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 811 |
+
modal_to_nodal_data_features_module: FeaturesModule,
|
| 812 |
+
modal_to_nodal_model_features_module: FeaturesModule,
|
| 813 |
+
nodal_mapping_module: MappingModule,
|
| 814 |
+
correction_transform_module: TransformModule,
|
| 815 |
+
prediction_mask: typing.Pytree,
|
| 816 |
+
time_axis: int = 0,
|
| 817 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 818 |
+
transform_module: TransformModule = EncoderIdentityTransform,
|
| 819 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 820 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 821 |
+
name: Optional[str] = None,
|
| 822 |
+
):
|
| 823 |
+
nondim_pressure_centers = physics_specs.nondimensionalize(
|
| 824 |
+
input_coords.vertical.centers * scales.units.millibar)
|
| 825 |
+
nondim_input_coords = coordinate_systems.CoordinateSystem(
|
| 826 |
+
input_coords.horizontal,
|
| 827 |
+
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
|
| 828 |
+
spmd_mesh=input_coords.spmd_mesh,
|
| 829 |
+
)
|
| 830 |
+
super().__init__(name=name)
|
| 831 |
+
make_encoder_fn = functools.partial(
|
| 832 |
+
LearnedWeatherbenchToPrimitiveEncoder,
|
| 833 |
+
coords=coords, dt=dt,
|
| 834 |
+
physics_specs=physics_specs, aux_features=aux_features,
|
| 835 |
+
input_coords=nondim_input_coords,
|
| 836 |
+
modal_to_nodal_data_features_module=
|
| 837 |
+
modal_to_nodal_data_features_module,
|
| 838 |
+
modal_to_nodal_model_features_module=
|
| 839 |
+
modal_to_nodal_model_features_module,
|
| 840 |
+
nodal_mapping_module=nodal_mapping_module,
|
| 841 |
+
correction_transform_module=correction_transform_module,
|
| 842 |
+
prediction_mask=prediction_mask, time_axis=time_axis,
|
| 843 |
+
orography_module=orography_module,
|
| 844 |
+
transform_module=transform_module,
|
| 845 |
+
name=name
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
# Memory will be deterministic. State may be random.
|
| 849 |
+
self.memory_encoder = make_encoder_fn(
|
| 850 |
+
randomness_module=stochastic.NoRandomField,
|
| 851 |
+
perturbation_module=perturbations.NoPerturbation,
|
| 852 |
+
)
|
| 853 |
+
self.state_encoder = make_encoder_fn(
|
| 854 |
+
randomness_module=randomness_module,
|
| 855 |
+
perturbation_module=perturbation_module,
|
| 856 |
+
)
|
| 857 |
+
|
| 858 |
+
self.nondim_transform_fn = transforms.NondimensionalizeTransform(
|
| 859 |
+
coords, dt, physics_specs, aux_features, nondim_input_coords,
|
| 860 |
+
inputs_to_units_mapping=inputs_to_units_mapping)
|
| 861 |
+
|
| 862 |
+
def __call__(
|
| 863 |
+
self,
|
| 864 |
+
inputs: DataState,
|
| 865 |
+
forcing: Forcing,
|
| 866 |
+
) -> ModelState:
|
| 867 |
+
nondim_inputs = self.nondim_transform_fn(inputs)
|
| 868 |
+
memory = self.memory_encoder(nondim_inputs, forcing=forcing)
|
| 869 |
+
model_state = self.state_encoder(nondim_inputs, forcing=forcing)
|
| 870 |
+
return ModelState(
|
| 871 |
+
state=model_state.state, # pyrefly: ignore[unexpected-keyword]
|
| 872 |
+
memory=memory.state, # pyrefly: ignore[unexpected-keyword]
|
| 873 |
+
randomness=model_state.randomness, # pyrefly: ignore[unexpected-keyword]
|
| 874 |
+
)
|
model/legacy/equations.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""ML modules for equation-based models."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Callable, Optional, Sequence, Union
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import held_suarez
|
| 19 |
+
from dinosaur import primitive_equations
|
| 20 |
+
from dinosaur import pytree_utils
|
| 21 |
+
from dinosaur import scales
|
| 22 |
+
from dinosaur import shallow_water
|
| 23 |
+
from dinosaur import sigma_coordinates
|
| 24 |
+
from dinosaur import time_integration
|
| 25 |
+
from dinosaur import typing
|
| 26 |
+
from dinosaur import xarray_utils
|
| 27 |
+
import gin
|
| 28 |
+
import haiku as hk
|
| 29 |
+
import jax
|
| 30 |
+
import jax.numpy as jnp
|
| 31 |
+
from model.legacy import features
|
| 32 |
+
from model.legacy import mappings
|
| 33 |
+
from model.legacy import orographies
|
| 34 |
+
from model.legacy import parameterizations
|
| 35 |
+
|
| 36 |
+
units = scales.units
|
| 37 |
+
SCALE = scales.DEFAULT_SCALE
|
| 38 |
+
QuantityOrStr = Union[str, scales.Quantity]
|
| 39 |
+
EquationModule = Callable[..., time_integration.ImplicitExplicitODE]
|
| 40 |
+
TransformModule = typing.TransformModule
|
| 41 |
+
FeaturesModule = features.FeaturesModule
|
| 42 |
+
OrographyModule = orographies.OrographyModule
|
| 43 |
+
MappingModule = mappings.MappingModule
|
| 44 |
+
StepFilterModule = Callable[..., typing.PyTreeStepFilterFn]
|
| 45 |
+
|
| 46 |
+
REF_TEMP_KEY = xarray_utils.REF_TEMP_KEY
|
| 47 |
+
REF_POTENTIAL_KEY = xarray_utils.REF_POTENTIAL_KEY
|
| 48 |
+
OROGRAPHY = xarray_utils.OROGRAPHY
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@gin.register
|
| 52 |
+
class ShallowWaterEquations(shallow_water.ShallowWaterEquations):
|
| 53 |
+
"""Equation module for shallow water system."""
|
| 54 |
+
|
| 55 |
+
def __init__(
|
| 56 |
+
self,
|
| 57 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 58 |
+
dt: float,
|
| 59 |
+
physics_specs: shallow_water.ShallowWaterSpecs,
|
| 60 |
+
aux_features: typing.AuxFeatures,
|
| 61 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 62 |
+
name: Optional[str] = None,
|
| 63 |
+
):
|
| 64 |
+
reference_potential = aux_features.get(REF_POTENTIAL_KEY, None)
|
| 65 |
+
if reference_potential is None:
|
| 66 |
+
raise ValueError(f'must supply {REF_POTENTIAL_KEY} in `aux_features`.')
|
| 67 |
+
modal_orography_init_fn = orography_module(
|
| 68 |
+
coords, dt, physics_specs, aux_features)
|
| 69 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 70 |
+
super().__init__(
|
| 71 |
+
coords=coords,
|
| 72 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 73 |
+
orography=modal_orography,
|
| 74 |
+
reference_potential=reference_potential,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@gin.register
|
| 79 |
+
class PrimitiveEquations(primitive_equations.PrimitiveEquations):
|
| 80 |
+
"""Equation module for primitive equations."""
|
| 81 |
+
|
| 82 |
+
def __init__(
|
| 83 |
+
self,
|
| 84 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 85 |
+
dt: float,
|
| 86 |
+
physics_specs: primitive_equations.PrimitiveEquationsSpecs,
|
| 87 |
+
aux_features: typing.AuxFeatures,
|
| 88 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 89 |
+
vertical_advection: Callable[..., jax.Array] = (
|
| 90 |
+
sigma_coordinates.centered_vertical_advection
|
| 91 |
+
),
|
| 92 |
+
include_vertical_advection: bool = True,
|
| 93 |
+
name: Optional[str] = None,
|
| 94 |
+
):
|
| 95 |
+
ref_temperatures = aux_features.get(REF_TEMP_KEY, None)
|
| 96 |
+
if ref_temperatures is None:
|
| 97 |
+
raise ValueError(f'must supply {REF_TEMP_KEY} in `aux_features`.')
|
| 98 |
+
modal_orography_init_fn = orography_module(
|
| 99 |
+
coords, dt, physics_specs, aux_features)
|
| 100 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 101 |
+
super().__init__(
|
| 102 |
+
coords=coords,
|
| 103 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 104 |
+
reference_temperature=ref_temperatures,
|
| 105 |
+
orography=modal_orography,
|
| 106 |
+
vertical_advection=vertical_advection,
|
| 107 |
+
include_vertical_advection=include_vertical_advection,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@gin.register
|
| 112 |
+
class PrimitiveEquationsWithTime(
|
| 113 |
+
primitive_equations.PrimitiveEquationsWithTime
|
| 114 |
+
):
|
| 115 |
+
"""Equation module for primitive equations."""
|
| 116 |
+
|
| 117 |
+
def __init__(
|
| 118 |
+
self,
|
| 119 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 120 |
+
dt: float,
|
| 121 |
+
physics_specs: primitive_equations.PrimitiveEquationsSpecs,
|
| 122 |
+
aux_features: typing.AuxFeatures,
|
| 123 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 124 |
+
vertical_advection: Callable[..., jax.Array] = (
|
| 125 |
+
sigma_coordinates.centered_vertical_advection
|
| 126 |
+
),
|
| 127 |
+
include_vertical_advection: bool = True,
|
| 128 |
+
name: Optional[str] = None,
|
| 129 |
+
):
|
| 130 |
+
ref_temperatures = aux_features.get(REF_TEMP_KEY, None)
|
| 131 |
+
if ref_temperatures is None:
|
| 132 |
+
raise ValueError(f'must supply {REF_TEMP_KEY} in `aux_features`.')
|
| 133 |
+
modal_orography_init_fn = orography_module(
|
| 134 |
+
coords, dt, physics_specs, aux_features)
|
| 135 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 136 |
+
super().__init__(
|
| 137 |
+
coords=coords,
|
| 138 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 139 |
+
reference_temperature=ref_temperatures,
|
| 140 |
+
orography=modal_orography,
|
| 141 |
+
vertical_advection=vertical_advection,
|
| 142 |
+
include_vertical_advection=include_vertical_advection,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
@gin.register
|
| 147 |
+
class MoistPrimitiveEquations(
|
| 148 |
+
primitive_equations.MoistPrimitiveEquations
|
| 149 |
+
):
|
| 150 |
+
"""Equation module for moist primitive equations."""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 155 |
+
dt: float,
|
| 156 |
+
physics_specs: primitive_equations.PrimitiveEquationsSpecs,
|
| 157 |
+
aux_features: typing.AuxFeatures,
|
| 158 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 159 |
+
vertical_advection: Callable[..., jax.Array] = (
|
| 160 |
+
sigma_coordinates.centered_vertical_advection
|
| 161 |
+
),
|
| 162 |
+
include_vertical_advection: bool = True,
|
| 163 |
+
name: Optional[str] = None,
|
| 164 |
+
):
|
| 165 |
+
ref_temperatures = aux_features.get(REF_TEMP_KEY, None)
|
| 166 |
+
if ref_temperatures is None:
|
| 167 |
+
raise ValueError(f'must supply {REF_TEMP_KEY} in `aux_features`.')
|
| 168 |
+
modal_orography_init_fn = orography_module(
|
| 169 |
+
coords, dt, physics_specs, aux_features)
|
| 170 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 171 |
+
super().__init__(
|
| 172 |
+
coords=coords,
|
| 173 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 174 |
+
reference_temperature=ref_temperatures,
|
| 175 |
+
orography=modal_orography,
|
| 176 |
+
vertical_advection=vertical_advection,
|
| 177 |
+
include_vertical_advection=include_vertical_advection,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@gin.register
|
| 182 |
+
class MoistPrimitiveEquationsWithCloudMoisture(
|
| 183 |
+
primitive_equations.MoistPrimitiveEquationsWithCloudMoisture
|
| 184 |
+
):
|
| 185 |
+
"""Equation module for moist primitive equations with clouds."""
|
| 186 |
+
|
| 187 |
+
def __init__(
|
| 188 |
+
self,
|
| 189 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 190 |
+
dt: float,
|
| 191 |
+
physics_specs: primitive_equations.PrimitiveEquationsSpecs,
|
| 192 |
+
aux_features: typing.AuxFeatures,
|
| 193 |
+
orography_module: OrographyModule = orographies.ClippedOrography,
|
| 194 |
+
vertical_advection: Callable[..., jax.Array] = (
|
| 195 |
+
sigma_coordinates.centered_vertical_advection
|
| 196 |
+
),
|
| 197 |
+
include_vertical_advection: bool = True,
|
| 198 |
+
name: Optional[str] = None,
|
| 199 |
+
):
|
| 200 |
+
ref_temperatures = aux_features.get(REF_TEMP_KEY, None)
|
| 201 |
+
if ref_temperatures is None:
|
| 202 |
+
raise ValueError(f'must supply {REF_TEMP_KEY} in `aux_features`.')
|
| 203 |
+
modal_orography_init_fn = orography_module(
|
| 204 |
+
coords, dt, physics_specs, aux_features)
|
| 205 |
+
modal_orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
|
| 206 |
+
super().__init__(
|
| 207 |
+
coords=coords,
|
| 208 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 209 |
+
reference_temperature=ref_temperatures,
|
| 210 |
+
orography=modal_orography,
|
| 211 |
+
vertical_advection=vertical_advection,
|
| 212 |
+
include_vertical_advection=include_vertical_advection,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@gin.register
|
| 217 |
+
class MoistPrimitiveEquationsWithCloudMoisutre(
|
| 218 |
+
MoistPrimitiveEquationsWithCloudMoisture
|
| 219 |
+
):
|
| 220 |
+
"""Temporary alias with mis-spelled name."""
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@gin.register
|
| 224 |
+
class HeldSuarezEquations(held_suarez.HeldSuarezForcing):
|
| 225 |
+
"""Equation module for Held-Suarez forcing equations."""
|
| 226 |
+
|
| 227 |
+
def __init__(
|
| 228 |
+
self,
|
| 229 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 230 |
+
dt: float,
|
| 231 |
+
physics_specs: primitive_equations.PrimitiveEquationsSpecs,
|
| 232 |
+
aux_features: typing.AuxFeatures,
|
| 233 |
+
name: Optional[str] = None,
|
| 234 |
+
):
|
| 235 |
+
ref_temperatures = aux_features.get(REF_TEMP_KEY, None)
|
| 236 |
+
if ref_temperatures is None:
|
| 237 |
+
raise ValueError(f'must supply {REF_TEMP_KEY} in `aux_features`.')
|
| 238 |
+
super().__init__(
|
| 239 |
+
coords=coords,
|
| 240 |
+
physics_specs=physics_specs, # pyrefly: ignore[bad-argument-type]
|
| 241 |
+
reference_temperature=ref_temperatures)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
# TODO(dkochkov) Test if vertical diffusion works well with euler integrator.
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
@gin.register
|
| 248 |
+
class VerticalDiffusion(time_integration.ExplicitODE):
|
| 249 |
+
"""Equation module that adds explicit diffusion along vertical direction."""
|
| 250 |
+
|
| 251 |
+
def __init__(
|
| 252 |
+
self,
|
| 253 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 254 |
+
dt: float,
|
| 255 |
+
physics_specs: Any,
|
| 256 |
+
aux_features: typing.AuxFeatures,
|
| 257 |
+
timescale: QuantityOrStr = gin.REQUIRED,
|
| 258 |
+
):
|
| 259 |
+
self.coords = coords
|
| 260 |
+
timescale = dt / physics_specs.nondimensionalize(scales.Quantity(timescale))
|
| 261 |
+
timescales = coords.vertical.boundaries * timescale # pyrefly: ignore[missing-attribute]
|
| 262 |
+
self.level_weighted_timescales = timescales[:, jnp.newaxis, jnp.newaxis]
|
| 263 |
+
|
| 264 |
+
def explicit_terms(self, state: typing.PyTreeState) -> typing.PyTreeState:
|
| 265 |
+
def vertical_diffusion_fn(x: typing.Array) -> typing.Array:
|
| 266 |
+
# TODO(dkochkov) Consider using sigma_coordinates.centered_difference.
|
| 267 |
+
x_grad = x[1:, ...] - x[:-1, ...]
|
| 268 |
+
# padding with zero values for vertical fluxes.
|
| 269 |
+
pad_width = ((1, 1), (0, 0), (0, 0))
|
| 270 |
+
x_grad = jnp.pad(x_grad, pad_width)
|
| 271 |
+
fluxes = self.level_weighted_timescales * x_grad
|
| 272 |
+
# TODO(dkochkov) Consider using sigma_coordinates.centered_difference.
|
| 273 |
+
return fluxes[1:, ...] - fluxes[:-1, ...]
|
| 274 |
+
|
| 275 |
+
nodal_state = self.coords.horizontal.to_nodal(state)
|
| 276 |
+
nodal_tendency = pytree_utils.tree_map_where(
|
| 277 |
+
condition_fn=lambda x: jnp.asarray(x).shape == self.coords.nodal_shape, # pyrefly: ignore[bad-argument-type]
|
| 278 |
+
f=vertical_diffusion_fn,
|
| 279 |
+
g=jnp.zeros_like,
|
| 280 |
+
x=nodal_state)
|
| 281 |
+
modal_tendency = self.coords.horizontal.to_modal(nodal_tendency)
|
| 282 |
+
return self.coords.horizontal.clip_wavenumbers(modal_tendency)
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@gin.register
|
| 286 |
+
class NoDynamics(time_integration.ImplicitExplicitODE):
|
| 287 |
+
"""The constant ODE, ∂u/∂t = 0."""
|
| 288 |
+
|
| 289 |
+
def __init__(self, *args, **kwargs):
|
| 290 |
+
del args, kwargs
|
| 291 |
+
|
| 292 |
+
def explicit_terms(self, x: typing.PyTreeState) -> typing.PyTreeState:
|
| 293 |
+
return 0 * x # pyrefly: ignore[bad-return, unsupported-operation]
|
| 294 |
+
|
| 295 |
+
def implicit_terms(self, x: typing.PyTreeState) -> typing.PyTreeState:
|
| 296 |
+
return 0 * x # pyrefly: ignore[bad-return, unsupported-operation]
|
| 297 |
+
|
| 298 |
+
def implicit_inverse(
|
| 299 |
+
self, x: typing.PyTreeState, time_step: float
|
| 300 |
+
) -> typing.PyTreeState:
|
| 301 |
+
return x
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
@gin.register
|
| 305 |
+
def composed_equations_module(
|
| 306 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 307 |
+
dt: float,
|
| 308 |
+
physics_specs: Any,
|
| 309 |
+
aux_features: typing.AuxFeatures,
|
| 310 |
+
equation_modules: Sequence[EquationModule],
|
| 311 |
+
) -> time_integration.ImplicitExplicitODE:
|
| 312 |
+
"""Returns an equation module that represents a composition of equations."""
|
| 313 |
+
equations = tuple(eq(coords, dt, physics_specs, aux_features)
|
| 314 |
+
for eq in equation_modules)
|
| 315 |
+
return time_integration.compose_equations(equations)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
@gin.register
|
| 319 |
+
class DirectNeuralEquations(hk.Module, time_integration.ExplicitODE):
|
| 320 |
+
"""Computes explicit tendencies for the input state.
|
| 321 |
+
|
| 322 |
+
This equation module predicts tendencies directly in the nodal representation
|
| 323 |
+
and returns values transformed back to the modal space. The nodal tendencies
|
| 324 |
+
are computed by the `nodal_mapping_module` from preprocessed nodal features
|
| 325 |
+
computed by `modal_to_nodal_features_module` followed by the
|
| 326 |
+
`tendency_transform_module`.
|
| 327 |
+
"""
|
| 328 |
+
|
| 329 |
+
def __init__(
|
| 330 |
+
self,
|
| 331 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 332 |
+
dt: float,
|
| 333 |
+
physics_specs: Any,
|
| 334 |
+
aux_features: typing.AuxFeatures,
|
| 335 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 336 |
+
nodal_mapping_module: mappings.MappingModule,
|
| 337 |
+
tendency_transform_module: TransformModule,
|
| 338 |
+
prediction_mask: Optional[typing.Pytree] = None,
|
| 339 |
+
filter_module: Optional[StepFilterModule] = None,
|
| 340 |
+
name: Optional[str] = None,
|
| 341 |
+
):
|
| 342 |
+
super().__init__(name=name)
|
| 343 |
+
self.parameterization_fn = parameterizations.DirectNeuralParameterization(
|
| 344 |
+
coords=coords,
|
| 345 |
+
dt=dt,
|
| 346 |
+
physics_specs=physics_specs,
|
| 347 |
+
aux_features=aux_features,
|
| 348 |
+
modal_to_nodal_features_module=modal_to_nodal_features_module,
|
| 349 |
+
nodal_mapping_module=nodal_mapping_module,
|
| 350 |
+
tendency_transform_module=tendency_transform_module,
|
| 351 |
+
prediction_mask=prediction_mask,
|
| 352 |
+
filter_module=filter_module,
|
| 353 |
+
name=name,
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
def explicit_terms(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 357 |
+
modal_tendencies = self.parameterization_fn(inputs, forcing=None)
|
| 358 |
+
modal_tendencies = pytree_utils.none_to_zeros(modal_tendencies, inputs)
|
| 359 |
+
return modal_tendencies
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@gin.register
|
| 363 |
+
class DivCurlNeuralEquations(hk.Module, time_integration.ExplicitODE):
|
| 364 |
+
"""Computes explicit tendencies using div and curl operators for `u, v` terms.
|
| 365 |
+
|
| 366 |
+
This equation module predicts tendencies of the inputs with velocity-based
|
| 367 |
+
parameterization of the `divergence` and `vorticity` components. Specifically,
|
| 368 |
+
we replace predictions of `divergence` and `vorticity` by nodal predictions
|
| 369 |
+
of `u`, and `v`, which are then differentiated using modal representation.
|
| 370 |
+
"""
|
| 371 |
+
|
| 372 |
+
def __init__(
|
| 373 |
+
self,
|
| 374 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 375 |
+
dt: float,
|
| 376 |
+
physics_specs: Any,
|
| 377 |
+
aux_features: typing.AuxFeatures,
|
| 378 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 379 |
+
nodal_mapping_module: mappings.MappingModule,
|
| 380 |
+
tendency_transform_module: TransformModule,
|
| 381 |
+
prediction_mask: Optional[typing.Pytree] = None,
|
| 382 |
+
filter_module: Optional[StepFilterModule] = None,
|
| 383 |
+
name: Optional[str] = None,
|
| 384 |
+
):
|
| 385 |
+
super().__init__(name=name)
|
| 386 |
+
self.parameterization_fn = parameterizations.DivCurlNeuralParameterization(
|
| 387 |
+
coords=coords,
|
| 388 |
+
dt=dt,
|
| 389 |
+
physics_specs=physics_specs,
|
| 390 |
+
aux_features=aux_features,
|
| 391 |
+
modal_to_nodal_features_module=modal_to_nodal_features_module,
|
| 392 |
+
nodal_mapping_module=nodal_mapping_module,
|
| 393 |
+
tendency_transform_module=tendency_transform_module,
|
| 394 |
+
prediction_mask=prediction_mask,
|
| 395 |
+
filter_module=filter_module,
|
| 396 |
+
name=name,
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
def explicit_terms(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 400 |
+
modal_tendencies = self.parameterization_fn(inputs, forcing=None)
|
| 401 |
+
modal_tendencies = pytree_utils.none_to_zeros(modal_tendencies, inputs)
|
| 402 |
+
return modal_tendencies
|
model/legacy/features.py
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules that computes relevant state features to be used by ML components."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Callable, Mapping, Optional, Protocol, Sequence
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import primitive_equations
|
| 19 |
+
from dinosaur import pytree_utils
|
| 20 |
+
from dinosaur import radiation
|
| 21 |
+
from dinosaur import scales
|
| 22 |
+
from dinosaur import spherical_harmonic
|
| 23 |
+
from dinosaur import typing
|
| 24 |
+
from dinosaur import xarray_utils
|
| 25 |
+
import gin
|
| 26 |
+
import haiku as hk
|
| 27 |
+
import jax
|
| 28 |
+
import jax.numpy as jnp
|
| 29 |
+
from model.legacy import transforms
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
Array = typing.Array
|
| 34 |
+
Pytree = typing.Pytree
|
| 35 |
+
TransformModule = typing.TransformModule
|
| 36 |
+
KeyWithCosLatFactor = typing.KeyWithCosLatFactor
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class FeaturesFn(Protocol):
|
| 40 |
+
|
| 41 |
+
def __call__(
|
| 42 |
+
self,
|
| 43 |
+
inputs: typing.Pytree,
|
| 44 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 45 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 46 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 47 |
+
forcing: Optional[typing.Pytree] = None,
|
| 48 |
+
) -> typing.Pytree:
|
| 49 |
+
...
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
FeaturesModule = Callable[..., FeaturesFn]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 56 |
+
class PrimitiveEquationsDiagnosticState(hk.Module):
|
| 57 |
+
"""Features modules that returns processed DiagnosticState for PE."""
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 62 |
+
dt: float,
|
| 63 |
+
physics_specs: Any,
|
| 64 |
+
aux_features: typing.AuxFeatures,
|
| 65 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 66 |
+
name: Optional[str] = None,
|
| 67 |
+
):
|
| 68 |
+
super().__init__(name=name)
|
| 69 |
+
self.features_transform_fn = features_transform_module(
|
| 70 |
+
coords, dt, physics_specs, aux_features
|
| 71 |
+
)
|
| 72 |
+
self.coords = coords
|
| 73 |
+
|
| 74 |
+
def __call__(
|
| 75 |
+
self,
|
| 76 |
+
inputs: typing.Pytree,
|
| 77 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 78 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 79 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 80 |
+
forcing: Optional[typing.Pytree] = None,
|
| 81 |
+
) -> primitive_equations.DiagnosticState:
|
| 82 |
+
del memory, diagnostics, randomness, forcing # unused
|
| 83 |
+
if not isinstance(inputs, primitive_equations.State):
|
| 84 |
+
inputs = primitive_equations.State(**inputs)
|
| 85 |
+
d_state = primitive_equations.compute_diagnostic_state(inputs, self.coords)
|
| 86 |
+
return self.features_transform_fn(d_state.asdict())
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 90 |
+
class VelocityAndPrognostics(hk.Module):
|
| 91 |
+
"""Features module that returns prognostics + u,v and optionally gradients."""
|
| 92 |
+
|
| 93 |
+
def __init__(
|
| 94 |
+
self,
|
| 95 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 96 |
+
dt: float,
|
| 97 |
+
physics_specs: Any,
|
| 98 |
+
aux_features: typing.AuxFeatures,
|
| 99 |
+
fields_to_include: Optional[Sequence[str]] = None,
|
| 100 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 101 |
+
compute_gradients_module: TransformModule = transforms.EmptyTransform,
|
| 102 |
+
name: Optional[str] = None,
|
| 103 |
+
):
|
| 104 |
+
super().__init__(name=name)
|
| 105 |
+
self.features_transform_fn = features_transform_module(
|
| 106 |
+
coords, dt, physics_specs, aux_features
|
| 107 |
+
)
|
| 108 |
+
self.coords = coords
|
| 109 |
+
self.fields_to_include = fields_to_include
|
| 110 |
+
self.compute_gradients_fn = compute_gradients_module(
|
| 111 |
+
coords, dt, physics_specs, aux_features
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
def _extract_features(
|
| 115 |
+
self,
|
| 116 |
+
inputs: typing.Pytree,
|
| 117 |
+
prefix: str = '',
|
| 118 |
+
) -> typing.Pytree:
|
| 119 |
+
"""Returns a nodal velocity and prognostic features."""
|
| 120 |
+
# Note: all intermediate features have an explicit cos-lat factors in key.
|
| 121 |
+
# These factors are removed in the `__call__` method before returning.
|
| 122 |
+
|
| 123 |
+
# compute `u, v` if div/curl is available and `u, v` not in prognosics.
|
| 124 |
+
if set(['vorticity', 'divergence']).issubset(inputs.keys()) and not set(
|
| 125 |
+
['u', 'v']
|
| 126 |
+
).intersection(inputs.keys()):
|
| 127 |
+
cos_lat_u, cos_lat_v = spherical_harmonic.get_cos_lat_vector(
|
| 128 |
+
inputs['vorticity'], inputs['divergence'], self.coords.horizontal
|
| 129 |
+
)
|
| 130 |
+
modal_features = {
|
| 131 |
+
KeyWithCosLatFactor(prefix + 'u', 1): cos_lat_u,
|
| 132 |
+
KeyWithCosLatFactor(prefix + 'v', 1): cos_lat_v,
|
| 133 |
+
}
|
| 134 |
+
else:
|
| 135 |
+
modal_features = {}
|
| 136 |
+
prognostics_keys = list(inputs.keys())
|
| 137 |
+
prognostics_keys.remove('tracers')
|
| 138 |
+
prognostics_keys.remove('sim_time')
|
| 139 |
+
for k in prognostics_keys:
|
| 140 |
+
if self.fields_to_include is None or k in self.fields_to_include:
|
| 141 |
+
modal_features[KeyWithCosLatFactor(prefix + k, 0)] = inputs[k]
|
| 142 |
+
|
| 143 |
+
for k, v in inputs['tracers'].items():
|
| 144 |
+
if self.fields_to_include is None or k in self.fields_to_include:
|
| 145 |
+
modal_features[KeyWithCosLatFactor(prefix + k, 0)] = v
|
| 146 |
+
# Computing gradient features and adjusting cos_lat factors.
|
| 147 |
+
modal_features = self.coords.with_dycore_sharding(modal_features)
|
| 148 |
+
diff_operator_features = self.compute_gradients_fn(modal_features)
|
| 149 |
+
sec_lat = 1 / self.coords.horizontal.cos_lat
|
| 150 |
+
sec2_lat = self.coords.horizontal.sec2_lat
|
| 151 |
+
sec_lat_scales = {0: 1, 1: sec_lat, 2: sec2_lat}
|
| 152 |
+
# Computing all features in nodal space.
|
| 153 |
+
features = {}
|
| 154 |
+
for k, v in (diff_operator_features | modal_features).items():
|
| 155 |
+
sec_lat_scale = sec_lat_scales[k.factor_order]
|
| 156 |
+
features[k.name] = self.coords.horizontal.to_nodal(v) * sec_lat_scale
|
| 157 |
+
features = self.coords.with_dycore_sharding(features)
|
| 158 |
+
return features
|
| 159 |
+
|
| 160 |
+
def __call__(
|
| 161 |
+
self,
|
| 162 |
+
inputs: typing.Pytree,
|
| 163 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 164 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 165 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 166 |
+
forcing: Optional[typing.Pytree] = None,
|
| 167 |
+
) -> typing.Pytree:
|
| 168 |
+
del memory, diagnostics, randomness, forcing # unused.
|
| 169 |
+
nodal_features = self._extract_features(inputs)
|
| 170 |
+
return self.features_transform_fn(nodal_features)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 174 |
+
class MemoryVelocityAndValues(VelocityAndPrognostics):
|
| 175 |
+
"""Similar to `VelocityAndPrognostics`, but operates on memory."""
|
| 176 |
+
|
| 177 |
+
def __call__(
|
| 178 |
+
self,
|
| 179 |
+
inputs: typing.Pytree,
|
| 180 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 181 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 182 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 183 |
+
forcing: Optional[typing.Pytree] = None,
|
| 184 |
+
) -> typing.Pytree:
|
| 185 |
+
del inputs, diagnostics, randomness, forcing # unused.
|
| 186 |
+
nodal_features = self._extract_features(memory, 'memory_')
|
| 187 |
+
return self.features_transform_fn(nodal_features)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 191 |
+
class NodalInputVelocityAndPrognostics(VelocityAndPrognostics):
|
| 192 |
+
"""Features modules that returns velocities, temperature, and optionally gradients."""
|
| 193 |
+
|
| 194 |
+
def __call__(
|
| 195 |
+
self,
|
| 196 |
+
inputs: typing.Pytree,
|
| 197 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 198 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 199 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 200 |
+
forcing: Optional[typing.Pytree] = None,
|
| 201 |
+
) -> typing.Pytree:
|
| 202 |
+
to_modal_fn = self.coords.horizontal.to_modal
|
| 203 |
+
inputs = to_modal_fn(inputs)
|
| 204 |
+
memory = to_modal_fn(memory)
|
| 205 |
+
return super().__call__(inputs, memory, randomness, forcing)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 209 |
+
class RadiationFeatures(hk.Module):
|
| 210 |
+
"""Feature module that computes incident radiation flux."""
|
| 211 |
+
|
| 212 |
+
def __init__(
|
| 213 |
+
self,
|
| 214 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 215 |
+
dt: float,
|
| 216 |
+
physics_specs: Any,
|
| 217 |
+
aux_features: typing.AuxFeatures,
|
| 218 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 219 |
+
name: Optional[str] = None,
|
| 220 |
+
):
|
| 221 |
+
super().__init__(name=name)
|
| 222 |
+
self.features_transform_fn = features_transform_module(
|
| 223 |
+
coords, dt, physics_specs, aux_features
|
| 224 |
+
)
|
| 225 |
+
ref_datetime_str = str(aux_features[xarray_utils.REFERENCE_DATETIME_KEY])
|
| 226 |
+
self.solar_radiation = radiation.SolarRadiation.normalized(
|
| 227 |
+
coords=coords,
|
| 228 |
+
physics_specs=physics_specs,
|
| 229 |
+
reference_datetime=np.datetime64(ref_datetime_str),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
def __call__(
|
| 233 |
+
self,
|
| 234 |
+
inputs: typing.Pytree,
|
| 235 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 236 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 237 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 238 |
+
forcing: Optional[typing.Pytree] = None,
|
| 239 |
+
) -> typing.Pytree:
|
| 240 |
+
del memory, diagnostics, randomness, forcing # unused.
|
| 241 |
+
features = {}
|
| 242 |
+
features['radiation'] = self.solar_radiation.radiation_flux(
|
| 243 |
+
inputs['sim_time']
|
| 244 |
+
)
|
| 245 |
+
# TODO(janniyuval) add a flag that allow to get radiation of next time step
|
| 246 |
+
# insert a feature axis.
|
| 247 |
+
features = jax.tree_util.tree_map(lambda x: jnp.expand_dims(x, 0), features)
|
| 248 |
+
return self.features_transform_fn(features)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 252 |
+
class OrbitalTimeFeatures(hk.Module):
|
| 253 |
+
"""Feature module that computes orbital time features."""
|
| 254 |
+
|
| 255 |
+
def __init__(
|
| 256 |
+
self,
|
| 257 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 258 |
+
dt: float,
|
| 259 |
+
physics_specs: Any,
|
| 260 |
+
aux_features: typing.AuxFeatures,
|
| 261 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 262 |
+
name: Optional[str] = None,
|
| 263 |
+
):
|
| 264 |
+
super().__init__(name=name)
|
| 265 |
+
self.features_transform_fn = features_transform_module(
|
| 266 |
+
coords, dt, physics_specs, aux_features
|
| 267 |
+
)
|
| 268 |
+
ref_datetime_str = str(aux_features[xarray_utils.REFERENCE_DATETIME_KEY])
|
| 269 |
+
self.solar_radiation = radiation.SolarRadiation.normalized(
|
| 270 |
+
coords=coords,
|
| 271 |
+
physics_specs=physics_specs,
|
| 272 |
+
reference_datetime=np.datetime64(ref_datetime_str),
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
def __call__(
|
| 276 |
+
self,
|
| 277 |
+
inputs: typing.Pytree,
|
| 278 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 279 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 280 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 281 |
+
forcing: Optional[typing.Pytree] = None,
|
| 282 |
+
) -> typing.Pytree:
|
| 283 |
+
del memory, diagnostics, randomness, forcing # unused.
|
| 284 |
+
features = {}
|
| 285 |
+
# Cosine and sine of Earth's orbital phase around the Sun
|
| 286 |
+
orbital_time = self.solar_radiation.time_to_orbital_time(inputs['sim_time'])
|
| 287 |
+
# Convert from orbital_phase=0 on January 1st UTC to orbital_phase=0 at the
|
| 288 |
+
# approximate perihelion (when earth is closest to the sun).
|
| 289 |
+
orbital_phase = orbital_time.orbital_phase - radiation.PERIHELION
|
| 290 |
+
# All longitude, latitude locations share the same orbital phase
|
| 291 |
+
ones = jnp.ones(self.solar_radiation.coords.surface_nodal_shape)
|
| 292 |
+
features['cos_orbital_phase'] = jnp.cos(orbital_phase) * ones
|
| 293 |
+
features['sin_orbital_phase'] = jnp.sin(orbital_phase) * ones
|
| 294 |
+
# Cosine and sine of local hour angle (angle from solar noon)
|
| 295 |
+
solar_hour_angle = self.solar_radiation.solar_hour_angle(inputs['sim_time'])
|
| 296 |
+
solar_hour_angle = jnp.expand_dims(solar_hour_angle, 0)
|
| 297 |
+
features['cos_solar_hour'] = jnp.cos(solar_hour_angle)
|
| 298 |
+
features['sin_solar_hour'] = jnp.sin(solar_hour_angle)
|
| 299 |
+
# TODO(janniyuval) add a flag that allow to get radiation of next time step
|
| 300 |
+
return self.features_transform_fn(features)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 304 |
+
class ForcingFeatures(hk.Module):
|
| 305 |
+
"""Feature module that provides forcing values as features."""
|
| 306 |
+
|
| 307 |
+
def __init__(
|
| 308 |
+
self,
|
| 309 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 310 |
+
dt: float,
|
| 311 |
+
physics_specs: Any,
|
| 312 |
+
aux_features: typing.AuxFeatures,
|
| 313 |
+
forcing_to_include: Sequence[str] = tuple(),
|
| 314 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 315 |
+
name: Optional[str] = None,
|
| 316 |
+
):
|
| 317 |
+
super().__init__(name=name)
|
| 318 |
+
self.forcing_to_include = forcing_to_include
|
| 319 |
+
self.features_transform_fn = features_transform_module(
|
| 320 |
+
coords, dt, physics_specs, aux_features
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
def __call__(
|
| 324 |
+
self,
|
| 325 |
+
inputs: Pytree,
|
| 326 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 327 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 328 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 329 |
+
forcing: Optional[typing.Forcing] = None,
|
| 330 |
+
) -> Pytree:
|
| 331 |
+
del inputs, memory, diagnostics, randomness
|
| 332 |
+
features = {}
|
| 333 |
+
for key in self.forcing_to_include:
|
| 334 |
+
value = forcing[key] # pyrefly: ignore[unsupported-operation]
|
| 335 |
+
# Expect singleton "level" dimension for surface forcings
|
| 336 |
+
if value.ndim > 3:
|
| 337 |
+
raise ValueError(
|
| 338 |
+
f'Expected forcing "{key}" to have ndim <= 3, got {value.ndim}'
|
| 339 |
+
)
|
| 340 |
+
if value.ndim == 2:
|
| 341 |
+
value = jnp.expand_dims(value, axis=0)
|
| 342 |
+
if value.shape[0] != 1:
|
| 343 |
+
raise ValueError(
|
| 344 |
+
f'Expected forcing "{key}" to have leading dimension 1'
|
| 345 |
+
f'for level, got {value.shape}'
|
| 346 |
+
)
|
| 347 |
+
features[key] = value
|
| 348 |
+
|
| 349 |
+
return self.features_transform_fn(features)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 353 |
+
class LatitudeFeatures(hk.Module):
|
| 354 |
+
"""Feature module that creates cos and sin of latitude as features."""
|
| 355 |
+
|
| 356 |
+
def __init__(
|
| 357 |
+
self,
|
| 358 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 359 |
+
dt: float,
|
| 360 |
+
physics_specs: Any,
|
| 361 |
+
aux_features: typing.AuxFeatures,
|
| 362 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 363 |
+
name: Optional[str] = None,
|
| 364 |
+
):
|
| 365 |
+
super().__init__(name=name)
|
| 366 |
+
self.features_transform_fn = features_transform_module(
|
| 367 |
+
coords, dt, physics_specs, aux_features
|
| 368 |
+
)
|
| 369 |
+
self.coords = coords
|
| 370 |
+
|
| 371 |
+
def __call__(
|
| 372 |
+
self,
|
| 373 |
+
inputs: typing.Pytree,
|
| 374 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 375 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 376 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 377 |
+
forcing: Optional[typing.Pytree] = None,
|
| 378 |
+
) -> typing.Pytree:
|
| 379 |
+
del inputs, memory, diagnostics, randomness, forcing # unused.
|
| 380 |
+
_, sin_lat = self.coords.horizontal.nodal_mesh
|
| 381 |
+
sin_features = sin_lat[np.newaxis, ...]
|
| 382 |
+
cos_features = jnp.cos(jnp.arcsin(sin_features))
|
| 383 |
+
features = {
|
| 384 |
+
'cos_latitude': cos_features,
|
| 385 |
+
'sin_latitude': sin_features,
|
| 386 |
+
}
|
| 387 |
+
return self.features_transform_fn(features)
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 391 |
+
class RandomnessFeatures(hk.Module):
|
| 392 |
+
"""Feature module that returns fields from `randomness` as features."""
|
| 393 |
+
|
| 394 |
+
def __init__(
|
| 395 |
+
self,
|
| 396 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 397 |
+
dt: float,
|
| 398 |
+
physics_specs: Any,
|
| 399 |
+
aux_features: typing.AuxFeatures,
|
| 400 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 401 |
+
name: Optional[str] = None,
|
| 402 |
+
):
|
| 403 |
+
super().__init__(name=name)
|
| 404 |
+
self.features_transform_fn = features_transform_module(
|
| 405 |
+
coords, dt, physics_specs, aux_features
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
def __call__(
|
| 409 |
+
self,
|
| 410 |
+
inputs: typing.Pytree,
|
| 411 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 412 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 413 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 414 |
+
forcing: Optional[typing.Pytree] = None,
|
| 415 |
+
) -> typing.Pytree:
|
| 416 |
+
del inputs, memory, diagnostics, forcing # unused.
|
| 417 |
+
if randomness is None:
|
| 418 |
+
random_features = {}
|
| 419 |
+
elif isinstance(randomness, dict):
|
| 420 |
+
random_features, _ = pytree_utils.flatten_dict(randomness)
|
| 421 |
+
elif isinstance(randomness, jax.Array):
|
| 422 |
+
random_features = {'randomness': randomness}
|
| 423 |
+
else:
|
| 424 |
+
raise ValueError(f'randomness has unsupported {type(randomness)=}.')
|
| 425 |
+
# random fields are 2D by construction, adding a feature/level dimension.
|
| 426 |
+
if randomness is not None:
|
| 427 |
+
ndims = set(x.ndim for x in jax.tree_util.tree_leaves(random_features))
|
| 428 |
+
if not ndims.issubset({2, 3}):
|
| 429 |
+
raise ValueError(
|
| 430 |
+
f'Random fields expected to be 2D and/or 3D. Found {ndims=}'
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
def make_3d(x):
|
| 434 |
+
if x.ndim == 3:
|
| 435 |
+
return x
|
| 436 |
+
if x.ndim == 2:
|
| 437 |
+
return x[np.newaxis, ...]
|
| 438 |
+
|
| 439 |
+
random_features = jax.tree_util.tree_map(make_3d, random_features)
|
| 440 |
+
return self.features_transform_fn(random_features)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 444 |
+
class OrographyFeatures(hk.Module):
|
| 445 |
+
"""Feature module that computes orographic features."""
|
| 446 |
+
|
| 447 |
+
def __init__(
|
| 448 |
+
self,
|
| 449 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 450 |
+
dt: float,
|
| 451 |
+
physics_specs: Any,
|
| 452 |
+
aux_features: typing.AuxFeatures,
|
| 453 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 454 |
+
name: Optional[str] = None,
|
| 455 |
+
):
|
| 456 |
+
super().__init__(name=name)
|
| 457 |
+
if xarray_utils.OROGRAPHY not in aux_features:
|
| 458 |
+
raise ValueError('OrographyFeatures requires orography in aux_features.')
|
| 459 |
+
self.nodal_orography = aux_features[xarray_utils.OROGRAPHY]
|
| 460 |
+
self.features_transform_fn = features_transform_module(
|
| 461 |
+
coords, dt, physics_specs, aux_features
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
def __call__(
|
| 465 |
+
self,
|
| 466 |
+
inputs: typing.Pytree,
|
| 467 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 468 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 469 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 470 |
+
forcing: Optional[typing.Pytree] = None,
|
| 471 |
+
) -> typing.Pytree:
|
| 472 |
+
del inputs, memory, diagnostics, randomness, forcing # unused.
|
| 473 |
+
features = {
|
| 474 |
+
xarray_utils.OROGRAPHY: jnp.expand_dims(self.nodal_orography, 0),
|
| 475 |
+
}
|
| 476 |
+
return self.features_transform_fn(features)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 480 |
+
class OneHotAuxFeatures(hk.Module):
|
| 481 |
+
"""Feature module that produces one-hot encodings from binary covariates."""
|
| 482 |
+
|
| 483 |
+
def __init__(
|
| 484 |
+
self,
|
| 485 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 486 |
+
dt: float,
|
| 487 |
+
physics_specs: Any,
|
| 488 |
+
aux_features: typing.AuxFeatures,
|
| 489 |
+
covariate_keys: Sequence[str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 490 |
+
convert_float_to_int: bool = False,
|
| 491 |
+
name: Optional[str] = None,
|
| 492 |
+
):
|
| 493 |
+
del coords, dt, physics_specs # unused.
|
| 494 |
+
super().__init__(name=name)
|
| 495 |
+
covariates = {}
|
| 496 |
+
num_classes = {}
|
| 497 |
+
for key in covariate_keys:
|
| 498 |
+
if key not in aux_features:
|
| 499 |
+
raise ValueError(f'Covariate {key} not found in aux_features.')
|
| 500 |
+
if not np.issubdtype(aux_features[key].dtype, np.integer):
|
| 501 |
+
if convert_float_to_int:
|
| 502 |
+
aux_features[key] = np.round(aux_features[key]).astype(int)
|
| 503 |
+
else:
|
| 504 |
+
raise ValueError(
|
| 505 |
+
f'Covariate {key} is expected to be integer dtype, '
|
| 506 |
+
f'but is: {aux_features[key].dtype}'
|
| 507 |
+
)
|
| 508 |
+
covariates[key] = aux_features[key]
|
| 509 |
+
num_classes[key] = np.unique(aux_features[key]).size
|
| 510 |
+
self.covariates = covariates
|
| 511 |
+
self.num_classes = num_classes
|
| 512 |
+
|
| 513 |
+
def __call__(
|
| 514 |
+
self,
|
| 515 |
+
inputs: typing.Pytree,
|
| 516 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 517 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 518 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 519 |
+
forcing: Optional[typing.Pytree] = None,
|
| 520 |
+
) -> dict[str, jnp.ndarray]:
|
| 521 |
+
del inputs, memory, diagnostics, randomness, forcing # unused.
|
| 522 |
+
features = {
|
| 523 |
+
k: jax.nn.one_hot(v, self.num_classes[k], axis=0)
|
| 524 |
+
for k, v in self.covariates.items()
|
| 525 |
+
}
|
| 526 |
+
return features
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 530 |
+
class LearnedPositionalFeatures(hk.Module):
|
| 531 |
+
"""Feature module with learned params at surface nodal locations."""
|
| 532 |
+
|
| 533 |
+
def __init__(
|
| 534 |
+
self,
|
| 535 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 536 |
+
dt: float,
|
| 537 |
+
physics_specs: Any,
|
| 538 |
+
aux_features: typing.AuxFeatures,
|
| 539 |
+
latent_size: int,
|
| 540 |
+
scale: float = 1.0,
|
| 541 |
+
name: Optional[str] = None,
|
| 542 |
+
):
|
| 543 |
+
super().__init__(name=name)
|
| 544 |
+
self.scale = scale
|
| 545 |
+
self.padding = coords.horizontal.nodal_padding
|
| 546 |
+
unpadded_nodal_shape = tuple(
|
| 547 |
+
x - y for x, y in zip(coords.horizontal.nodal_shape, self.padding)
|
| 548 |
+
)
|
| 549 |
+
self.positional_features = hk.get_parameter(
|
| 550 |
+
'learned_positional_features',
|
| 551 |
+
(latent_size,) + unpadded_nodal_shape,
|
| 552 |
+
jnp.float32,
|
| 553 |
+
init=hk.initializers.Constant(0.0),
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
def __call__(
|
| 557 |
+
self,
|
| 558 |
+
inputs: typing.Pytree,
|
| 559 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 560 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 561 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 562 |
+
forcing: Optional[typing.Pytree] = None,
|
| 563 |
+
) -> dict[str, jnp.ndarray]:
|
| 564 |
+
"""Returns scaled parameter values at surface nodal locations."""
|
| 565 |
+
del inputs, memory, diagnostics, randomness, forcing # unused.
|
| 566 |
+
pad_x, pad_y = self.padding
|
| 567 |
+
positional_features = self.scale * jnp.pad(
|
| 568 |
+
self.positional_features, [(0, 0), (0, pad_x), (0, pad_y)]
|
| 569 |
+
)
|
| 570 |
+
return {'learned_positional_features': positional_features}
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 574 |
+
class EmbeddingSurfaceFeatures(hk.Module):
|
| 575 |
+
"""Feature module that specifies embedding surface outputs as features.
|
| 576 |
+
|
| 577 |
+
Returns {feature_name: nn_output}
|
| 578 |
+
where nn_output.shape = (output_size, lon, lat).
|
| 579 |
+
"""
|
| 580 |
+
|
| 581 |
+
def __init__(
|
| 582 |
+
self,
|
| 583 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 584 |
+
dt: float,
|
| 585 |
+
physics_specs: Any,
|
| 586 |
+
aux_features: typing.AuxFeatures,
|
| 587 |
+
feature_name: str,
|
| 588 |
+
output_size: int,
|
| 589 |
+
embedding_module: typing.EmbeddingModule,
|
| 590 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 591 |
+
name: Optional[str] = None,
|
| 592 |
+
):
|
| 593 |
+
super().__init__(name=name)
|
| 594 |
+
# output shapes are arrays to be pytree leaves for tree_map
|
| 595 |
+
output_shapes = {
|
| 596 |
+
feature_name: np.asarray((output_size,) + coords.horizontal.nodal_shape)
|
| 597 |
+
}
|
| 598 |
+
self.embedding_fn = embedding_module(
|
| 599 |
+
coords, dt, physics_specs, aux_features, output_shapes=output_shapes
|
| 600 |
+
)
|
| 601 |
+
self.features_transform_fn = features_transform_module(
|
| 602 |
+
coords, dt, physics_specs, aux_features
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
def __call__(
|
| 606 |
+
self,
|
| 607 |
+
inputs: typing.Pytree,
|
| 608 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 609 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 610 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 611 |
+
forcing: Optional[typing.Pytree] = None,
|
| 612 |
+
) -> typing.Pytree:
|
| 613 |
+
features = self.embedding_fn(
|
| 614 |
+
inputs, memory, diagnostics, randomness, forcing # pyrefly: ignore[bad-argument-type]
|
| 615 |
+
)
|
| 616 |
+
return self.features_transform_fn(features)
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 620 |
+
class EmbeddingVolumeFeatures(hk.Module):
|
| 621 |
+
"""Feature module that specifies embedding volume outputs as features.
|
| 622 |
+
|
| 623 |
+
Returns {feature_name_0: nn_output_0,
|
| 624 |
+
feature_name_1: nn_output_1,
|
| 625 |
+
...
|
| 626 |
+
}
|
| 627 |
+
where the NN output array has shape (output_size, level, lon, lat), which is
|
| 628 |
+
unpacked over output_size such that nn_output_{i}.shape = (level, lon, lat)
|
| 629 |
+
for each i in range(output_size).
|
| 630 |
+
"""
|
| 631 |
+
|
| 632 |
+
def __init__(
|
| 633 |
+
self,
|
| 634 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 635 |
+
dt: float,
|
| 636 |
+
physics_specs: Any,
|
| 637 |
+
aux_features: typing.AuxFeatures,
|
| 638 |
+
feature_name: str,
|
| 639 |
+
output_size: int,
|
| 640 |
+
embedding_module: typing.EmbeddingModule,
|
| 641 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 642 |
+
name: Optional[str] = None,
|
| 643 |
+
):
|
| 644 |
+
super().__init__(name=name)
|
| 645 |
+
# output shapes are arrays to be pytree leaves for tree_map
|
| 646 |
+
output_shapes = {
|
| 647 |
+
f'{feature_name}_{i}': np.asarray(coords.nodal_shape)
|
| 648 |
+
for i in range(output_size)
|
| 649 |
+
}
|
| 650 |
+
self.embedding_fn = embedding_module(
|
| 651 |
+
coords, dt, physics_specs, aux_features, output_shapes=output_shapes
|
| 652 |
+
)
|
| 653 |
+
self.features_transform_fn = features_transform_module(
|
| 654 |
+
coords, dt, physics_specs, aux_features
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
def __call__(
|
| 658 |
+
self,
|
| 659 |
+
inputs: typing.Pytree,
|
| 660 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 661 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 662 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 663 |
+
forcing: Optional[typing.Pytree] = None,
|
| 664 |
+
) -> typing.Pytree:
|
| 665 |
+
features = self.embedding_fn(
|
| 666 |
+
inputs, memory, diagnostics, randomness, forcing # pyrefly: ignore[bad-argument-type]
|
| 667 |
+
)
|
| 668 |
+
return self.features_transform_fn(features)
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 672 |
+
class FloatDataFeatures(hk.Module):
|
| 673 |
+
"""Feature module that supplies floating point covariates from data."""
|
| 674 |
+
|
| 675 |
+
def __init__(
|
| 676 |
+
self,
|
| 677 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 678 |
+
dt: float,
|
| 679 |
+
physics_specs: Any,
|
| 680 |
+
aux_features: typing.AuxFeatures,
|
| 681 |
+
covariate_data_path: str = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 682 |
+
covariate_keys: Sequence[str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 683 |
+
renaming_dict: Optional[Mapping[str, str]] = None,
|
| 684 |
+
compute_gradients_module: TransformModule = transforms.EmptyTransform,
|
| 685 |
+
name: Optional[str] = None,
|
| 686 |
+
):
|
| 687 |
+
super().__init__(name=name)
|
| 688 |
+
self.covariates = {}
|
| 689 |
+
self.compute_gradients_fn = compute_gradients_module(
|
| 690 |
+
coords, dt, physics_specs, aux_features
|
| 691 |
+
)
|
| 692 |
+
self.coords = coords
|
| 693 |
+
ds = xarray_utils.ds_from_path_or_aux(covariate_data_path, aux_features)
|
| 694 |
+
if renaming_dict is not None:
|
| 695 |
+
ds = ds.rename(renaming_dict)
|
| 696 |
+
lon, lat = (ds[xarray_utils.XR_LON_NAME], ds[xarray_utils.XR_LAT_NAME])
|
| 697 |
+
xarray_utils.verify_grid_consistency(lon, lat, coords.horizontal)
|
| 698 |
+
lon_lat_order = (xarray_utils.XR_LON_NAME, xarray_utils.XR_LAT_NAME)
|
| 699 |
+
for key in covariate_keys:
|
| 700 |
+
data = ds[key].transpose(*lon_lat_order)
|
| 701 |
+
data_units = scales.parse_units(data.attrs['units'])
|
| 702 |
+
data = physics_specs.nondimensionalize(data.values * data_units)
|
| 703 |
+
if data.ndim != 3:
|
| 704 |
+
data = data[np.newaxis, ...]
|
| 705 |
+
self.covariates[key] = data
|
| 706 |
+
|
| 707 |
+
def __call__(
|
| 708 |
+
self,
|
| 709 |
+
inputs: typing.Pytree,
|
| 710 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 711 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 712 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 713 |
+
forcing: Optional[typing.Pytree] = None,
|
| 714 |
+
) -> dict[str, jnp.ndarray]:
|
| 715 |
+
del inputs, memory, diagnostics, forcing, randomness # unused.
|
| 716 |
+
features = {k: v for k, v in self.covariates.items()}
|
| 717 |
+
modal_features = self.coords.horizontal.to_modal(features)
|
| 718 |
+
modal_features = { # jit should eliminate to_modal if it is not used.
|
| 719 |
+
KeyWithCosLatFactor(k, 0): v for k, v in modal_features.items()
|
| 720 |
+
}
|
| 721 |
+
modal_gradient_features = self.compute_gradients_fn(modal_features)
|
| 722 |
+
sec_lat = 1 / self.coords.horizontal.cos_lat
|
| 723 |
+
sec2_lat = self.coords.horizontal.sec2_lat
|
| 724 |
+
sec_lat_scales = {0: 1, 1: sec_lat, 2: sec2_lat}
|
| 725 |
+
for k, v in modal_gradient_features.items():
|
| 726 |
+
sec_lat_scale = sec_lat_scales[k.factor_order]
|
| 727 |
+
features[k.name] = self.coords.horizontal.to_nodal(v) * sec_lat_scale
|
| 728 |
+
return features
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 732 |
+
class CombinedFeatures(hk.Module):
|
| 733 |
+
"""Feature module that combines multiple feature modules together."""
|
| 734 |
+
|
| 735 |
+
def __init__(
|
| 736 |
+
self,
|
| 737 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 738 |
+
dt: float,
|
| 739 |
+
physics_specs: Any,
|
| 740 |
+
aux_features: typing.AuxFeatures,
|
| 741 |
+
feature_modules: Sequence[FeaturesModule] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 742 |
+
feature_module_names_to_exclude: Sequence[str] = tuple(),
|
| 743 |
+
features_to_exclude: Sequence[str] = tuple(),
|
| 744 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 745 |
+
name: Optional[str] = None,
|
| 746 |
+
):
|
| 747 |
+
super().__init__(name=name)
|
| 748 |
+
self.feature_fns = [
|
| 749 |
+
module(coords, dt, physics_specs, aux_features)
|
| 750 |
+
for module in feature_modules
|
| 751 |
+
]
|
| 752 |
+
self.feature_module_names_to_exclude = feature_module_names_to_exclude
|
| 753 |
+
self.features_to_exclude = features_to_exclude
|
| 754 |
+
self.features_transform_fn = features_transform_module(
|
| 755 |
+
coords, dt, physics_specs, aux_features
|
| 756 |
+
)
|
| 757 |
+
|
| 758 |
+
def __call__(
|
| 759 |
+
self,
|
| 760 |
+
inputs: typing.Pytree,
|
| 761 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 762 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 763 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 764 |
+
forcing: Optional[typing.Forcing] = None,
|
| 765 |
+
) -> dict[str, jnp.ndarray]:
|
| 766 |
+
all_features = {}
|
| 767 |
+
for feature_fn in self.feature_fns:
|
| 768 |
+
if type(feature_fn).__name__ not in self.feature_module_names_to_exclude:
|
| 769 |
+
features = feature_fn(inputs, memory, diagnostics, randomness, forcing)
|
| 770 |
+
for k, v in features.items():
|
| 771 |
+
if k in all_features:
|
| 772 |
+
raise ValueError(f'Encountered duplicate feature {k}')
|
| 773 |
+
all_features[k] = v
|
| 774 |
+
all_features = self.features_transform_fn(all_features)
|
| 775 |
+
for k in self.features_to_exclude:
|
| 776 |
+
all_features.pop(k, None)
|
| 777 |
+
return all_features
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 781 |
+
class NullFeatures(hk.Module):
|
| 782 |
+
"""Placeholder features module that returns an empty dict."""
|
| 783 |
+
|
| 784 |
+
def __init__(
|
| 785 |
+
self,
|
| 786 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 787 |
+
dt: float,
|
| 788 |
+
physics_specs: Any,
|
| 789 |
+
aux_features: typing.AuxFeatures,
|
| 790 |
+
name: Optional[str] = None,
|
| 791 |
+
):
|
| 792 |
+
del coords, dt, physics_specs, aux_features # unused
|
| 793 |
+
super().__init__(name=name)
|
| 794 |
+
|
| 795 |
+
def __call__(
|
| 796 |
+
self,
|
| 797 |
+
inputs: typing.Pytree,
|
| 798 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 799 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 800 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 801 |
+
forcing: Optional[typing.Pytree] = None,
|
| 802 |
+
) -> dict[str, jnp.ndarray]:
|
| 803 |
+
del inputs, memory, diagnostics, randomness, forcing # unused
|
| 804 |
+
return {}
|
| 805 |
+
|
| 806 |
+
|
| 807 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 808 |
+
class PressureFeatures(hk.Module):
|
| 809 |
+
"""Feature module that computes pressure."""
|
| 810 |
+
|
| 811 |
+
def __init__(
|
| 812 |
+
self,
|
| 813 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 814 |
+
dt: float,
|
| 815 |
+
physics_specs: Any,
|
| 816 |
+
aux_features: typing.AuxFeatures,
|
| 817 |
+
features_transform_module: TransformModule = transforms.IdentityTransform,
|
| 818 |
+
name: Optional[str] = None,
|
| 819 |
+
):
|
| 820 |
+
super().__init__(name=name)
|
| 821 |
+
self.coords = coords
|
| 822 |
+
self.features_transform_fn = features_transform_module(
|
| 823 |
+
coords, dt, physics_specs, aux_features
|
| 824 |
+
)
|
| 825 |
+
|
| 826 |
+
def _nodal_pressure(
|
| 827 |
+
self,
|
| 828 |
+
inputs: typing.Pytree,
|
| 829 |
+
prefix: str = '',
|
| 830 |
+
) -> Mapping[str, Array]:
|
| 831 |
+
"""Computes nodal pressure from model inputs."""
|
| 832 |
+
# Compute nodal, dimensionalized quantities
|
| 833 |
+
to_nodal_fn = self.coords.horizontal.to_nodal
|
| 834 |
+
sigma = self.coords.vertical.centers
|
| 835 |
+
surface_pressure = jnp.exp(to_nodal_fn(inputs['log_surface_pressure']))
|
| 836 |
+
pressure = surface_pressure * sigma[:, jnp.newaxis, jnp.newaxis]
|
| 837 |
+
nodal_features = {prefix + 'pressure': pressure}
|
| 838 |
+
return nodal_features
|
| 839 |
+
|
| 840 |
+
def __call__(
|
| 841 |
+
self,
|
| 842 |
+
inputs: typing.Pytree,
|
| 843 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 844 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 845 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 846 |
+
forcing: Optional[typing.Pytree] = None,
|
| 847 |
+
) -> typing.Pytree:
|
| 848 |
+
del memory, diagnostics, randomness, forcing # unused.
|
| 849 |
+
nodal_features = self._nodal_pressure(inputs)
|
| 850 |
+
return self.features_transform_fn(nodal_features)
|
| 851 |
+
|
| 852 |
+
|
| 853 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 854 |
+
class MemoryPressureFeatures(PressureFeatures):
|
| 855 |
+
"""Feature module that computes pressure from memory values."""
|
| 856 |
+
|
| 857 |
+
def __call__(
|
| 858 |
+
self,
|
| 859 |
+
inputs: typing.Pytree,
|
| 860 |
+
memory: Optional[typing.PyTreeState] = None,
|
| 861 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 862 |
+
randomness: Optional[typing.PyTreeState] = None,
|
| 863 |
+
forcing: Optional[typing.Pytree] = None,
|
| 864 |
+
) -> typing.Pytree:
|
| 865 |
+
del inputs, diagnostics, randomness, forcing # unused.
|
| 866 |
+
nodal_features = self._nodal_pressure(memory, 'memory_')
|
| 867 |
+
return self.features_transform_fn(nodal_features)
|
model/legacy/filters.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines `filtering` that aim to improve stability of integration."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import filtering
|
| 19 |
+
from dinosaur import pytree_utils
|
| 20 |
+
from dinosaur import scales
|
| 21 |
+
from dinosaur import time_integration
|
| 22 |
+
from dinosaur import typing
|
| 23 |
+
import gin
|
| 24 |
+
import haiku as hk
|
| 25 |
+
import jax
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
QuantityOrStr = Union[str, scales.Quantity]
|
| 30 |
+
|
| 31 |
+
StepFilterModule = Callable[..., typing.PyTreeStepFilterFn]
|
| 32 |
+
TransformModule = typing.TransformModule
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# =============================================================================
|
| 36 |
+
# Step filters that attenuate modal components between time steps.
|
| 37 |
+
# =============================================================================
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@gin.register
|
| 41 |
+
class NoFilter(hk.Module):
|
| 42 |
+
"""Filter module that performs no filtering."""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 47 |
+
dt: float,
|
| 48 |
+
physics_specs: Any,
|
| 49 |
+
aux_features: Dict[str, Any],
|
| 50 |
+
name: Optional[str] = None,
|
| 51 |
+
):
|
| 52 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 53 |
+
super().__init__(name=name)
|
| 54 |
+
|
| 55 |
+
def __call__(
|
| 56 |
+
self,
|
| 57 |
+
u: typing.PyTreeState,
|
| 58 |
+
u_next: typing.PyTreeState
|
| 59 |
+
) -> typing.PyTreeState:
|
| 60 |
+
del u # unused.
|
| 61 |
+
return u_next
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@gin.register
|
| 65 |
+
class ClipFilter(hk.Module):
|
| 66 |
+
"""Filter that clips highest total wavenumber in the next state."""
|
| 67 |
+
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 71 |
+
dt: float,
|
| 72 |
+
physics_specs: Any,
|
| 73 |
+
aux_features: Dict[str, Any],
|
| 74 |
+
wavenumbers_to_clip: int = 1,
|
| 75 |
+
name: Optional[str] = None,
|
| 76 |
+
):
|
| 77 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 78 |
+
del dt, physics_specs, aux_features # unused.
|
| 79 |
+
super().__init__(name=name)
|
| 80 |
+
self.coords = coords
|
| 81 |
+
self.wavenumbers_to_clip = wavenumbers_to_clip
|
| 82 |
+
|
| 83 |
+
def __call__(
|
| 84 |
+
self,
|
| 85 |
+
u: typing.PyTreeState,
|
| 86 |
+
u_next: typing.PyTreeState
|
| 87 |
+
) -> typing.PyTreeState:
|
| 88 |
+
del u # unused.
|
| 89 |
+
return self.coords.horizontal.clip_wavenumbers(
|
| 90 |
+
u_next, self.wavenumbers_to_clip
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@gin.register
|
| 95 |
+
class ExponentialLeapfrogFilter(hk.Module):
|
| 96 |
+
"""Filter that removes high frequency components from a spectral state."""
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 101 |
+
dt: float,
|
| 102 |
+
physics_specs: Any,
|
| 103 |
+
aux_features: Dict[str, Any],
|
| 104 |
+
tau: QuantityOrStr = '0.010938',
|
| 105 |
+
order: int = 18,
|
| 106 |
+
cutoff: float = 0,
|
| 107 |
+
name: Optional[str] = None,
|
| 108 |
+
):
|
| 109 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 110 |
+
del aux_features # unused.
|
| 111 |
+
super().__init__(name=name)
|
| 112 |
+
tau = physics_specs.nondimensionalize(scales.Quantity(tau))
|
| 113 |
+
self.filter_fn = time_integration.exponential_leapfrog_step_filter(
|
| 114 |
+
coords.horizontal, dt, tau, order, cutoff)
|
| 115 |
+
|
| 116 |
+
def __call__(
|
| 117 |
+
self,
|
| 118 |
+
u: typing.PyTreeState,
|
| 119 |
+
u_next: typing.PyTreeState
|
| 120 |
+
) -> typing.PyTreeState:
|
| 121 |
+
return self.filter_fn(u, u_next)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@gin.register
|
| 125 |
+
class ExponentialFilter(hk.Module):
|
| 126 |
+
"""Filter that removes high frequency components from a spectral state."""
|
| 127 |
+
|
| 128 |
+
def __init__(
|
| 129 |
+
self,
|
| 130 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 131 |
+
dt: float,
|
| 132 |
+
physics_specs: Any,
|
| 133 |
+
aux_features: Dict[str, Any],
|
| 134 |
+
tau: QuantityOrStr = '0.010938',
|
| 135 |
+
order: int = 18,
|
| 136 |
+
cutoff: float = 0,
|
| 137 |
+
name: Optional[str] = None,
|
| 138 |
+
):
|
| 139 |
+
"""See `time_integration.exponential_step_filter` for details."""
|
| 140 |
+
del aux_features # unused.
|
| 141 |
+
super().__init__(name=name)
|
| 142 |
+
tau = physics_specs.nondimensionalize(scales.Quantity(tau))
|
| 143 |
+
self.filter_fn = time_integration.exponential_step_filter(
|
| 144 |
+
coords.horizontal, dt, tau, order, cutoff)
|
| 145 |
+
|
| 146 |
+
def __call__(
|
| 147 |
+
self,
|
| 148 |
+
u: typing.PyTreeState,
|
| 149 |
+
u_next: typing.PyTreeState
|
| 150 |
+
) -> typing.PyTreeState:
|
| 151 |
+
return self.filter_fn(u, u_next)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@gin.register
|
| 155 |
+
class HorizontalDiffusionFilter(hk.Module):
|
| 156 |
+
"""Filter that applies implicit diffusion operator to a spectral state."""
|
| 157 |
+
|
| 158 |
+
def __init__(
|
| 159 |
+
self,
|
| 160 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 161 |
+
dt: float,
|
| 162 |
+
physics_specs: Any,
|
| 163 |
+
aux_features: Dict[str, Any],
|
| 164 |
+
tau: QuantityOrStr = '',
|
| 165 |
+
order: int = 1,
|
| 166 |
+
name: Optional[str] = None,
|
| 167 |
+
):
|
| 168 |
+
"""See `time_integration.horizontal_diffusion_filter` for details."""
|
| 169 |
+
del aux_features # unused.
|
| 170 |
+
super().__init__(name=name)
|
| 171 |
+
tau = physics_specs.nondimensionalize(scales.Quantity(tau))
|
| 172 |
+
self.filter_fn = time_integration.horizontal_diffusion_step_filter(
|
| 173 |
+
coords.horizontal, dt, tau, order)
|
| 174 |
+
|
| 175 |
+
def __call__(
|
| 176 |
+
self,
|
| 177 |
+
u: typing.PyTreeState,
|
| 178 |
+
u_next: typing.PyTreeState
|
| 179 |
+
) -> typing.PyTreeState:
|
| 180 |
+
del u # unused
|
| 181 |
+
return self.filter_fn(u_next) # pytype: disable=wrong-arg-count # always-use-return-annotations
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@gin.register
|
| 185 |
+
class RobertAsselinLeapfrogFilter(hk.Module):
|
| 186 |
+
"""Time smoothing filter."""
|
| 187 |
+
|
| 188 |
+
def __init__(
|
| 189 |
+
self,
|
| 190 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 191 |
+
dt: float,
|
| 192 |
+
physics_specs: Any,
|
| 193 |
+
aux_features: Dict[str, Any],
|
| 194 |
+
strength: float = 0.05,
|
| 195 |
+
name: Optional[str] = None,
|
| 196 |
+
):
|
| 197 |
+
"""See `time_integration.robert_asselin_leapfrog_filter` for details."""
|
| 198 |
+
del dt, coords, physics_specs, aux_features # unused.
|
| 199 |
+
super().__init__(name=name)
|
| 200 |
+
self.filter_fn = time_integration.robert_asselin_leapfrog_filter(strength)
|
| 201 |
+
|
| 202 |
+
def __call__(
|
| 203 |
+
self,
|
| 204 |
+
u: typing.PyTreeState,
|
| 205 |
+
u_next: typing.PyTreeState
|
| 206 |
+
) -> typing.PyTreeState:
|
| 207 |
+
return self.filter_fn(u, u_next)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
@gin.register
|
| 211 |
+
class LearnedExponentialFilter(hk.Module):
|
| 212 |
+
"""Low pass filter with learned parameters."""
|
| 213 |
+
|
| 214 |
+
def __init__(
|
| 215 |
+
self,
|
| 216 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 217 |
+
dt: float,
|
| 218 |
+
physics_specs: Any,
|
| 219 |
+
aux_features: Dict[str, Any],
|
| 220 |
+
name: Optional[str] = None,
|
| 221 |
+
):
|
| 222 |
+
del dt, physics_specs, aux_features # unused.
|
| 223 |
+
self.coords = coords
|
| 224 |
+
self.a_init = hk.initializers.Constant(16)
|
| 225 |
+
self.p_init = hk.initializers.Constant(18)
|
| 226 |
+
self.c_init = hk.initializers.Constant(0)
|
| 227 |
+
super().__init__(name=name)
|
| 228 |
+
|
| 229 |
+
def __call__(
|
| 230 |
+
self,
|
| 231 |
+
u: typing.PyTreeState,
|
| 232 |
+
u_next: typing.PyTreeState
|
| 233 |
+
) -> typing.PyTreeState:
|
| 234 |
+
del u # unused.
|
| 235 |
+
a_logit = hk.get_parameter('attenuation_logit', shape=(), init=self.a_init)
|
| 236 |
+
p_logit = hk.get_parameter('order_logit', shape=(), init=self.p_init)
|
| 237 |
+
c_logit = hk.get_parameter('threshold_logit', shape=(), init=self.c_init)
|
| 238 |
+
a = jax.nn.softplus(a_logit)
|
| 239 |
+
p = jax.nn.softplus(p_logit)
|
| 240 |
+
c = jax.nn.sigmoid(c_logit)
|
| 241 |
+
filter_fn = filtering.exponential_filter(self.coords.horizontal, a, p, c) # pytype: disable=wrong-arg-types # jax-nn-types
|
| 242 |
+
return filter_fn(u_next)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@gin.register
|
| 246 |
+
class SequentialStepFilter(hk.Module):
|
| 247 |
+
"""Filter module that combines multiple step filters applied sequentially."""
|
| 248 |
+
|
| 249 |
+
def __init__(
|
| 250 |
+
self,
|
| 251 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 252 |
+
dt: float,
|
| 253 |
+
physics_specs: Any,
|
| 254 |
+
aux_features: Dict[str, Any],
|
| 255 |
+
filter_modules: Sequence[StepFilterModule],
|
| 256 |
+
name: Optional[str] = None,
|
| 257 |
+
):
|
| 258 |
+
super().__init__(name=name)
|
| 259 |
+
self.filter_fns = [module(coords, dt, physics_specs, aux_features)
|
| 260 |
+
for module in filter_modules]
|
| 261 |
+
|
| 262 |
+
def __call__(
|
| 263 |
+
self,
|
| 264 |
+
u: typing.PyTreeState,
|
| 265 |
+
u_next: typing.PyTreeState
|
| 266 |
+
) -> typing.PyTreeState:
|
| 267 |
+
for filter_fn in self.filter_fns:
|
| 268 |
+
u_next = filter_fn(u, u_next)
|
| 269 |
+
return u_next
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@gin.register
|
| 273 |
+
class LayeredStepFilter(hk.Module):
|
| 274 |
+
"""Filter decorator that uses varying time-scales at different levels."""
|
| 275 |
+
|
| 276 |
+
def __init__(
|
| 277 |
+
self,
|
| 278 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 279 |
+
dt: float,
|
| 280 |
+
physics_specs: Any,
|
| 281 |
+
aux_features: Dict[str, Any],
|
| 282 |
+
filter_module: Union[HorizontalDiffusionFilter, ExponentialFilter],
|
| 283 |
+
tau_vals: Union[Sequence[float], np.ndarray],
|
| 284 |
+
tau_units: QuantityOrStr,
|
| 285 |
+
name: Optional[str] = None,
|
| 286 |
+
):
|
| 287 |
+
super().__init__(name=name)
|
| 288 |
+
tau = (np.asarray(tau_vals) * scales.Quantity(tau_units))
|
| 289 |
+
tau = tau[:, np.newaxis, np.newaxis] # add spatial axes.
|
| 290 |
+
self.filter_fn = filter_module(
|
| 291 |
+
coords, dt, physics_specs, aux_features, tau=tau) # pyrefly: ignore[bad-argument-count, unexpected-keyword]
|
| 292 |
+
|
| 293 |
+
def __call__(
|
| 294 |
+
self,
|
| 295 |
+
u: typing.PyTreeState,
|
| 296 |
+
u_next: typing.PyTreeState
|
| 297 |
+
) -> typing.PyTreeState:
|
| 298 |
+
return self.filter_fn(u, u_next) # pyrefly: ignore[not-callable]
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@gin.register
|
| 302 |
+
class MaskedFilter(hk.Module):
|
| 303 |
+
"""Filter that is only applied to a part of the state."""
|
| 304 |
+
|
| 305 |
+
def __init__(
|
| 306 |
+
self,
|
| 307 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 308 |
+
dt: float,
|
| 309 |
+
physics_specs: Any,
|
| 310 |
+
aux_features: Dict[str, Any],
|
| 311 |
+
filter_module: StepFilterModule,
|
| 312 |
+
mask: typing.Pytree,
|
| 313 |
+
name: Optional[str] = None,
|
| 314 |
+
):
|
| 315 |
+
super().__init__(name=name)
|
| 316 |
+
self.filter_fn = filter_module(coords, dt, physics_specs, aux_features)
|
| 317 |
+
self.mask = mask
|
| 318 |
+
|
| 319 |
+
def __call__(
|
| 320 |
+
self,
|
| 321 |
+
u: typing.PyTreeState,
|
| 322 |
+
u_next: typing.PyTreeState
|
| 323 |
+
) -> typing.PyTreeState:
|
| 324 |
+
mask = type(u_next)(**self.mask) # convert to same structure.
|
| 325 |
+
return jax.tree_util.tree_map(
|
| 326 |
+
lambda x, y, b: self.filter_fn(x, y) if b else y,
|
| 327 |
+
u, u_next, mask)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
@gin.register
|
| 331 |
+
class FilterFromTransform(hk.Module):
|
| 332 |
+
"""Filter module that wraps a transform module."""
|
| 333 |
+
|
| 334 |
+
def __init__(
|
| 335 |
+
self,
|
| 336 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 337 |
+
dt: float,
|
| 338 |
+
physics_specs: Any,
|
| 339 |
+
aux_features: Dict[str, Any],
|
| 340 |
+
transform_module: TransformModule,
|
| 341 |
+
name: Optional[str] = None,
|
| 342 |
+
):
|
| 343 |
+
super().__init__(name=name)
|
| 344 |
+
self.transform_fn = transform_module(
|
| 345 |
+
coords, dt, physics_specs, aux_features)
|
| 346 |
+
|
| 347 |
+
def __call__(
|
| 348 |
+
self,
|
| 349 |
+
u: typing.PyTreeState,
|
| 350 |
+
u_next: typing.PyTreeState
|
| 351 |
+
) -> typing.PyTreeState:
|
| 352 |
+
return self.transform_fn(u_next)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@gin.register
|
| 356 |
+
class FixGlobalMeanFilter(hk.Module):
|
| 357 |
+
"""Filter that removes the change in the global mean of certain keys."""
|
| 358 |
+
|
| 359 |
+
def __init__(
|
| 360 |
+
self,
|
| 361 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 362 |
+
dt: float,
|
| 363 |
+
physics_specs: Any,
|
| 364 |
+
aux_features: Dict[str, Any],
|
| 365 |
+
keys: tuple[str, ...] = ('log_surface_pressure',),
|
| 366 |
+
name: Optional[str] = None,
|
| 367 |
+
):
|
| 368 |
+
del aux_features # unused.
|
| 369 |
+
super().__init__(name=name)
|
| 370 |
+
self.keys = keys
|
| 371 |
+
|
| 372 |
+
def __call__(
|
| 373 |
+
self, u: typing.PyTreeState, u_next: typing.PyTreeState
|
| 374 |
+
) -> typing.PyTreeState:
|
| 375 |
+
u_dict, _ = pytree_utils.as_dict(u)
|
| 376 |
+
u_dict, _ = pytree_utils.flatten_dict(u_dict)
|
| 377 |
+
u_next_dict, from_dict_fn = pytree_utils.as_dict(u_next)
|
| 378 |
+
u_next_dict, _ = pytree_utils.flatten_dict(u_next_dict)
|
| 379 |
+
for key in self.keys:
|
| 380 |
+
global_mean = u_dict[key][..., 0]
|
| 381 |
+
u_next_dict[key] = u_next_dict[key].at[..., 0].set(global_mean)
|
| 382 |
+
u_next_dict = pytree_utils.unflatten_dict(u_next_dict)
|
| 383 |
+
return from_dict_fn(u_next_dict)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# =============================================================================
|
| 387 |
+
# Filters that act on modal variables without time-step context.
|
| 388 |
+
# =============================================================================
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
@gin.register
|
| 392 |
+
class DataNoFilter(hk.Module):
|
| 393 |
+
"""Filter module that performs no filtering."""
|
| 394 |
+
|
| 395 |
+
def __init__(
|
| 396 |
+
self,
|
| 397 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 398 |
+
dt: float,
|
| 399 |
+
physics_specs: Any,
|
| 400 |
+
aux_features: Dict[str, Any],
|
| 401 |
+
name: Optional[str] = None,
|
| 402 |
+
):
|
| 403 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 404 |
+
super().__init__(name=name)
|
| 405 |
+
|
| 406 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 407 |
+
return inputs
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
@gin.register
|
| 411 |
+
class DataExponentialFilter(hk.Module):
|
| 412 |
+
"""Filter that removes high frequency components from a modal data."""
|
| 413 |
+
|
| 414 |
+
def __init__(
|
| 415 |
+
self,
|
| 416 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 417 |
+
dt: float,
|
| 418 |
+
physics_specs: Any,
|
| 419 |
+
aux_features: Dict[str, Any],
|
| 420 |
+
attenuation: float = 16,
|
| 421 |
+
order: int = 18,
|
| 422 |
+
cutoff: float = 0,
|
| 423 |
+
name: Optional[str] = None,
|
| 424 |
+
):
|
| 425 |
+
"""See `filtering.exponential_filter` for details."""
|
| 426 |
+
del dt, physics_specs, aux_features # unused.
|
| 427 |
+
super().__init__(name=name)
|
| 428 |
+
self.filter_fn = filtering.exponential_filter(
|
| 429 |
+
coords.horizontal, attenuation, order, cutoff)
|
| 430 |
+
|
| 431 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 432 |
+
return self.filter_fn(inputs)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@gin.register
|
| 436 |
+
class PerVariableDataFilter(hk.Module):
|
| 437 |
+
"""Filter module that applies different filters for each variable."""
|
| 438 |
+
|
| 439 |
+
def __init__(
|
| 440 |
+
self,
|
| 441 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 442 |
+
dt: float,
|
| 443 |
+
physics_specs: Any,
|
| 444 |
+
aux_features: Dict[str, Any],
|
| 445 |
+
per_variable_filters: Dict[str, Any],
|
| 446 |
+
name: Optional[str] = None,
|
| 447 |
+
):
|
| 448 |
+
"""See `filtering.exponential_filter` for details."""
|
| 449 |
+
super().__init__(name=name)
|
| 450 |
+
self.filter_fns = jax.tree_util.tree_map(
|
| 451 |
+
lambda m: m(coords, dt, physics_specs, aux_features),
|
| 452 |
+
per_variable_filters)
|
| 453 |
+
|
| 454 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 455 |
+
inputs_dict, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 456 |
+
return from_dict_fn(jax.tree_util.tree_map(
|
| 457 |
+
lambda x, fn: fn(x), inputs_dict, self.filter_fns))
|
model/legacy/forcings.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines `forcing` modules that produce time-dependent focing values."""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import functools
|
| 19 |
+
import logging
|
| 20 |
+
from typing import Any, Optional, Union
|
| 21 |
+
|
| 22 |
+
from dinosaur import coordinate_systems
|
| 23 |
+
from dinosaur import scales
|
| 24 |
+
from dinosaur import typing
|
| 25 |
+
import gin
|
| 26 |
+
import haiku as hk
|
| 27 |
+
import jax
|
| 28 |
+
import jax.numpy as jnp
|
| 29 |
+
from model.legacy import transforms
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 33 |
+
tree_map = jax.tree_util.tree_map
|
| 34 |
+
units = scales.units
|
| 35 |
+
|
| 36 |
+
Pytree = typing.Pytree
|
| 37 |
+
ForcingData = typing.ForcingData
|
| 38 |
+
ForcingFn = typing.ForcingFn
|
| 39 |
+
Forcing = typing.Forcing
|
| 40 |
+
TransformModule = typing.TransformModule
|
| 41 |
+
Quantity = units.Quantity
|
| 42 |
+
QuantityOrStr = Union[str, scales.Quantity]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# _FORCING_ERRORS global will store errors obtained during a io_callback.
|
| 46 |
+
# The user can periodically call _check_errors to see if errors have accumulated
|
| 47 |
+
# TODO(langmore) Use a more universal mechanism (not just in forcings.py) to
|
| 48 |
+
# handle errors, if we like this, then make public.
|
| 49 |
+
_FORCING_ERRORS = []
|
| 50 |
+
|
| 51 |
+
# pylint: disable=logging-fstring-interpolation
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ForcingDataError(Exception):
|
| 55 |
+
"""To raise when an error is encountered with forcing data."""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 59 |
+
class NoForcing(hk.Module):
|
| 60 |
+
"""Module that returns an empty Forcing object."""
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self,
|
| 64 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 65 |
+
dt: float,
|
| 66 |
+
physics_specs: Any,
|
| 67 |
+
aux_features: typing.AuxFeatures,
|
| 68 |
+
time_axis: int = 0,
|
| 69 |
+
name: Optional[str] = None,
|
| 70 |
+
):
|
| 71 |
+
super().__init__(name=name)
|
| 72 |
+
del coords, dt, physics_specs, aux_features, time_axis
|
| 73 |
+
|
| 74 |
+
def __call__(
|
| 75 |
+
self,
|
| 76 |
+
forcing_data: ForcingData,
|
| 77 |
+
sim_time: float,
|
| 78 |
+
) -> Forcing:
|
| 79 |
+
"""Returns forcings at the specified sim_time."""
|
| 80 |
+
del forcing_data, sim_time
|
| 81 |
+
return {}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 85 |
+
class DynamicDataForcing(hk.Module):
|
| 86 |
+
"""Modules that returns forcing values by querying time-varying data.
|
| 87 |
+
|
| 88 |
+
Input to __call__ `sim_time` must match a value in forcing_data['sim_time']
|
| 89 |
+
within dt_tolerance, or else it returns nan for all pytree values.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 95 |
+
dt: float,
|
| 96 |
+
physics_specs: Any,
|
| 97 |
+
aux_features: typing.AuxFeatures,
|
| 98 |
+
inputs_to_units_mapping: dict[str, str],
|
| 99 |
+
forcing_transform: TransformModule = transforms.IdentityTransform,
|
| 100 |
+
time_axis: int = 0,
|
| 101 |
+
data_time_step: float | QuantityOrStr | None = None,
|
| 102 |
+
dt_tolerance: Union[float, QuantityOrStr] = '1 hour',
|
| 103 |
+
# TODO(langmore) Remove checking once bug arising from http://cl/624039690
|
| 104 |
+
# is fixed.
|
| 105 |
+
check_sim_time_errors: bool = False,
|
| 106 |
+
name: Optional[str] = None,
|
| 107 |
+
):
|
| 108 |
+
logging.info(f'[NGCM] Initializing DynamicDataForcing with {dt_tolerance=}')
|
| 109 |
+
# TODO(shoyer): remove data_time_step entirely, once we're sure that no
|
| 110 |
+
# saved checkpoints that we care about will break.
|
| 111 |
+
del data_time_step # no longer used
|
| 112 |
+
super().__init__(name=name)
|
| 113 |
+
self.time_axis = time_axis
|
| 114 |
+
self.nondim_transform_fn = transforms.NondimensionalizeTransform(
|
| 115 |
+
coords,
|
| 116 |
+
dt,
|
| 117 |
+
physics_specs,
|
| 118 |
+
aux_features,
|
| 119 |
+
input_coords=None,
|
| 120 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 121 |
+
)
|
| 122 |
+
self.forcing_transform_fn = forcing_transform(
|
| 123 |
+
coords, dt, physics_specs, aux_features
|
| 124 |
+
)
|
| 125 |
+
if isinstance(dt_tolerance, (str, scales.Quantity)): # pyrefly: ignore[invalid-argument]
|
| 126 |
+
dt_tolerance = physics_specs.nondimensionalize(
|
| 127 |
+
scales.Quantity(dt_tolerance)
|
| 128 |
+
)
|
| 129 |
+
self.dt_tolerance = dt_tolerance
|
| 130 |
+
self._check_sim_time_errors = check_sim_time_errors
|
| 131 |
+
|
| 132 |
+
def __call__(
|
| 133 |
+
self,
|
| 134 |
+
forcing_data: ForcingData,
|
| 135 |
+
sim_time: float,
|
| 136 |
+
) -> Forcing:
|
| 137 |
+
"""Returns forcings at the specified sim_time."""
|
| 138 |
+
forcing_data = self.nondim_transform_fn(forcing_data)
|
| 139 |
+
|
| 140 |
+
times = forcing_data['sim_time']
|
| 141 |
+
approx_index = jnp.interp(sim_time, times, jnp.arange(times.size))
|
| 142 |
+
index = jnp.round(approx_index).astype('int32')
|
| 143 |
+
|
| 144 |
+
# Slice leaf values by index
|
| 145 |
+
field_index_fn = functools.partial(
|
| 146 |
+
jax.lax.dynamic_index_in_dim,
|
| 147 |
+
index=index,
|
| 148 |
+
axis=self.time_axis,
|
| 149 |
+
keepdims=False,
|
| 150 |
+
)
|
| 151 |
+
_assert_no_scalars(forcing_data)
|
| 152 |
+
forcing = tree_map(field_index_fn, forcing_data)
|
| 153 |
+
|
| 154 |
+
# Replace leaf values with nan if forcing['sim_time'] does not match
|
| 155 |
+
# the requested sim_time value within dt_tolerance.
|
| 156 |
+
abs_error = jnp.abs(forcing['sim_time'] - sim_time)
|
| 157 |
+
is_valid = abs_error < self.dt_tolerance
|
| 158 |
+
forcing = jax.tree_util.tree_map(
|
| 159 |
+
lambda x: jnp.where(is_valid, x, jnp.nan), forcing
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Also add errors (if any) to _FORCING_ERRORS so _check_errors can be called
|
| 163 |
+
# to raise.
|
| 164 |
+
if self._check_sim_time_errors:
|
| 165 |
+
jax.experimental.io_callback(
|
| 166 |
+
_check_sim_time_close_to_forcing_sim_time,
|
| 167 |
+
None, # Returns None
|
| 168 |
+
sim_time=sim_time,
|
| 169 |
+
forcing_sim_time=forcing['sim_time'],
|
| 170 |
+
tolerance=self.dt_tolerance,
|
| 171 |
+
)
|
| 172 |
+
return self.forcing_transform_fn(forcing)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 176 |
+
class PersistenceDataForcing(hk.Module):
|
| 177 |
+
"""Modules that returns forcing using first time index of forcing_data."""
|
| 178 |
+
|
| 179 |
+
def __init__(
|
| 180 |
+
self,
|
| 181 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 182 |
+
dt: float,
|
| 183 |
+
physics_specs: Any,
|
| 184 |
+
aux_features: typing.AuxFeatures,
|
| 185 |
+
inputs_to_units_mapping: dict[str, str],
|
| 186 |
+
forcing_transform: TransformModule = transforms.IdentityTransform,
|
| 187 |
+
time_axis: int = 0,
|
| 188 |
+
name: Optional[str] = None,
|
| 189 |
+
):
|
| 190 |
+
logging.info('[NGCM] Initializing PersistenceDataForcing')
|
| 191 |
+
super().__init__(name=name)
|
| 192 |
+
self.time_axis = time_axis
|
| 193 |
+
self.nondim_transform_fn = transforms.NondimensionalizeTransform(
|
| 194 |
+
coords,
|
| 195 |
+
dt,
|
| 196 |
+
physics_specs,
|
| 197 |
+
aux_features,
|
| 198 |
+
input_coords=None,
|
| 199 |
+
inputs_to_units_mapping=inputs_to_units_mapping,
|
| 200 |
+
)
|
| 201 |
+
self.forcing_transform_fn = forcing_transform(
|
| 202 |
+
coords, dt, physics_specs, aux_features
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
def __call__(
|
| 206 |
+
self,
|
| 207 |
+
forcing_data: ForcingData,
|
| 208 |
+
sim_time: float,
|
| 209 |
+
) -> Forcing:
|
| 210 |
+
"""Returns forcings from the first time index of sim_time."""
|
| 211 |
+
del sim_time # unused
|
| 212 |
+
forcing_data = self.nondim_transform_fn(forcing_data)
|
| 213 |
+
idx = 0
|
| 214 |
+
|
| 215 |
+
# Slice leaf values by index
|
| 216 |
+
field_index_fn = functools.partial(
|
| 217 |
+
jax.lax.dynamic_index_in_dim,
|
| 218 |
+
index=idx,
|
| 219 |
+
axis=self.time_axis,
|
| 220 |
+
keepdims=False,
|
| 221 |
+
)
|
| 222 |
+
_assert_no_scalars(forcing_data)
|
| 223 |
+
forcing = tree_map(field_index_fn, forcing_data)
|
| 224 |
+
return self.forcing_transform_fn(forcing)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features'])
|
| 228 |
+
class IncrementSSTForcingTransform(hk.Module):
|
| 229 |
+
"""Transform Forcing by uniformly incrementing sea surface temperature."""
|
| 230 |
+
|
| 231 |
+
def __init__(
|
| 232 |
+
self,
|
| 233 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 234 |
+
dt: float,
|
| 235 |
+
physics_specs: Any,
|
| 236 |
+
aux_features: typing.AuxFeatures,
|
| 237 |
+
temperature_change: Quantity,
|
| 238 |
+
key: str = 'sea_surface_temperature',
|
| 239 |
+
name: Optional[str] = None,
|
| 240 |
+
):
|
| 241 |
+
super().__init__(name=name)
|
| 242 |
+
del coords, dt, aux_features # unused
|
| 243 |
+
self.temperature_change = physics_specs.nondimensionalize(
|
| 244 |
+
units.Quantity(temperature_change) # pyrefly: ignore[not-callable]
|
| 245 |
+
)
|
| 246 |
+
self.key = key
|
| 247 |
+
|
| 248 |
+
def __call__(self, forcing: Forcing) -> Forcing:
|
| 249 |
+
assert isinstance(forcing, dict)
|
| 250 |
+
forcing = forcing.copy()
|
| 251 |
+
forcing[self.key] = forcing[self.key] + self.temperature_change
|
| 252 |
+
return forcing
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def _assert_no_scalars(tree: Pytree):
|
| 256 |
+
dims = tree_map(lambda x: len(jnp.shape(x)), tree)
|
| 257 |
+
if not all(d > 0 for d in tree_leaves(dims)):
|
| 258 |
+
raise ValueError(f'Scalar shapes encountered: {dims=}')
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# TODO(langmore) Use a more universal mechanism (not just in forcings.py) to
|
| 262 |
+
# handle errors, if we like this, then make public.
|
| 263 |
+
def _check_sim_time_close_to_forcing_sim_time(
|
| 264 |
+
sim_time: np.ndarray,
|
| 265 |
+
forcing_sim_time: np.ndarray,
|
| 266 |
+
tolerance: float,
|
| 267 |
+
) -> None:
|
| 268 |
+
"""Checks |sim_time - forcing_sim_time| < tolerance add to _FORCING_ERRORS."""
|
| 269 |
+
abs_error = np.abs(forcing_sim_time - sim_time)
|
| 270 |
+
if abs_error < tolerance:
|
| 271 |
+
return
|
| 272 |
+
err_msg = (
|
| 273 |
+
f'{sim_time=} differed from {forcing_sim_time=} by {abs_error=} which is '
|
| 274 |
+
f'> {tolerance=}'
|
| 275 |
+
)
|
| 276 |
+
_FORCING_ERRORS.append(err_msg)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# TODO(langmore) Use a more universal mechanism (not just in forcings.py) to
|
| 280 |
+
# handle errors, if we like this, then make public.
|
| 281 |
+
def _check_errors( # pylint: disable=dangerous-default-value
|
| 282 |
+
max_to_print: int = 4,
|
| 283 |
+
err_list: list[str] = _FORCING_ERRORS,
|
| 284 |
+
) -> None:
|
| 285 |
+
"""Check err_list and raise ForcingDataError if nonempty."""
|
| 286 |
+
n_err = len(err_list)
|
| 287 |
+
if n_err:
|
| 288 |
+
raise ForcingDataError(
|
| 289 |
+
f'ForcingDataError found: {n_err} exceptions: '
|
| 290 |
+
f'The first {min(n_err, max_to_print)} are: '
|
| 291 |
+
f'{", ".join(err_list[:max_to_print])}'
|
| 292 |
+
)
|
model/legacy/gin_utils.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Helper functions for processing and parsing gin configurations."""
|
| 15 |
+
|
| 16 |
+
import contextlib
|
| 17 |
+
import logging
|
| 18 |
+
import threading
|
| 19 |
+
import gin
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
_GIN_LOCK = threading.RLock()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _remove_unknown_reference(gin_config_str: str) -> str:
|
| 26 |
+
"""Removes unknown references form `gin_config_str`."""
|
| 27 |
+
# this happens when we have gin MACROS reference not imported objects.
|
| 28 |
+
return '\n'.join([
|
| 29 |
+
line for line in gin_config_str.splitlines()
|
| 30 |
+
if 'gin.config._UnknownConfigurable' not in line
|
| 31 |
+
])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def parse_gin_config(
|
| 35 |
+
physics_config_str: str,
|
| 36 |
+
model_config_str: str,
|
| 37 |
+
override_physics_configs_from_data: bool,
|
| 38 |
+
gin_bindings: list[str],
|
| 39 |
+
):
|
| 40 |
+
"""Parses physics_config_str, model_config_str and gin_bindings in order.
|
| 41 |
+
|
| 42 |
+
We use skip unknown parameters in model_config_str to avoid errors associated
|
| 43 |
+
with irrelevant training parameters that refer to configurables only imported
|
| 44 |
+
for training.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
physics_config_str: gin configuration string for physics_specifications
|
| 48 |
+
object that stores relevant physics constants.
|
| 49 |
+
model_config_str: gin configuration string of the model.
|
| 50 |
+
override_physics_configs_from_data: whether to reparse `physics_config_str`
|
| 51 |
+
after processing `model_config_str`.
|
| 52 |
+
gin_bindings: additional gin configuration strings that will be parsed last.
|
| 53 |
+
"""
|
| 54 |
+
gin.parse_config(physics_config_str)
|
| 55 |
+
gin.parse_config(model_config_str, skip_unknown=True)
|
| 56 |
+
if override_physics_configs_from_data:
|
| 57 |
+
gin.parse_config(physics_config_str)
|
| 58 |
+
gin.parse_config(gin_bindings)
|
| 59 |
+
logging.info('Evaluating model with the following config:\n %s',
|
| 60 |
+
gin.config_str())
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@contextlib.contextmanager
|
| 64 |
+
def specific_config(
|
| 65 |
+
gin_config: str,
|
| 66 |
+
clear_current: bool = True,
|
| 67 |
+
skip_unknown: bool = True,
|
| 68 |
+
):
|
| 69 |
+
"""Context manager for evaluation of functions with `gin_config`."""
|
| 70 |
+
with _GIN_LOCK:
|
| 71 |
+
# avoid splitting long lines into multiples that may contain unknown refs.
|
| 72 |
+
current_config = gin.config_str(max_line_length=len(gin.config_str()))
|
| 73 |
+
current_config = _remove_unknown_reference(current_config)
|
| 74 |
+
if clear_current:
|
| 75 |
+
gin.clear_config()
|
| 76 |
+
try:
|
| 77 |
+
gin.parse_config(gin_config, skip_unknown=skip_unknown)
|
| 78 |
+
yield
|
| 79 |
+
finally:
|
| 80 |
+
gin.clear_config()
|
| 81 |
+
gin.parse_config(current_config)
|
model/legacy/initializers.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Implementation of custom initializers for NN parameters."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Optional, Sequence
|
| 17 |
+
|
| 18 |
+
import gin
|
| 19 |
+
import haiku as hk
|
| 20 |
+
import jax
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Registering default initializers.
|
| 25 |
+
Constant = gin.external_configurable(hk.initializers.Constant)
|
| 26 |
+
VarianceScaling = gin.external_configurable(hk.initializers.VarianceScaling)
|
| 27 |
+
Orthogonal = gin.external_configurable(hk.initializers.Orthogonal)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _compute_fans(
|
| 31 |
+
shape: Sequence[int],
|
| 32 |
+
fan_in_axes: Optional[Sequence[int]] = None,
|
| 33 |
+
) -> tuple[int, int]:
|
| 34 |
+
"""Computes the number of input and output units for a weight shape."""
|
| 35 |
+
# adapted from dm-haiku/_src/initializers.py
|
| 36 |
+
if len(shape) < 1:
|
| 37 |
+
fan_in = fan_out = 1
|
| 38 |
+
elif len(shape) == 1:
|
| 39 |
+
fan_in = fan_out = shape[0]
|
| 40 |
+
elif len(shape) == 2:
|
| 41 |
+
fan_in, fan_out = shape
|
| 42 |
+
else:
|
| 43 |
+
if fan_in_axes is not None:
|
| 44 |
+
# Compute fan-in using user-specified fan-in axes.
|
| 45 |
+
fan_in = np.prod([shape[i] for i in fan_in_axes])
|
| 46 |
+
fan_out = np.prod([s for i, s in enumerate(shape)
|
| 47 |
+
if i not in fan_in_axes])
|
| 48 |
+
else:
|
| 49 |
+
# If no axes specified, assume convolution kernels (2D, 3D, or more.)
|
| 50 |
+
# kernel_shape: (..., input_depth, depth)
|
| 51 |
+
receptive_field_size = np.prod(shape[:-2])
|
| 52 |
+
fan_in = shape[-2] * receptive_field_size
|
| 53 |
+
fan_out = shape[-1] * receptive_field_size
|
| 54 |
+
return fan_in, fan_out # pyrefly: ignore[bad-return]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@gin.register
|
| 58 |
+
class ReducingVarianceScaling(hk.initializers.Initializer): # pyrefly: ignore[invalid-inheritance]
|
| 59 |
+
"""Initializer that result in variance that reduces as width increases.
|
| 60 |
+
|
| 61 |
+
Initializes weights that result in features with expected variance of
|
| 62 |
+
`scale / n`, where `n` corresponds to the width of the layer. This initializer
|
| 63 |
+
can be used in the output layer to achieve µ parameterization [1].
|
| 64 |
+
|
| 65 |
+
References:
|
| 66 |
+
[1]: https://arxiv.org/abs/2203.03466
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
scale=1.0,
|
| 72 |
+
mode='fan_in',
|
| 73 |
+
distribution='truncated_normal',
|
| 74 |
+
fan_in_axes=None,
|
| 75 |
+
):
|
| 76 |
+
"""Constructs `ReducingVarianceScaling` initializer.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
scale: Variance scale for a width == 1 initialization.
|
| 80 |
+
mode: One of ``fan_in``, ``fan_out``, ``fan_avg``
|
| 81 |
+
distribution: Random distribution to use. One of ``truncated_normal``,
|
| 82 |
+
``normal`` or ``uniform``.
|
| 83 |
+
fan_in_axes: Optional sequence of int specifying which axes of the shape
|
| 84 |
+
are part of the fan-in. If none provided, then the weight is assumed
|
| 85 |
+
to be like a convolution kernel, where all leading dimensions are part
|
| 86 |
+
of the fan-in, and only the trailing dimension is part of the fan-out.
|
| 87 |
+
Useful if instantiating multi-headed attention weights.
|
| 88 |
+
"""
|
| 89 |
+
if scale < 0.0:
|
| 90 |
+
raise ValueError('`scale` must be a positive float.')
|
| 91 |
+
if mode not in {'fan_in', 'fan_out', 'fan_avg'}:
|
| 92 |
+
raise ValueError('Invalid `mode` argument:', mode)
|
| 93 |
+
distribution = distribution.lower()
|
| 94 |
+
if distribution not in {'normal', 'truncated_normal', 'uniform'}:
|
| 95 |
+
raise ValueError('Invalid `distribution` argument:', distribution)
|
| 96 |
+
self.scale = scale
|
| 97 |
+
self.mode = mode
|
| 98 |
+
self.distribution = distribution
|
| 99 |
+
self.fan_in_axes = fan_in_axes
|
| 100 |
+
|
| 101 |
+
def __call__(self, shape: Sequence[int], dtype: Any) -> jax.Array:
|
| 102 |
+
scale = self.scale
|
| 103 |
+
fan_in, fan_out = _compute_fans(shape, self.fan_in_axes)
|
| 104 |
+
if self.mode == 'fan_in':
|
| 105 |
+
scale /= max(1.0, fan_in) ** 2
|
| 106 |
+
elif self.mode == 'fan_out':
|
| 107 |
+
scale /= max(1.0, fan_out) ** 2
|
| 108 |
+
else:
|
| 109 |
+
scale /= max(1.0, (fan_in + fan_out) / 2.0) ** 2
|
| 110 |
+
|
| 111 |
+
if self.distribution == 'truncated_normal':
|
| 112 |
+
stddev = np.sqrt(scale)
|
| 113 |
+
# Adjust stddev for truncation.
|
| 114 |
+
# Constant from scipy.stats.truncnorm.std(a=-2, b=2, loc=0., scale=1.)
|
| 115 |
+
distribution_stddev = np.asarray(.87962566103423978, dtype=dtype)
|
| 116 |
+
stddev = stddev / distribution_stddev
|
| 117 |
+
return hk.initializers.TruncatedNormal(stddev=stddev)(shape, dtype)
|
| 118 |
+
elif self.distribution == 'normal':
|
| 119 |
+
stddev = np.sqrt(scale)
|
| 120 |
+
return hk.initializers.RandomNormal(stddev=stddev)(shape, dtype)
|
| 121 |
+
else:
|
| 122 |
+
limit = np.sqrt(3.0 * scale)
|
| 123 |
+
uniform_init = hk.initializers.RandomUniform(minval=-limit, maxval=limit)
|
| 124 |
+
return uniform_init(shape, dtype)
|
model/legacy/integrators.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines configurable time integrators to be used by models."""
|
| 15 |
+
|
| 16 |
+
from typing import Callable
|
| 17 |
+
from dinosaur import time_integration
|
| 18 |
+
from dinosaur import typing
|
| 19 |
+
import gin
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
TimeIntegrator = Callable[
|
| 23 |
+
[time_integration.ImplicitExplicitODE, typing.Numeric], typing.TimeStepFn]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
backward_forward_euler = gin.external_configurable(
|
| 27 |
+
time_integration.backward_forward_euler)
|
| 28 |
+
crank_nicolson_rk2 = gin.external_configurable(
|
| 29 |
+
time_integration.crank_nicolson_rk2)
|
| 30 |
+
crank_nicolson_rk3 = gin.external_configurable(
|
| 31 |
+
time_integration.crank_nicolson_rk3)
|
| 32 |
+
crank_nicolson_rk4 = gin.external_configurable(
|
| 33 |
+
time_integration.crank_nicolson_rk4)
|
| 34 |
+
imex_rk_sil3 = gin.external_configurable(time_integration.imex_rk_sil3)
|
| 35 |
+
semi_implicit_leapfrog = gin.external_configurable(
|
| 36 |
+
time_integration.semi_implicit_leapfrog)
|
model/legacy/layers.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Basic neural network layers for whirl/gcm codebase."""
|
| 15 |
+
|
| 16 |
+
from typing import Callable, Optional, Sequence, Tuple
|
| 17 |
+
from dinosaur import typing
|
| 18 |
+
import gin
|
| 19 |
+
import haiku as hk
|
| 20 |
+
import jax
|
| 21 |
+
import jax.numpy as jnp
|
| 22 |
+
|
| 23 |
+
from model.legacy import initializers # pylint: disable=unused-import
|
| 24 |
+
|
| 25 |
+
Array = typing.Array
|
| 26 |
+
GatingFactory = typing.GatingFactory
|
| 27 |
+
TowerFactory = typing.TowerFactory
|
| 28 |
+
MLP = gin.external_configurable(hk.nets.MLP)
|
| 29 |
+
|
| 30 |
+
# nonlinearities
|
| 31 |
+
relu = gin.external_configurable(jax.nn.relu)
|
| 32 |
+
gelu = gin.external_configurable(jax.nn.gelu)
|
| 33 |
+
silu = gin.external_configurable(jax.nn.silu)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@gin.register(denylist=['output_size'])
|
| 37 |
+
class MlpUniform(hk.nets.MLP):
|
| 38 |
+
"""MLP network with same output size in each hidden layer."""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
output_size: int,
|
| 43 |
+
num_hidden_units: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 44 |
+
num_hidden_layers: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 45 |
+
w_init: Optional[hk.initializers.Initializer] = None,
|
| 46 |
+
b_init: Optional[hk.initializers.Initializer] = None,
|
| 47 |
+
with_bias: bool = True,
|
| 48 |
+
activation: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.relu,
|
| 49 |
+
activate_final: bool = False,
|
| 50 |
+
w_init_final: Optional[hk.initializers.Initializer] = None,
|
| 51 |
+
b_init_final: Optional[hk.initializers.Initializer] = None,
|
| 52 |
+
name: Optional[str] = None,
|
| 53 |
+
):
|
| 54 |
+
hidden_output_sizes = [num_hidden_units] * num_hidden_layers
|
| 55 |
+
super().__init__(
|
| 56 |
+
hidden_output_sizes,
|
| 57 |
+
w_init=w_init,
|
| 58 |
+
b_init=b_init,
|
| 59 |
+
with_bias=with_bias,
|
| 60 |
+
activation=activation,
|
| 61 |
+
activate_final=True, # last layer added explicitly.
|
| 62 |
+
name=name,
|
| 63 |
+
)
|
| 64 |
+
self.linear_final = hk.Linear(
|
| 65 |
+
output_size=output_size,
|
| 66 |
+
w_init=w_init_final,
|
| 67 |
+
b_init=b_init_final,
|
| 68 |
+
with_bias=with_bias,
|
| 69 |
+
name='linear_%d' % num_hidden_layers,
|
| 70 |
+
)
|
| 71 |
+
self.activate_linear_final = activate_final
|
| 72 |
+
|
| 73 |
+
def __call__(
|
| 74 |
+
self,
|
| 75 |
+
inputs: jax.Array,
|
| 76 |
+
dropout_rate: Optional[float] = None,
|
| 77 |
+
rng: Optional[jax.Array] = None,
|
| 78 |
+
) -> jax.Array:
|
| 79 |
+
out = super().__call__(inputs, dropout_rate=dropout_rate, rng=rng)
|
| 80 |
+
out = self.linear_final(out)
|
| 81 |
+
if self.activate_linear_final:
|
| 82 |
+
out = self.activation(out)
|
| 83 |
+
return out
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@gin.register(denylist=['output_size'])
|
| 87 |
+
class ConvLonLat(hk.Module):
|
| 88 |
+
"""Two dimensional convolutional neural network."""
|
| 89 |
+
|
| 90 |
+
def __init__(
|
| 91 |
+
self,
|
| 92 |
+
output_size: int,
|
| 93 |
+
kernel_shape: Tuple[int, int] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 94 |
+
with_bias: bool = True,
|
| 95 |
+
name: Optional[str] = None,
|
| 96 |
+
):
|
| 97 |
+
super().__init__(name=name)
|
| 98 |
+
self._padding = []
|
| 99 |
+
for kernel_size in kernel_shape:
|
| 100 |
+
pad_left = kernel_size // 2
|
| 101 |
+
self._padding.append((pad_left, kernel_size - pad_left - 1))
|
| 102 |
+
# Use padding='VALID': since padding is done in call, haiku trims
|
| 103 |
+
self._conv_module = hk.Conv2D(
|
| 104 |
+
output_channels=output_size,
|
| 105 |
+
kernel_shape=kernel_shape,
|
| 106 |
+
with_bias=with_bias,
|
| 107 |
+
padding='VALID',
|
| 108 |
+
data_format='NCHW',
|
| 109 |
+
)
|
| 110 |
+
# NCHW = batch (ignored), channels (sigma), height (lon), width (lat)
|
| 111 |
+
|
| 112 |
+
def __call__(self, inputs: Array) -> Array:
|
| 113 |
+
"""Applies convolution to inputs."""
|
| 114 |
+
# Padding order is z, x, y
|
| 115 |
+
# Periodic padding in longitude (x)
|
| 116 |
+
# Zero padding in latitude (y)
|
| 117 |
+
inputs = jnp.pad(inputs, [(0, 0), self._padding[0], (0, 0)], mode='wrap')
|
| 118 |
+
# TODO(pnorgaard): consider rotated mirror padding to simulate wrapping
|
| 119 |
+
# around the N/S poles.
|
| 120 |
+
inputs = jnp.pad(
|
| 121 |
+
inputs, [(0, 0), (0, 0), self._padding[1]], mode='constant'
|
| 122 |
+
)
|
| 123 |
+
return self._conv_module(inputs)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@gin.register
|
| 127 |
+
class ConvLevel(hk.Conv1D):
|
| 128 |
+
"""1D convolution in the vertical (convolution on atmospheric columns)."""
|
| 129 |
+
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
output_channels: int,
|
| 133 |
+
kernel_shape: int,
|
| 134 |
+
dilation_rate: int = 1,
|
| 135 |
+
padding: str = 'SAME',
|
| 136 |
+
with_bias: bool = True,
|
| 137 |
+
w_init: Optional[hk.initializers.Initializer] = None,
|
| 138 |
+
b_init: Optional[hk.initializers.Initializer] = None,
|
| 139 |
+
data_format: str = 'NCW',
|
| 140 |
+
name: Optional[str] = None,
|
| 141 |
+
):
|
| 142 |
+
super().__init__(
|
| 143 |
+
output_channels=output_channels,
|
| 144 |
+
kernel_shape=kernel_shape,
|
| 145 |
+
rate=dilation_rate,
|
| 146 |
+
padding=padding,
|
| 147 |
+
with_bias=with_bias,
|
| 148 |
+
w_init=w_init,
|
| 149 |
+
b_init=b_init,
|
| 150 |
+
data_format=data_format,
|
| 151 |
+
name=name,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@gin.register
|
| 156 |
+
class VerticalConvNet(hk.Module):
|
| 157 |
+
"""1D CNN in the vertical (convolution on atmospheric columns)."""
|
| 158 |
+
|
| 159 |
+
def __init__(
|
| 160 |
+
self,
|
| 161 |
+
output_size: int,
|
| 162 |
+
channels: Sequence[int],
|
| 163 |
+
kernel_shapes: int | Sequence[int],
|
| 164 |
+
dilation_rates: int | Sequence[int],
|
| 165 |
+
padding: str = 'SAME',
|
| 166 |
+
with_bias: bool = True,
|
| 167 |
+
w_init: Optional[hk.initializers.Initializer] = None,
|
| 168 |
+
b_init: Optional[hk.initializers.Initializer] = None,
|
| 169 |
+
data_format: str = 'NCW',
|
| 170 |
+
activation: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.relu,
|
| 171 |
+
activate_final: bool = False,
|
| 172 |
+
w_init_final: Optional[hk.initializers.Initializer] = None,
|
| 173 |
+
b_init_final: Optional[hk.initializers.Initializer] = None,
|
| 174 |
+
name: Optional[str] = None,
|
| 175 |
+
):
|
| 176 |
+
super().__init__(name=name)
|
| 177 |
+
n_hidden = len(channels)
|
| 178 |
+
if isinstance(kernel_shapes, int):
|
| 179 |
+
kernel_shapes = [kernel_shapes] * (n_hidden + 1) # +1 for output layer.
|
| 180 |
+
if isinstance(dilation_rates, int):
|
| 181 |
+
dilation_rates = [dilation_rates] * (n_hidden + 1) # +1 for output layer.
|
| 182 |
+
channels = list(channels) + [output_size]
|
| 183 |
+
if len(set([len(channels), len(kernel_shapes), len(dilation_rates)])) != 1:
|
| 184 |
+
raise ValueError(
|
| 185 |
+
f'Missing kernel|dilation specs for {n_hidden + 1} '
|
| 186 |
+
f'layers, got {kernel_shapes=}, {dilation_rates=}.'
|
| 187 |
+
)
|
| 188 |
+
w_inits = [w_init] * n_hidden + [w_init_final]
|
| 189 |
+
b_inits = [b_init] * n_hidden + [b_init_final]
|
| 190 |
+
params = zip(channels, kernel_shapes, dilation_rates, w_inits, b_inits)
|
| 191 |
+
self.layers = []
|
| 192 |
+
for c, kernel, dilation, w_init_i, b_init_i in params:
|
| 193 |
+
self.layers.append(
|
| 194 |
+
ConvLevel(
|
| 195 |
+
output_channels=c,
|
| 196 |
+
kernel_shape=kernel,
|
| 197 |
+
dilation_rate=dilation,
|
| 198 |
+
padding=padding,
|
| 199 |
+
with_bias=with_bias,
|
| 200 |
+
w_init=w_init_i,
|
| 201 |
+
b_init=b_init_i,
|
| 202 |
+
data_format=data_format,
|
| 203 |
+
)
|
| 204 |
+
)
|
| 205 |
+
self.activation = activation
|
| 206 |
+
self.activate_final = activate_final
|
| 207 |
+
|
| 208 |
+
def __call__(self, inputs: Array) -> Array:
|
| 209 |
+
out = inputs
|
| 210 |
+
num_layers = len(self.layers)
|
| 211 |
+
for i, layer in enumerate(self.layers):
|
| 212 |
+
out = layer(out)
|
| 213 |
+
if i < (num_layers - 1) or self.activate_final:
|
| 214 |
+
out = self.activation(out)
|
| 215 |
+
return out
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@gin.register
|
| 219 |
+
class LevelTransformer(hk.Module):
|
| 220 |
+
"""Network that uses attention mechanism across vertical levels.
|
| 221 |
+
|
| 222 |
+
This network is a simple variation of a transformer architecture. It is
|
| 223 |
+
configurable to represent either the encoder and decoder blocks. Contrary to
|
| 224 |
+
other layers, this module accepts additional optional arguments: `latents` and
|
| 225 |
+
`positional_encoding` that enable it to represent computations with more
|
| 226 |
+
complex dependency structure. By default these arguments have value `None`, in
|
| 227 |
+
which case the network uses `inputs` and performs self-attention calculation
|
| 228 |
+
throughout. If `latents` are provided, then they are used for key and value
|
| 229 |
+
calculations for all attention blocks. If `positional_encoding` is provided,
|
| 230 |
+
then it is used to produce the first set of queries in an attention block.
|
| 231 |
+
Additionally this module supports extension with gating mechanism, generally
|
| 232 |
+
resembling GTrXL transformer from https://arxiv.org/pdf/1910.06764.pdf.
|
| 233 |
+
|
| 234 |
+
Attributes:
|
| 235 |
+
output_size: desired number of channels in the output of the module.
|
| 236 |
+
latent_size: latent representation size. Must be divisible by `num_heads`.
|
| 237 |
+
n_layers: number of transformer blocks in the network.
|
| 238 |
+
num_heads: number of attention heads in each attention layer.
|
| 239 |
+
key_size: size of key/query vectors to use for computing attention scores.
|
| 240 |
+
widening_factor: widening factor in dense layer at the end of each block.
|
| 241 |
+
activation: activation function to apply between linear transforms.
|
| 242 |
+
input_projection_net: network or layer to be used to project inputs into
|
| 243 |
+
initial latent representation. If set to `None`, then input projection is
|
| 244 |
+
skipped entirely (only possible if input size == latent_size).
|
| 245 |
+
skip_final_projection: whether to skip final projection layer. If set to
|
| 246 |
+
`True`, then requested `output_size` must be equal to `latent_size`.
|
| 247 |
+
gating_module: gating mechanism to use to combine residual connection and
|
| 248 |
+
dense updates. Defaults to residual connections.
|
| 249 |
+
name: optional name for the module.
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
def __init__(
|
| 253 |
+
self,
|
| 254 |
+
output_size: int,
|
| 255 |
+
latent_size: int,
|
| 256 |
+
n_layers: int,
|
| 257 |
+
num_heads: int,
|
| 258 |
+
key_size: int,
|
| 259 |
+
widening_factor: int = 2,
|
| 260 |
+
activation: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.gelu,
|
| 261 |
+
input_projection_net: TowerFactory = hk.Linear,
|
| 262 |
+
skip_final_projection: bool = False,
|
| 263 |
+
gating_module: GatingFactory = lambda: lambda x, y: x + y,
|
| 264 |
+
name: Optional[str] = None,
|
| 265 |
+
):
|
| 266 |
+
super().__init__(name=name)
|
| 267 |
+
value_size, reminder = divmod(latent_size, num_heads)
|
| 268 |
+
if reminder != 0:
|
| 269 |
+
raise ValueError(f'{latent_size=} is not divisible by {num_heads=}.')
|
| 270 |
+
|
| 271 |
+
self.output_size = output_size
|
| 272 |
+
self.latent_size = latent_size
|
| 273 |
+
self.n_layers = n_layers
|
| 274 |
+
self.num_heads = num_heads
|
| 275 |
+
self.key_size = key_size
|
| 276 |
+
self.value_size = value_size
|
| 277 |
+
self.wide_latent_size = widening_factor * latent_size
|
| 278 |
+
self.activation = activation
|
| 279 |
+
self.w_init = hk.initializers.VarianceScaling(2 / self.n_layers)
|
| 280 |
+
self.gating_fn = gating_module()
|
| 281 |
+
|
| 282 |
+
if input_projection_net is not None:
|
| 283 |
+
self.project_input_fn = input_projection_net(latent_size)
|
| 284 |
+
else:
|
| 285 |
+
|
| 286 |
+
def skip_with_check_fn(inputs):
|
| 287 |
+
_, d = inputs.shape
|
| 288 |
+
if d != latent_size:
|
| 289 |
+
raise ValueError(
|
| 290 |
+
f'{inputs.shape=} not compatible with {latent_size=}'
|
| 291 |
+
' Specify projection module in the transformer.'
|
| 292 |
+
)
|
| 293 |
+
return inputs
|
| 294 |
+
|
| 295 |
+
self.project_input_fn = skip_with_check_fn
|
| 296 |
+
if skip_final_projection:
|
| 297 |
+
if output_size != self.latent_size:
|
| 298 |
+
raise ValueError(
|
| 299 |
+
f'Unable to skip projection for {output_size=}, '
|
| 300 |
+
f'{self.latent_size=}.'
|
| 301 |
+
)
|
| 302 |
+
self.final_projection = lambda x: x
|
| 303 |
+
else:
|
| 304 |
+
self.final_projection = hk.Linear(output_size)
|
| 305 |
+
|
| 306 |
+
@hk.transparent
|
| 307 |
+
def layer_norm(self, x: jnp.ndarray) -> jnp.ndarray:
|
| 308 |
+
"""Applies a unique LayerNorm to x with default settings."""
|
| 309 |
+
ln = hk.LayerNorm(axis=-1, create_scale=True, create_offset=True)
|
| 310 |
+
return ln(x)
|
| 311 |
+
|
| 312 |
+
def __call__(
|
| 313 |
+
self,
|
| 314 |
+
inputs: Array,
|
| 315 |
+
latents: Optional[Array] = None,
|
| 316 |
+
positional_encoding: Optional[Array] = None,
|
| 317 |
+
) -> Array:
|
| 318 |
+
"""Applies transformer layer to inputs. See class docstring for details."""
|
| 319 |
+
inputs = jnp.transpose(inputs) # transpose to [levels, channels].
|
| 320 |
+
h = self.project_input_fn(inputs)
|
| 321 |
+
if latents is not None:
|
| 322 |
+
latents = jnp.transpose(latents)
|
| 323 |
+
if positional_encoding is not None:
|
| 324 |
+
init_query_input = jnp.transpose(positional_encoding)
|
| 325 |
+
special_query_stage = 0 # uses `positional_encoding` for first query.
|
| 326 |
+
else:
|
| 327 |
+
special_query_stage = -1 # ensures we pass `h_norm` to query in h_attn.
|
| 328 |
+
h_dense = None # not used in the first layer.
|
| 329 |
+
last_layer_id = self.n_layers - 1
|
| 330 |
+
for layer_id in range(self.n_layers - 1):
|
| 331 |
+
# connects residual updates from the previous layer; skipped first time.
|
| 332 |
+
h = self.gating_fn(h, h_dense) if h_dense is not None else h
|
| 333 |
+
# apply layer norm before the attention block, as in GTrXL.
|
| 334 |
+
h_norm = self.layer_norm(h) # pyrefly: ignore[bad-argument-type]
|
| 335 |
+
attn_block = hk.MultiHeadAttention(
|
| 336 |
+
num_heads=self.num_heads,
|
| 337 |
+
key_size=self.key_size,
|
| 338 |
+
value_size=self.value_size,
|
| 339 |
+
model_size=self.latent_size,
|
| 340 |
+
w_init=self.w_init,
|
| 341 |
+
)
|
| 342 |
+
# attend to `latents` if in decoding stage, otherwise use self-attention.
|
| 343 |
+
h_attn = attn_block(
|
| 344 |
+
query=init_query_input if layer_id == special_query_stage else h_norm, # pyrefly: ignore[unbound-name]
|
| 345 |
+
key=latents if latents is not None else h_norm,
|
| 346 |
+
value=latents if latents is not None else h_norm,
|
| 347 |
+
)
|
| 348 |
+
# connects residual updates from attention layer.
|
| 349 |
+
h = self.gating_fn(h, h_attn)
|
| 350 |
+
if layer_id != last_layer_id:
|
| 351 |
+
dense_block = hk.Sequential([
|
| 352 |
+
hk.Linear(self.wide_latent_size, w_init=self.w_init),
|
| 353 |
+
self.activation,
|
| 354 |
+
hk.Linear(self.latent_size, w_init=self.w_init),
|
| 355 |
+
])
|
| 356 |
+
h_dense = dense_block(self.layer_norm(h)) # pyrefly: ignore[bad-argument-type]
|
| 357 |
+
|
| 358 |
+
h_dense = self.final_projection(h)
|
| 359 |
+
h_dense = jnp.transpose(h_dense) # transpose back to [channels, levels].
|
| 360 |
+
return h_dense
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@gin.register(denylist=['output_size'])
|
| 364 |
+
class LevelBiLSTM(hk.Module):
|
| 365 |
+
"""Applies a bidirectional LSTM to inputs.
|
| 366 |
+
|
| 367 |
+
This network is a bi-directional LSTM. This module accepts additional
|
| 368 |
+
optional argument, window_size which determines the number of positional
|
| 369 |
+
features the LSTM will use at each step. By default this argument have
|
| 370 |
+
value `1`, in which case the network uses features from a single level at
|
| 371 |
+
each step.
|
| 372 |
+
|
| 373 |
+
Attributes:
|
| 374 |
+
output_size: desired number of channels in the output of the module.
|
| 375 |
+
hidden_size: size of the hidden state in the LSTM.
|
| 376 |
+
n_layers: number of bi-directional LSTM layers in the network.
|
| 377 |
+
final_activation: optional activation to be applied to the output.
|
| 378 |
+
window_size: number of (local) features the LSTM will use at each step.
|
| 379 |
+
name: optional name for the module.
|
| 380 |
+
"""
|
| 381 |
+
def __init__(
|
| 382 |
+
self,
|
| 383 |
+
output_size: int,
|
| 384 |
+
hidden_size: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 385 |
+
n_layers: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 386 |
+
final_activation: Optional[Callable[[jnp.ndarray], jnp.ndarray]] = None,
|
| 387 |
+
window_size: int = 1,
|
| 388 |
+
name='lstm'):
|
| 389 |
+
super().__init__(name=name)
|
| 390 |
+
self.hidden_size = hidden_size
|
| 391 |
+
self.n_layers = n_layers
|
| 392 |
+
self.final_projection = hk.Linear(output_size)
|
| 393 |
+
self.final_activation = final_activation
|
| 394 |
+
self.window_size = window_size
|
| 395 |
+
|
| 396 |
+
self.fw_lstms = []
|
| 397 |
+
self.bw_lstms = []
|
| 398 |
+
for i in range(n_layers):
|
| 399 |
+
self.fw_lstms.append(hk.LSTM(hidden_size, name=f"{name}_fw_{i}"))
|
| 400 |
+
self.bw_lstms.append(hk.LSTM(hidden_size, name=f"{name}_bw_{i}"))
|
| 401 |
+
|
| 402 |
+
def sliding_window_reshape(self, data):
|
| 403 |
+
"""Reshapes data to include local vertical features."""
|
| 404 |
+
levels_num = data.shape[0]
|
| 405 |
+
pad_start = (self.window_size - 1) // 2
|
| 406 |
+
pad_end = self.window_size - 1 - pad_start
|
| 407 |
+
padded_data = jnp.pad(data, [(pad_start, pad_end)] + [(0, 0)])
|
| 408 |
+
feature_indices = (
|
| 409 |
+
jnp.arange(self.window_size)[jnp.newaxis, :]
|
| 410 |
+
+ jnp.arange(levels_num)[:, jnp.newaxis]
|
| 411 |
+
)
|
| 412 |
+
windowed_data = padded_data[feature_indices, ...]
|
| 413 |
+
windowed_data = jnp.reshape(
|
| 414 |
+
windowed_data,
|
| 415 |
+
[
|
| 416 |
+
windowed_data.shape[0],
|
| 417 |
+
windowed_data.shape[2] * windowed_data.shape[1],
|
| 418 |
+
],
|
| 419 |
+
)
|
| 420 |
+
return windowed_data
|
| 421 |
+
|
| 422 |
+
def __call__(self, inputs):
|
| 423 |
+
inputs = jnp.transpose(inputs) # transpose to [levels, channels].
|
| 424 |
+
if self.window_size > 1:
|
| 425 |
+
inputs = self.sliding_window_reshape(inputs)
|
| 426 |
+
for i in range(self.n_layers):
|
| 427 |
+
#TODO(janniyuval): initializing from previous hidden state?
|
| 428 |
+
fw_initial_state = self.fw_lstms[i].initial_state(None)
|
| 429 |
+
bw_initial_state = self.bw_lstms[i].initial_state(None)
|
| 430 |
+
|
| 431 |
+
fw_outputs, _ = hk.dynamic_unroll(
|
| 432 |
+
self.fw_lstms[i], inputs, fw_initial_state
|
| 433 |
+
)
|
| 434 |
+
bw_outputs, _ = hk.dynamic_unroll(
|
| 435 |
+
self.bw_lstms[i], inputs, bw_initial_state, reverse=True
|
| 436 |
+
)
|
| 437 |
+
outputs = jnp.concatenate([fw_outputs, bw_outputs], axis=-1)
|
| 438 |
+
inputs = outputs
|
| 439 |
+
h_dense = self.final_projection(outputs) # pyrefly: ignore[unbound-name]
|
| 440 |
+
if self.final_activation is not None:
|
| 441 |
+
h_dense = self.final_activation(h_dense)
|
| 442 |
+
h_dense = jnp.transpose(h_dense) # transpose back to [channels, levels].
|
| 443 |
+
return h_dense
|
model/legacy/mappings.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules that transform data between pytrees."""
|
| 15 |
+
|
| 16 |
+
from typing import Callable, Optional, Sequence
|
| 17 |
+
from dinosaur import pytree_utils
|
| 18 |
+
from dinosaur import typing
|
| 19 |
+
import gin
|
| 20 |
+
import haiku as hk
|
| 21 |
+
import jax
|
| 22 |
+
from model.legacy import transforms
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
Array = typing.Array
|
| 26 |
+
Tower = Callable[[int], Callable[..., Array]]
|
| 27 |
+
MappingModule = Callable[[typing.Pytree], typing.Pytree]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@gin.register(denylist=['output_shapes'])
|
| 31 |
+
class NodalMapping(hk.Module):
|
| 32 |
+
"""Maps the pytree of nodal features to a pytree of specified structure.
|
| 33 |
+
|
| 34 |
+
This module packs the pytree into a single array of shape (n, lon, lat),
|
| 35 |
+
passes it to a NN tower, and unpacks the result into a pytree with the
|
| 36 |
+
structure of output_shapes, typically (m, lon, lat).
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
output_shapes: typing.Pytree,
|
| 42 |
+
tower_factory: Tower = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 43 |
+
name: Optional[str] = None,
|
| 44 |
+
):
|
| 45 |
+
super().__init__(name=name)
|
| 46 |
+
feature_axis = -3 # default column axis.
|
| 47 |
+
output_size = sum([x[feature_axis]
|
| 48 |
+
for x in jax.tree_util.tree_leaves(output_shapes)])
|
| 49 |
+
# tower preserves the last two spatial dimensions.
|
| 50 |
+
self.tower = tower_factory(output_size)
|
| 51 |
+
self.output_shapes = output_shapes
|
| 52 |
+
self.feature_axis = feature_axis
|
| 53 |
+
|
| 54 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 55 |
+
array = pytree_utils.pack_pytree(inputs, self.feature_axis)
|
| 56 |
+
if array.ndim != 3:
|
| 57 |
+
raise ValueError(f'Expected input array with ndim=3, got {array.shape=}')
|
| 58 |
+
outputs = self.tower(array)
|
| 59 |
+
if outputs.ndim != 3:
|
| 60 |
+
raise ValueError(f'Expected outputs with ndim=3, got {outputs.shape=}')
|
| 61 |
+
return pytree_utils.unpack_to_pytree(
|
| 62 |
+
outputs, self.output_shapes, self.feature_axis)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@gin.register(denylist=['output_shapes'])
|
| 66 |
+
class NodalVolumeMapping(hk.Module):
|
| 67 |
+
"""Maps the pytree of nodal volume features to a pytree of given structure.
|
| 68 |
+
|
| 69 |
+
This module stacks the input pytree into an array of shape
|
| 70 |
+
(channel, level, lon, lat), passes it to a NN tower. The output from the NN
|
| 71 |
+
is expected to have shape (n, level, lon, lat), and gets unpacked to a pytree
|
| 72 |
+
with the structure of output_shapes, e.g.
|
| 73 |
+
output_shapes = {
|
| 74 |
+
'var_1': jnp.asarray((level, lon, lat)),
|
| 75 |
+
'var_2': jnp.asarray((level, lon, lat)),
|
| 76 |
+
...,
|
| 77 |
+
'var_n': jnp.asarray((level, lon, lat)),
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
The leaves of the input pytree must have the same shape, e.g. (1, lon, lat) or
|
| 81 |
+
(level, lon, lat). To mix shapes, broadcast before passing to the mapping.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
def __init__(
|
| 85 |
+
self,
|
| 86 |
+
output_shapes: typing.Pytree,
|
| 87 |
+
tower_factory: Tower = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 88 |
+
name: Optional[str] = None
|
| 89 |
+
):
|
| 90 |
+
super().__init__(name=name)
|
| 91 |
+
feature_axis = 0
|
| 92 |
+
output_size = len(jax.tree_util.tree_leaves(output_shapes))
|
| 93 |
+
|
| 94 |
+
# tower preserves the last two spatial dimensions.
|
| 95 |
+
self.tower = tower_factory(output_size)
|
| 96 |
+
self.output_shapes = output_shapes
|
| 97 |
+
self.feature_axis = feature_axis
|
| 98 |
+
|
| 99 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 100 |
+
array = pytree_utils.stack_pytree(inputs, axis=self.feature_axis)
|
| 101 |
+
if array.ndim != 4:
|
| 102 |
+
raise ValueError(f'Expected input array with ndim=4, got {array.shape=}')
|
| 103 |
+
outputs = self.tower(array)
|
| 104 |
+
if outputs.ndim != 4:
|
| 105 |
+
raise ValueError(f'Expected outputs with ndim=4, got {outputs.shape=}')
|
| 106 |
+
return pytree_utils.unstack_to_pytree(
|
| 107 |
+
outputs, self.output_shapes, axis=self.feature_axis
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@gin.register
|
| 112 |
+
class NodalVolumeTransformerMapping(hk.Module):
|
| 113 |
+
"""Maps the pytree of nodal volume features to a pytree of given structure.
|
| 114 |
+
|
| 115 |
+
Similar to NodalVolumeMapping, but uses certain features as positional
|
| 116 |
+
encoding arguments to the underlying transformer networks. Inputs are
|
| 117 |
+
expected to be of shape (channel, level, lon, lat), which are split into
|
| 118 |
+
encoder inputs, decoder inputs and positional encodings, which are then passed
|
| 119 |
+
to transformer towers. The output of the NN is expected to have shape
|
| 120 |
+
(n, level*, lon, lat), and gets unpacked to a pytree with the structure of
|
| 121 |
+
output_shapes, e.g.
|
| 122 |
+
output_shapes = {
|
| 123 |
+
'out_1': jnp.asarray((level*, lon, lat)),
|
| 124 |
+
'out_2': jnp.asarray((level*, lon, lat)),
|
| 125 |
+
...,
|
| 126 |
+
'out_n': jnp.asarray((level*, lon, lat)),
|
| 127 |
+
}
|
| 128 |
+
Note: the output number of levels `level*` is equal to those defined by the
|
| 129 |
+
`decoder_inputs_selection_module`. In case it is empty, level* == level.
|
| 130 |
+
|
| 131 |
+
The leaves of the encoder/decoder pytrees must have the same shape, e.g.
|
| 132 |
+
(1, lon, lat) or (level, lon, lat) or (level*, lon, lat). To mix shapes,
|
| 133 |
+
broadcast before passing to the mapping.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
def __init__(
|
| 137 |
+
self,
|
| 138 |
+
output_shapes: typing.Pytree,
|
| 139 |
+
encoder_transformer_tower_factory: Tower = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 140 |
+
decoder_transformer_tower_factory: Tower = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 141 |
+
latent_size: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 142 |
+
encoder_inputs_selection_module=gin.REQUIRED,
|
| 143 |
+
decoder_inputs_selection_module=transforms.EmptyTransform,
|
| 144 |
+
encoder_pos_encoding_module=transforms.EmptyTransform,
|
| 145 |
+
decoder_pos_encoding_module=transforms.EmptyTransform,
|
| 146 |
+
name: Optional[str] = None
|
| 147 |
+
):
|
| 148 |
+
super().__init__(name=name)
|
| 149 |
+
feature_axis = 0
|
| 150 |
+
output_size = len(jax.tree_util.tree_leaves(output_shapes))
|
| 151 |
+
self.encoder_tower = encoder_transformer_tower_factory(latent_size)
|
| 152 |
+
self.decoder_tower = decoder_transformer_tower_factory(output_size)
|
| 153 |
+
self.output_shapes = output_shapes
|
| 154 |
+
self.feature_axis = feature_axis
|
| 155 |
+
self.get_encoder_inputs_fn = encoder_inputs_selection_module() # pyrefly: ignore[not-callable]
|
| 156 |
+
self.get_decode_inputs_fn = decoder_inputs_selection_module()
|
| 157 |
+
self.encoder_positional_encodings_fn = encoder_pos_encoding_module()
|
| 158 |
+
self.decoder_positional_encodings_fn = decoder_pos_encoding_module()
|
| 159 |
+
|
| 160 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 161 |
+
enc_inputs = self.get_encoder_inputs_fn(inputs)
|
| 162 |
+
dec_inputs = self.get_decode_inputs_fn(inputs)
|
| 163 |
+
enc_array = pytree_utils.stack_pytree(enc_inputs, axis=self.feature_axis)
|
| 164 |
+
dec_array = pytree_utils.stack_pytree(dec_inputs, axis=self.feature_axis)
|
| 165 |
+
enc_pos_encoding = pytree_utils.stack_pytree(
|
| 166 |
+
self.encoder_positional_encodings_fn(inputs), axis=self.feature_axis)
|
| 167 |
+
dec_pos_encoding = pytree_utils.stack_pytree(
|
| 168 |
+
self.decoder_positional_encodings_fn(inputs), axis=self.feature_axis)
|
| 169 |
+
input_ndims = set(
|
| 170 |
+
x.ndim
|
| 171 |
+
for x in [enc_array, dec_array, enc_pos_encoding, dec_pos_encoding]
|
| 172 |
+
if x is not None)
|
| 173 |
+
if input_ndims != {4}:
|
| 174 |
+
raise ValueError(f'Expected all inputs have ndim=4, got {input_ndims=}')
|
| 175 |
+
latents = self.encoder_tower(enc_array, None, enc_pos_encoding)
|
| 176 |
+
# if dec_array is None, use latents as `inputs` and provide no `latents`.
|
| 177 |
+
decoder_latents = None if dec_array is None else latents
|
| 178 |
+
# if dec_array is None, use `latents`, otherwise use dec_array as `inputs`.
|
| 179 |
+
dec_array = dec_array if dec_array is not None else latents
|
| 180 |
+
outputs = self.decoder_tower(dec_array, decoder_latents, dec_pos_encoding)
|
| 181 |
+
if outputs.ndim != 4:
|
| 182 |
+
raise ValueError(f'Expected outputs with ndim=4, got {outputs.shape=}')
|
| 183 |
+
return pytree_utils.unstack_to_pytree(
|
| 184 |
+
outputs, self.output_shapes, axis=self.feature_axis
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@gin.register(denylist=['output_shapes'])
|
| 189 |
+
class ParallelMapping(hk.Module):
|
| 190 |
+
"""Maps a pytree to a pytree by additively compbining multiple mappings.
|
| 191 |
+
|
| 192 |
+
Outputs of `mappings` must be compatible with each other.
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
def __init__(
|
| 196 |
+
self,
|
| 197 |
+
output_shapes: typing.Pytree,
|
| 198 |
+
mappings: Sequence[MappingModule] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 199 |
+
name: Optional[str] = None,
|
| 200 |
+
):
|
| 201 |
+
super().__init__(name=name)
|
| 202 |
+
self.mapping_fns = [m(output_shapes) for m in mappings]
|
| 203 |
+
|
| 204 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 205 |
+
results = [mapping_fn(inputs) for mapping_fn in self.mapping_fns]
|
| 206 |
+
return jax.tree_util.tree_map(lambda *args: sum(args), *results)
|
model/legacy/model_builder.py
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Defines AbstractModel API, standard implementations and helper functions."""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import collections
|
| 18 |
+
import dataclasses
|
| 19 |
+
import functools
|
| 20 |
+
from typing import Any, Callable, Dict, Optional, Sequence, Union
|
| 21 |
+
from dinosaur import coordinate_systems
|
| 22 |
+
from dinosaur import layer_coordinates
|
| 23 |
+
from dinosaur import scales
|
| 24 |
+
from dinosaur import sigma_coordinates
|
| 25 |
+
from dinosaur import spherical_harmonic
|
| 26 |
+
from dinosaur import time_integration
|
| 27 |
+
from dinosaur import typing
|
| 28 |
+
from dinosaur import xarray_utils
|
| 29 |
+
import gin
|
| 30 |
+
import haiku as hk
|
| 31 |
+
import jax.numpy as jnp
|
| 32 |
+
|
| 33 |
+
from model.legacy import correctors # pylint: disable=unused-import
|
| 34 |
+
from model.legacy import decoders # pylint: disable=unused-import
|
| 35 |
+
from model.legacy import embeddings # pylint: disable=unused-import
|
| 36 |
+
from model.legacy import encoders # pylint: disable=unused-import
|
| 37 |
+
from model.legacy import equations # pylint: disable=unused-import
|
| 38 |
+
from model.legacy import features # pylint: disable=unused-import
|
| 39 |
+
from model.legacy import filters # pylint: disable=unused-import
|
| 40 |
+
from model.legacy import forcings # pylint: disable=unused-import
|
| 41 |
+
from model.legacy import gin_utils
|
| 42 |
+
from model.legacy import layers # pylint: disable=unused-import
|
| 43 |
+
from model.legacy import mappings # pylint: disable=unused-import
|
| 44 |
+
from model.legacy import model_utils
|
| 45 |
+
from model.legacy import physics_specifications
|
| 46 |
+
from model.legacy import steps # pylint: disable=unused-import
|
| 47 |
+
from model.legacy import stochastic # pylint: disable=unused-import
|
| 48 |
+
from model.legacy import towers # pylint: disable=unused-import
|
| 49 |
+
from model.legacy import transforms # pylint: disable=unused-import
|
| 50 |
+
import numpy as np
|
| 51 |
+
import xarray
|
| 52 |
+
# Note: many unused imports are needed to load configurable components;
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
DEFAULT_REFERENCE_TEMPERATURE = 288
|
| 56 |
+
DEFAULT_REFERENCE_DATETIME_STR = '1979-01-01T00'
|
| 57 |
+
|
| 58 |
+
Array = typing.Array
|
| 59 |
+
AuxFeatures = typing.AuxFeatures
|
| 60 |
+
DataState = typing.DataState
|
| 61 |
+
PyTreeState = typing.PyTreeState
|
| 62 |
+
ModelState = typing.ModelState
|
| 63 |
+
ForcingData = typing.ForcingData
|
| 64 |
+
Forcing = typing.Forcing
|
| 65 |
+
Numeric = typing.Numeric
|
| 66 |
+
QuantityOrStr = Union[str, scales.Quantity]
|
| 67 |
+
# Specifying the full signatures of Callable would get somewhat onerous
|
| 68 |
+
# pylint: disable=g-bare-generic
|
| 69 |
+
|
| 70 |
+
# Overzealous linter is getting confused by ABC typing.
|
| 71 |
+
# pylint: disable=function-missing-types
|
| 72 |
+
# pylint: disable=missing-arg-types
|
| 73 |
+
|
| 74 |
+
# Register data to xarray conversion methods.
|
| 75 |
+
data_to_xarray = gin.external_configurable(
|
| 76 |
+
xarray_utils.data_to_xarray, 'data_to_xarray'
|
| 77 |
+
)
|
| 78 |
+
# TODO(dkochkov) Remove this legacy name when no best checkpoints rely on it.
|
| 79 |
+
primitive_eq_to_xarray = gin.external_configurable(
|
| 80 |
+
xarray_utils.data_to_xarray, 'primitive_eq_to_xarray'
|
| 81 |
+
)
|
| 82 |
+
data_to_xarray_with_renaming = gin.external_configurable(
|
| 83 |
+
xarray_utils.data_to_xarray_with_renaming, 'data_to_xarray_with_renaming'
|
| 84 |
+
)
|
| 85 |
+
dynamic_covariate_data_to_xarray = gin.external_configurable(
|
| 86 |
+
xarray_utils.dynamic_covariate_data_to_xarray,
|
| 87 |
+
'dynamic_covariate_data_to_xarray',
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Register xarray to data conversion methods.
|
| 91 |
+
xarray_to_shallow_water = gin.external_configurable(
|
| 92 |
+
xarray_utils.xarray_to_shallow_water_eq_data, 'xarray_to_shallow_water'
|
| 93 |
+
)
|
| 94 |
+
xarray_to_primitive_eq = gin.external_configurable(
|
| 95 |
+
xarray_utils.xarray_to_primitive_eq_data, 'xarray_to_primitive_eq'
|
| 96 |
+
)
|
| 97 |
+
xarray_to_primitive_eq_with_time = gin.external_configurable(
|
| 98 |
+
xarray_utils.xarray_to_primitive_equations_with_time_data,
|
| 99 |
+
'xarray_to_primitive_eq_with_time',
|
| 100 |
+
)
|
| 101 |
+
xarray_to_weatherbench_data = gin.external_configurable(
|
| 102 |
+
xarray_utils.xarray_to_weatherbench_data, 'xarray_to_weatherbench_data'
|
| 103 |
+
)
|
| 104 |
+
xarray_to_data_with_renaming = gin.external_configurable(
|
| 105 |
+
xarray_utils.xarray_to_data_with_renaming, 'xarray_to_data_with_renaming'
|
| 106 |
+
)
|
| 107 |
+
xarray_to_dynamic_covariate_data = gin.external_configurable(
|
| 108 |
+
xarray_utils.xarray_to_dynamic_covariate_data,
|
| 109 |
+
'xarray_to_dynamic_covariate_data',
|
| 110 |
+
)
|
| 111 |
+
xarray_to_state_and_dynamic_covariate_data = gin.external_configurable(
|
| 112 |
+
xarray_utils.xarray_to_state_and_dynamic_covariate_data,
|
| 113 |
+
'xarray_to_state_and_dynamic_covariate_data',
|
| 114 |
+
)
|
| 115 |
+
coordinate_system_from_dataset = gin.external_configurable(
|
| 116 |
+
xarray_utils.coordinate_system_from_dataset,
|
| 117 |
+
'coordinate_system_from_dataset',
|
| 118 |
+
allowlist=['truncation', 'spherical_harmonics_impl'],
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Register grids and coordinates for instantiation of coordinate systems.
|
| 122 |
+
Grid = gin.external_configurable(
|
| 123 |
+
spherical_harmonic.Grid, denylist=['spmd_mesh']
|
| 124 |
+
)
|
| 125 |
+
GridWithWavenumbers = gin.external_configurable(
|
| 126 |
+
spherical_harmonic.Grid.with_wavenumbers, 'GridWithWavenumbers'
|
| 127 |
+
)
|
| 128 |
+
GridT21 = gin.external_configurable(spherical_harmonic.Grid.T21, 'GridT21')
|
| 129 |
+
GridT31 = gin.external_configurable(spherical_harmonic.Grid.T31, 'GridT31')
|
| 130 |
+
GridT42 = gin.external_configurable(spherical_harmonic.Grid.T42, 'GridT42')
|
| 131 |
+
GridT85 = gin.external_configurable(spherical_harmonic.Grid.T85, 'GridT85')
|
| 132 |
+
GridT106 = gin.external_configurable(spherical_harmonic.Grid.T106, 'GridT106')
|
| 133 |
+
GridT119 = gin.external_configurable(spherical_harmonic.Grid.T119, 'GridT119')
|
| 134 |
+
GridT170 = gin.external_configurable(spherical_harmonic.Grid.T170, 'GridT170')
|
| 135 |
+
GridT213 = gin.external_configurable(spherical_harmonic.Grid.T213, 'GridT213')
|
| 136 |
+
GridTL31 = gin.external_configurable(spherical_harmonic.Grid.TL31, 'GridTL31')
|
| 137 |
+
GridTL63 = gin.external_configurable(spherical_harmonic.Grid.TL63, 'GridTL63')
|
| 138 |
+
GridTL95 = gin.external_configurable(spherical_harmonic.Grid.TL95, 'GridTL95')
|
| 139 |
+
GridTL127 = gin.external_configurable(
|
| 140 |
+
spherical_harmonic.Grid.TL127, 'GridTL127'
|
| 141 |
+
)
|
| 142 |
+
GridTL159 = gin.external_configurable(
|
| 143 |
+
spherical_harmonic.Grid.TL159, 'GridTL159'
|
| 144 |
+
)
|
| 145 |
+
GridTL179 = gin.external_configurable(
|
| 146 |
+
spherical_harmonic.Grid.TL179, 'GridTL179'
|
| 147 |
+
)
|
| 148 |
+
GridTL255 = gin.external_configurable(
|
| 149 |
+
spherical_harmonic.Grid.TL255, 'GridTL255'
|
| 150 |
+
)
|
| 151 |
+
RealSphericalHarmonics = gin.external_configurable(
|
| 152 |
+
spherical_harmonic.RealSphericalHarmonics,
|
| 153 |
+
)
|
| 154 |
+
RealSphericalHarmonicsWithZeroImag = gin.external_configurable(
|
| 155 |
+
spherical_harmonic.RealSphericalHarmonicsWithZeroImag,
|
| 156 |
+
denylist=['spmd_mesh'],
|
| 157 |
+
)
|
| 158 |
+
LayerCoordinates = gin.external_configurable(layer_coordinates.LayerCoordinates)
|
| 159 |
+
SigmaCoordinates = gin.external_configurable(sigma_coordinates.SigmaCoordinates)
|
| 160 |
+
SigmaCoordinatesEquidistant = gin.external_configurable(
|
| 161 |
+
sigma_coordinates.SigmaCoordinates.equidistant,
|
| 162 |
+
'SigmaCoordinatesEquidistant',
|
| 163 |
+
)
|
| 164 |
+
CoordinateSystem = gin.external_configurable(
|
| 165 |
+
coordinate_systems.CoordinateSystem, denylist=['spmd_mesh']
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# Register vertical interpolation methods
|
| 169 |
+
centered_vertical_advection = gin.external_configurable(
|
| 170 |
+
sigma_coordinates.centered_vertical_advection
|
| 171 |
+
)
|
| 172 |
+
upwind_vertical_advection = gin.external_configurable(
|
| 173 |
+
sigma_coordinates.upwind_vertical_advection
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@dataclasses.dataclass(frozen=True)
|
| 178 |
+
class ModelSpecs(collections.abc.Mapping):
|
| 179 |
+
"""Specification of model configuration.
|
| 180 |
+
|
| 181 |
+
Attributes:
|
| 182 |
+
coords: horizontal and vertical grid data.
|
| 183 |
+
dt: nondimensionalized model time step.
|
| 184 |
+
physics_specs: physical constants and definition of custom units.
|
| 185 |
+
aux_features: additional static data.
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
coords: coordinate_systems.CoordinateSystem
|
| 189 |
+
dt: float
|
| 190 |
+
physics_specs: Any
|
| 191 |
+
aux_features: typing.AuxFeatures
|
| 192 |
+
|
| 193 |
+
def __len__(self):
|
| 194 |
+
return len(dataclasses.fields(self))
|
| 195 |
+
|
| 196 |
+
def __iter__(self):
|
| 197 |
+
return iter(f.name for f in dataclasses.fields(self))
|
| 198 |
+
|
| 199 |
+
def __getitem__(self, key):
|
| 200 |
+
return getattr(self, key)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@gin.configurable(
|
| 204 |
+
allowlist=(
|
| 205 |
+
'model_time_step',
|
| 206 |
+
'custom_coords',
|
| 207 |
+
'reference_temperature',
|
| 208 |
+
'reference_datetime_str',
|
| 209 |
+
)
|
| 210 |
+
)
|
| 211 |
+
def get_model_specs(
|
| 212 |
+
data_coords: coordinate_systems.CoordinateSystem,
|
| 213 |
+
physics_specs: Any,
|
| 214 |
+
aux_features: typing.AuxFeatures,
|
| 215 |
+
model_time_step: Optional[Union[float, QuantityOrStr]] = None,
|
| 216 |
+
custom_coords: Optional[coordinate_systems.CoordinateSystem] = None,
|
| 217 |
+
reference_temperature: Optional[float | Sequence] = None,
|
| 218 |
+
reference_datetime_str: Optional[str] = None,
|
| 219 |
+
) -> ModelSpecs:
|
| 220 |
+
"""Returns specifications for a WhirlModel configuration.
|
| 221 |
+
|
| 222 |
+
Provides gin hooks, and in some cases defaults, for model specification
|
| 223 |
+
formerly encoded in aux_features.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
data_coords: coordinate system in which states are represented in the data.
|
| 227 |
+
physics_specs: physical constants and definition of custom units.
|
| 228 |
+
aux_features: auxiliary features that come with the dataset.
|
| 229 |
+
model_time_step: duration of the outer time-step in our model, i.e., the
|
| 230 |
+
time by which the state is advanced in a single model.advance call.
|
| 231 |
+
custom_coords: optional coordinate system to be used by the model instead of
|
| 232 |
+
data_coords.
|
| 233 |
+
reference_temperature: reference temperature to use for sigma coordinates.
|
| 234 |
+
Must be None if already defined in aux_features. Default value of 288
|
| 235 |
+
used if None and also not in aux_features.
|
| 236 |
+
reference_datetime_str: reference datetime for which nondimensionalized time
|
| 237 |
+
is set to 0. Must be None if already defined in aux_features. Default
|
| 238 |
+
value of '1979-01-01T00' used if None and also not in aux_features.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
Configured specification of coordinate system, time-step, physical constants
|
| 242 |
+
and units, and aux_features and for our hybrid ML/physics model.
|
| 243 |
+
"""
|
| 244 |
+
if model_time_step is None:
|
| 245 |
+
raise ValueError('must provide model_time_step or outer_time_step')
|
| 246 |
+
|
| 247 |
+
if custom_coords is None:
|
| 248 |
+
coords = data_coords
|
| 249 |
+
else:
|
| 250 |
+
coords = dataclasses.replace(custom_coords, spmd_mesh=data_coords.spmd_mesh)
|
| 251 |
+
|
| 252 |
+
if aux_features.get(xarray_utils.REF_TEMP_KEY) is None:
|
| 253 |
+
if reference_temperature is None:
|
| 254 |
+
ones = np.ones(coords.vertical.layers, np.float32)
|
| 255 |
+
ref_temps = DEFAULT_REFERENCE_TEMPERATURE * ones
|
| 256 |
+
aux_features[xarray_utils.REF_TEMP_KEY] = ref_temps
|
| 257 |
+
else:
|
| 258 |
+
ones = np.ones(coords.vertical.layers, np.float32)
|
| 259 |
+
ref_temps = np.asarray(reference_temperature)
|
| 260 |
+
if ref_temps.ndim == 1 and ref_temps.shape[0] != coords.vertical.layers:
|
| 261 |
+
raise ValueError(
|
| 262 |
+
'`ref_temps` must be a scalar or a sequence with '
|
| 263 |
+
f'{coords.vertical.layers=} elements, got {ref_temps.shape=}'
|
| 264 |
+
)
|
| 265 |
+
ref_temps = ref_temps * ones
|
| 266 |
+
aux_features[xarray_utils.REF_TEMP_KEY] = ref_temps
|
| 267 |
+
else: # cannot set ref temp if already specified in aux_data
|
| 268 |
+
if reference_temperature is not None:
|
| 269 |
+
raise ValueError(
|
| 270 |
+
'reference temperature already specified in aux_features'
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if aux_features.get(xarray_utils.REFERENCE_DATETIME_KEY) is None:
|
| 274 |
+
if reference_datetime_str is None:
|
| 275 |
+
reference_datetime = np.datetime64(DEFAULT_REFERENCE_DATETIME_STR)
|
| 276 |
+
aux_features[xarray_utils.REFERENCE_DATETIME_KEY] = reference_datetime
|
| 277 |
+
else:
|
| 278 |
+
reference_datetime = np.datetime64(reference_datetime_str)
|
| 279 |
+
aux_features[xarray_utils.REFERENCE_DATETIME_KEY] = reference_datetime
|
| 280 |
+
else: # cannot set ref datetime if already specified in aux_data
|
| 281 |
+
if reference_datetime_str is not None:
|
| 282 |
+
raise ValueError('reference datetime already specified in aux_data')
|
| 283 |
+
|
| 284 |
+
if isinstance(model_time_step, (str, scales.Quantity)): # pyrefly: ignore[invalid-argument]
|
| 285 |
+
dt = physics_specs.nondimensionalize(scales.Quantity(model_time_step))
|
| 286 |
+
else:
|
| 287 |
+
dt = model_time_step
|
| 288 |
+
|
| 289 |
+
return ModelSpecs(
|
| 290 |
+
coords=coords,
|
| 291 |
+
dt=dt,
|
| 292 |
+
physics_specs=physics_specs,
|
| 293 |
+
aux_features=aux_features,
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def _identity(x):
|
| 298 |
+
return x
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class DynamicalSystem(hk.Module):
|
| 302 |
+
"""Abstract class for modeling dynamical systems."""
|
| 303 |
+
|
| 304 |
+
def __init__(
|
| 305 |
+
self,
|
| 306 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 307 |
+
dt: float,
|
| 308 |
+
physics_specs: Any,
|
| 309 |
+
aux_features: Dict[str, Any],
|
| 310 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 311 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 312 |
+
name: Optional[str] = None,
|
| 313 |
+
):
|
| 314 |
+
super().__init__(name=name)
|
| 315 |
+
self.coords = coords
|
| 316 |
+
self.dt = dt
|
| 317 |
+
self.physics_specs = physics_specs
|
| 318 |
+
self.aux_features = aux_features
|
| 319 |
+
self.input_coords = input_coords
|
| 320 |
+
self.output_coords = output_coords
|
| 321 |
+
|
| 322 |
+
def encode(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 323 |
+
"""Encodes input trajectory `x` with `forcing` to the model state."""
|
| 324 |
+
raise NotImplementedError('Model subclass did not define encode')
|
| 325 |
+
|
| 326 |
+
def decode(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 327 |
+
"""Decodes a model state `x` with `forcing` to a data representation."""
|
| 328 |
+
raise NotImplementedError('Model subclass did not define decode')
|
| 329 |
+
|
| 330 |
+
def advance(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 331 |
+
"""Returns a model state `x` with `forcing` advanced by `self.dt`."""
|
| 332 |
+
raise NotImplementedError('Model subclass did not define advance')
|
| 333 |
+
|
| 334 |
+
def forcing_fn(self, forcing_data: ForcingData, sim_time: Numeric) -> Forcing:
|
| 335 |
+
"""Returns forcing at sim_time, possibly using `forcing_data`."""
|
| 336 |
+
raise NotImplementedError('Model subclass did not define forcing_fn')
|
| 337 |
+
|
| 338 |
+
def trajectory(
|
| 339 |
+
self,
|
| 340 |
+
x,
|
| 341 |
+
outer_steps: int,
|
| 342 |
+
inner_steps: int = 1,
|
| 343 |
+
*,
|
| 344 |
+
forcing_data: ForcingData,
|
| 345 |
+
start_with_input: bool = False,
|
| 346 |
+
post_process_fn: Callable = _identity,
|
| 347 |
+
):
|
| 348 |
+
"""Returns a final model state and trajectory."""
|
| 349 |
+
|
| 350 |
+
def step_fn(x: PyTreeState) -> PyTreeState:
|
| 351 |
+
# if x does not have `sim_time`, expect forcing_fn to handle sim_time=None
|
| 352 |
+
if isinstance(x, typing.ModelState):
|
| 353 |
+
sim_time = getattr(x.state, 'sim_time', None)
|
| 354 |
+
else:
|
| 355 |
+
sim_time = getattr(x, 'sim_time', None)
|
| 356 |
+
forcing = self.forcing_fn(forcing_data, sim_time) # pyrefly: ignore[bad-argument-type]
|
| 357 |
+
x, forcing = self.coords.with_dycore_sharding((x, forcing))
|
| 358 |
+
y = self.advance(x, forcing)
|
| 359 |
+
y = self.coords.with_dycore_sharding(y)
|
| 360 |
+
return y
|
| 361 |
+
|
| 362 |
+
return trajectory_from_step(
|
| 363 |
+
step_fn,
|
| 364 |
+
outer_steps,
|
| 365 |
+
inner_steps,
|
| 366 |
+
start_with_input=start_with_input,
|
| 367 |
+
post_process_fn=post_process_fn,
|
| 368 |
+
)(x)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
@gin.configurable
|
| 372 |
+
class ModularStepModel(DynamicalSystem):
|
| 373 |
+
"""Dynamical model based on independent encoder/decoder/step components."""
|
| 374 |
+
|
| 375 |
+
def __init__(
|
| 376 |
+
self,
|
| 377 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 378 |
+
dt: float,
|
| 379 |
+
physics_specs: Any,
|
| 380 |
+
aux_features: Dict[str, Any],
|
| 381 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 382 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 383 |
+
advance_module=gin.REQUIRED,
|
| 384 |
+
encoder_module=gin.REQUIRED,
|
| 385 |
+
decoder_module=gin.REQUIRED,
|
| 386 |
+
forcing_module=forcings.NoForcing,
|
| 387 |
+
name: Optional[str] = None,
|
| 388 |
+
):
|
| 389 |
+
super().__init__(
|
| 390 |
+
coords,
|
| 391 |
+
dt,
|
| 392 |
+
physics_specs,
|
| 393 |
+
aux_features,
|
| 394 |
+
input_coords,
|
| 395 |
+
output_coords,
|
| 396 |
+
name=name,
|
| 397 |
+
)
|
| 398 |
+
self.advance_fn = advance_module(coords, dt, physics_specs, aux_features) # pyrefly: ignore[not-callable]
|
| 399 |
+
self.encoder_fn = encoder_module( # pyrefly: ignore[not-callable]
|
| 400 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 401 |
+
)
|
| 402 |
+
self.decoder_fn = decoder_module( # pyrefly: ignore[not-callable]
|
| 403 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 404 |
+
)
|
| 405 |
+
self.forcing_fn = forcing_module(coords, dt, physics_specs, aux_features)
|
| 406 |
+
|
| 407 |
+
def encode(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 408 |
+
return self.encoder_fn(x, forcing)
|
| 409 |
+
|
| 410 |
+
def decode(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 411 |
+
return self.decoder_fn(x, forcing)
|
| 412 |
+
|
| 413 |
+
def advance(self, x: PyTreeState, forcing: Forcing) -> PyTreeState:
|
| 414 |
+
return self.advance_fn(x, forcing)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
@gin.configurable
|
| 418 |
+
class StochasticModularStepModel(DynamicalSystem):
|
| 419 |
+
"""Dynamical model with modular components and stochasticity.
|
| 420 |
+
|
| 421 |
+
This instance of DynamicalSystem works with ModelState
|
| 422 |
+
representation of the model state. The `advance_module` initializes a
|
| 423 |
+
RandomnessModule. This must be compatible with ModelState.
|
| 424 |
+
Since randomness initialization might depend on the timestep at which it is
|
| 425 |
+
evolved, RandomnessModule module is initialized with `num_substeps`.
|
| 426 |
+
"""
|
| 427 |
+
|
| 428 |
+
def __init__(
|
| 429 |
+
self,
|
| 430 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 431 |
+
dt: float,
|
| 432 |
+
physics_specs: Any,
|
| 433 |
+
aux_features: Dict[str, Any],
|
| 434 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 435 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 436 |
+
advance_module=gin.REQUIRED,
|
| 437 |
+
encoder_module=gin.REQUIRED,
|
| 438 |
+
decoder_module=gin.REQUIRED,
|
| 439 |
+
forcing_module=forcings.NoForcing,
|
| 440 |
+
name: Optional[str] = None,
|
| 441 |
+
):
|
| 442 |
+
super().__init__(
|
| 443 |
+
coords,
|
| 444 |
+
dt,
|
| 445 |
+
physics_specs,
|
| 446 |
+
aux_features,
|
| 447 |
+
input_coords,
|
| 448 |
+
output_coords,
|
| 449 |
+
name=name,
|
| 450 |
+
)
|
| 451 |
+
self.advance_fn = advance_module(coords, dt, physics_specs, aux_features) # pyrefly: ignore[not-callable]
|
| 452 |
+
self.encoder_fn = encoder_module( # pyrefly: ignore[not-callable]
|
| 453 |
+
coords, dt, physics_specs, aux_features, input_coords
|
| 454 |
+
)
|
| 455 |
+
self.decoder_fn = decoder_module( # pyrefly: ignore[not-callable]
|
| 456 |
+
coords, dt, physics_specs, aux_features, output_coords
|
| 457 |
+
)
|
| 458 |
+
self.forcing_fn = forcing_module(coords, dt, physics_specs, aux_features)
|
| 459 |
+
|
| 460 |
+
def encode(
|
| 461 |
+
self,
|
| 462 |
+
x: DataState,
|
| 463 |
+
forcing: Forcing,
|
| 464 |
+
) -> ModelState:
|
| 465 |
+
"""Encodes model state and creates a new perturbation."""
|
| 466 |
+
model_state = self.encoder_fn(x, forcing=forcing)
|
| 467 |
+
# encoder_fn returns `ModelState` that contains prognostic state
|
| 468 |
+
# and initial values for memory, diagnostics and randomness.
|
| 469 |
+
return self.advance_fn.finalize_state(model_state, forcing)
|
| 470 |
+
|
| 471 |
+
def decode(self, x: ModelState, forcing: Forcing) -> typing.Pytree:
|
| 472 |
+
"""Returns model state with perturbation component removed."""
|
| 473 |
+
# TODO(langmore) Consider propagating decoding fields so decoder noise at
|
| 474 |
+
# different lead times is correlated.
|
| 475 |
+
return self.decoder_fn(x, forcing=forcing)
|
| 476 |
+
|
| 477 |
+
def advance(
|
| 478 |
+
self,
|
| 479 |
+
x: ModelState,
|
| 480 |
+
forcing: Forcing,
|
| 481 |
+
) -> ModelState:
|
| 482 |
+
"""Advances model state."""
|
| 483 |
+
return self.advance_fn(x, forcing)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
@gin.configurable(
|
| 487 |
+
allowlist=(
|
| 488 |
+
'checkpoint_step',
|
| 489 |
+
'checkpoint_multistep',
|
| 490 |
+
'checkpoint_post_process',
|
| 491 |
+
)
|
| 492 |
+
)
|
| 493 |
+
def trajectory_from_step(
|
| 494 |
+
step_fn: Callable,
|
| 495 |
+
outer_steps: int,
|
| 496 |
+
inner_steps: int,
|
| 497 |
+
*,
|
| 498 |
+
start_with_input: bool,
|
| 499 |
+
post_process_fn: Callable,
|
| 500 |
+
checkpoint_step: bool = True,
|
| 501 |
+
checkpoint_multistep: bool = False,
|
| 502 |
+
checkpoint_post_process: bool = True,
|
| 503 |
+
) -> Callable:
|
| 504 |
+
"""Returns a function that accumulates repeated applications of `step_fn`.
|
| 505 |
+
|
| 506 |
+
Compute a trajectory by repeatedly calling `step_fn()`
|
| 507 |
+
`outer_steps * inner_steps` times.
|
| 508 |
+
|
| 509 |
+
Args:
|
| 510 |
+
step_fn: function that takes a state and returns state after one time step.
|
| 511 |
+
outer_steps: number of steps to save in the generated trajectory.
|
| 512 |
+
inner_steps: number of repeated calls to step_fn() between saved steps.
|
| 513 |
+
start_with_input: if True, output the trajectory at steps [0, ..., steps-1]
|
| 514 |
+
instead of steps [1, ..., steps].
|
| 515 |
+
post_process_fn: function to apply to trajectory outputs.
|
| 516 |
+
checkpoint_step: whether to use `jax.checkpoint` on `step_fn`.
|
| 517 |
+
checkpoint_multistep: weather to use `jax.checkpoint` on `step_fn` repeated
|
| 518 |
+
steps between outputting observations used in the loss. Multi-step
|
| 519 |
+
checkpointing is off by default; turn it on to trade off ~25% increased
|
| 520 |
+
computed for ~25% less memory usage.
|
| 521 |
+
checkpoint_post_process: whether to use `jax.checkpoint` on
|
| 522 |
+
`post_process_fn`. `checkpoint_post_process` is a no-op if multi-step
|
| 523 |
+
checkpointing is enabled.
|
| 524 |
+
|
| 525 |
+
Returns:
|
| 526 |
+
A function that takes an initial state and returns a tuple consisting of:
|
| 527 |
+
(1) the final frame of the trajectory.
|
| 528 |
+
(2) trajectory of length `outer_steps` representing time evolution.
|
| 529 |
+
"""
|
| 530 |
+
if checkpoint_step:
|
| 531 |
+
step_fn = hk.remat(step_fn)
|
| 532 |
+
|
| 533 |
+
if checkpoint_post_process:
|
| 534 |
+
post_process_fn = hk.remat(post_process_fn)
|
| 535 |
+
|
| 536 |
+
if checkpoint_multistep:
|
| 537 |
+
|
| 538 |
+
def outer_scan_fn(f, init, xs, length=None):
|
| 539 |
+
return hk.scan(hk.remat(f), init, xs, length=length)
|
| 540 |
+
|
| 541 |
+
else:
|
| 542 |
+
outer_scan_fn = hk.scan
|
| 543 |
+
|
| 544 |
+
return time_integration.trajectory_from_step(
|
| 545 |
+
step_fn,
|
| 546 |
+
outer_steps,
|
| 547 |
+
inner_steps,
|
| 548 |
+
start_with_input=start_with_input,
|
| 549 |
+
post_process_fn=post_process_fn,
|
| 550 |
+
inner_scan_fn=hk.scan,
|
| 551 |
+
outer_scan_fn=outer_scan_fn,
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
@gin.configurable(allowlist=('model_cls', 'to_xarray_fn', 'from_xarray_fn'))
|
| 556 |
+
class WhirlModel:
|
| 557 |
+
"""Class that holds a Haiku model class and xarray conversion methods."""
|
| 558 |
+
|
| 559 |
+
def __init__(
|
| 560 |
+
self,
|
| 561 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 562 |
+
dt: float,
|
| 563 |
+
physics_specs: Any,
|
| 564 |
+
aux_features: Optional[AuxFeatures] = None,
|
| 565 |
+
input_coords: Optional[coordinate_systems.CoordinateSystem] = None,
|
| 566 |
+
output_coords: Optional[coordinate_systems.CoordinateSystem] = None,
|
| 567 |
+
model_cls: Callable[[], DynamicalSystem] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 568 |
+
to_xarray_fn: Optional[Callable[..., xarray.Dataset]] = None,
|
| 569 |
+
from_xarray_fn: Optional[Callable[..., DataState]] = None,
|
| 570 |
+
):
|
| 571 |
+
"""Constructs pre-defined model functions and holds conversion functions.
|
| 572 |
+
|
| 573 |
+
Args:
|
| 574 |
+
coords: horizontal and vertical descritization.
|
| 575 |
+
dt: time step of the model.
|
| 576 |
+
physics_specs: object describing the scales and physical constants.
|
| 577 |
+
aux_features: dictionary holding static features that the model may use.
|
| 578 |
+
input_coords: horizontal and vertical descritization of the input data. if
|
| 579 |
+
`None`, uses `coords`. Default `None.
|
| 580 |
+
output_coords: horizontal and vertical descritization for the output data.
|
| 581 |
+
if `None`, uses `coords`. Default `None.
|
| 582 |
+
model_cls: model Haiku class that implements encode/advance/decode fns.
|
| 583 |
+
to_xarray_fn: function that converts decoded data slices to xarray.
|
| 584 |
+
from_xarray_fn: function that extracts data slices from xarray.
|
| 585 |
+
"""
|
| 586 |
+
if aux_features is None:
|
| 587 |
+
aux_features = {}
|
| 588 |
+
if input_coords is None:
|
| 589 |
+
input_coords = coords
|
| 590 |
+
if output_coords is None:
|
| 591 |
+
output_coords = coords
|
| 592 |
+
self._coords = coords
|
| 593 |
+
self._data_coords = input_coords # by data coords we refer to model inputs.
|
| 594 |
+
specs = ModelSpecs(coords, dt, physics_specs, aux_features)
|
| 595 |
+
model_cls = functools.partial(
|
| 596 |
+
model_cls,
|
| 597 |
+
coords=coords,
|
| 598 |
+
dt=dt,
|
| 599 |
+
physics_specs=physics_specs,
|
| 600 |
+
aux_features=aux_features,
|
| 601 |
+
input_coords=input_coords,
|
| 602 |
+
output_coords=output_coords,
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
def forcing_fwd(forcing_data, sim_time):
|
| 606 |
+
return model_cls().forcing_fn(forcing_data, sim_time) # pytype: disable=wrong-keyword-args
|
| 607 |
+
|
| 608 |
+
forcing_fn = hk.transform(forcing_fwd).apply
|
| 609 |
+
encode_fwd = lambda x, forcing: model_cls().encode(x, forcing) # pytype: disable=wrong-keyword-args
|
| 610 |
+
encode_fn = hk.transform(encode_fwd).apply
|
| 611 |
+
decode_fwd = lambda x, forcing: model_cls().decode(x, forcing) # pytype: disable=wrong-keyword-args
|
| 612 |
+
decode_fn = hk.transform(decode_fwd).apply
|
| 613 |
+
advance_fwd = lambda x, forcing: model_cls().advance(x, forcing) # pytype: disable=wrong-keyword-args
|
| 614 |
+
advance_fn = hk.transform(advance_fwd).apply
|
| 615 |
+
if to_xarray_fn is not None:
|
| 616 |
+
to_xarray_fn = functools.partial(to_xarray_fn, coords=output_coords)
|
| 617 |
+
self.forcing_fn = forcing_fn
|
| 618 |
+
self.encode_fn = encode_fn
|
| 619 |
+
self.decode_fn = decode_fn
|
| 620 |
+
self.advance_fn = advance_fn
|
| 621 |
+
self.specs = specs
|
| 622 |
+
self.model_cls = model_cls
|
| 623 |
+
self.to_xarray_fn = to_xarray_fn
|
| 624 |
+
self.from_xarray_fn = from_xarray_fn
|
| 625 |
+
|
| 626 |
+
@property
|
| 627 |
+
def coords(self) -> coordinate_systems.CoordinateSystem:
|
| 628 |
+
return self._coords
|
| 629 |
+
|
| 630 |
+
@property
|
| 631 |
+
def data_coords(self) -> coordinate_systems.CoordinateSystem:
|
| 632 |
+
return self._data_coords
|
| 633 |
+
|
| 634 |
+
def init_params(
|
| 635 |
+
self,
|
| 636 |
+
rng: Array,
|
| 637 |
+
input_trajectory: typing.DataState,
|
| 638 |
+
forcing_data: ForcingData,
|
| 639 |
+
) -> typing.Params:
|
| 640 |
+
"""Returns model parameters by initializing encode/advance/decode fn."""
|
| 641 |
+
|
| 642 |
+
def fwd(x):
|
| 643 |
+
model = self.model_cls() # pytype: disable=wrong-keyword-args
|
| 644 |
+
decode = model_utils.with_forcing(
|
| 645 |
+
model.decode, model.forcing_fn, forcing_data
|
| 646 |
+
)
|
| 647 |
+
advance = model_utils.with_forcing(
|
| 648 |
+
model.advance, model.forcing_fn, forcing_data
|
| 649 |
+
)
|
| 650 |
+
encode = model_utils.with_forcing(
|
| 651 |
+
model.encode, model.forcing_fn, forcing_data
|
| 652 |
+
)
|
| 653 |
+
return decode(advance(encode(x)))
|
| 654 |
+
|
| 655 |
+
hk_model = hk.transform(fwd)
|
| 656 |
+
return hk_model.init(rng, input_trajectory)
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def get_whirl_model(
|
| 660 |
+
data_ds: xarray.Dataset,
|
| 661 |
+
model_config_str: str,
|
| 662 |
+
additional_gin_bindings: Optional[list[str]] = None,
|
| 663 |
+
) -> WhirlModel:
|
| 664 |
+
"""Returns a configured WhirlModel."""
|
| 665 |
+
if additional_gin_bindings is None:
|
| 666 |
+
additional_gin_bindings = []
|
| 667 |
+
|
| 668 |
+
try:
|
| 669 |
+
data_aux_features = xarray_utils.aux_features_from_xarray(data_ds)
|
| 670 |
+
except KeyError:
|
| 671 |
+
data_aux_features = {}
|
| 672 |
+
|
| 673 |
+
if 'physics_config_str' in data_ds.attrs:
|
| 674 |
+
physics_config_str = data_ds.attrs['physics_config_str']
|
| 675 |
+
else:
|
| 676 |
+
physics_config_str = '' # empty string is equivalent to skipping.
|
| 677 |
+
|
| 678 |
+
gin.enter_interactive_mode()
|
| 679 |
+
gin.clear_config()
|
| 680 |
+
gin_utils.parse_gin_config(
|
| 681 |
+
physics_config_str,
|
| 682 |
+
model_config_str,
|
| 683 |
+
override_physics_configs_from_data=True,
|
| 684 |
+
gin_bindings=additional_gin_bindings,
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
data_coords = coordinate_system_from_dataset(data_ds)
|
| 688 |
+
physics_specs = physics_specifications.get_physics_specs()
|
| 689 |
+
model_specs = get_model_specs(data_coords, physics_specs, data_aux_features)
|
| 690 |
+
return WhirlModel(
|
| 691 |
+
coords=model_specs.coords,
|
| 692 |
+
dt=model_specs.dt,
|
| 693 |
+
physics_specs=model_specs.physics_specs,
|
| 694 |
+
aux_features=model_specs.aux_features,
|
| 695 |
+
input_coords=data_coords,
|
| 696 |
+
output_coords=data_coords,
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
_ECMWF_CUTOFFS = {
|
| 701 |
+
# On Palmer 2009 (http://shortn/_56HCcQwmSS) page 4, the cutoffs for
|
| 702 |
+
# perturbations are given. Here we translate them to sigma levels.
|
| 703 |
+
# low_cutoffs: (100hPa, 50hPa)
|
| 704 |
+
'low_cutoffs': (0.05, 0.1), # Will not be accurate over topography.
|
| 705 |
+
# high_cutoffs: (1300m, 300m)
|
| 706 |
+
'high_cutoffs': (0.86, 0.965),
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def _piecewise_squasher(
|
| 711 |
+
sigma: Array,
|
| 712 |
+
low_cutoffs: Sequence[float],
|
| 713 |
+
high_cutoffs: Sequence[float],
|
| 714 |
+
) -> Array:
|
| 715 |
+
"""Piecewise linear values used to "squash" values by sigma level.
|
| 716 |
+
|
| 717 |
+
See function χ definition at: http://screen/5V3jzU7ZFA4vVJP
|
| 718 |
+
|
| 719 |
+
Args:
|
| 720 |
+
sigma: 1-D array of values for sigma levels. Should be in [0, 1].
|
| 721 |
+
low_cutoffs: σ=low_cutoffs[0] is when χ starts linearly increasing from 0.
|
| 722 |
+
σ=low_cutoffs[1] is when χ levels out at 1
|
| 723 |
+
high_cutoffs: σ=high_cutoffs[0] is when χ starts linearly decreasing from 1.
|
| 724 |
+
σ=high_cutoffs[1] is when χ reaches 0.
|
| 725 |
+
|
| 726 |
+
Returns:
|
| 727 |
+
Values χ of shape `sigma.shape + (1, 1)` that should be multiplied by
|
| 728 |
+
arrays of shape (n_levels, K, L) to "squash" high/low σ values.
|
| 729 |
+
"""
|
| 730 |
+
if sigma.ndim != 1:
|
| 731 |
+
raise ValueError(f'{sigma.shape=} but should have been a 1-D array')
|
| 732 |
+
if len(low_cutoffs) != 2:
|
| 733 |
+
raise ValueError(f'{len(low_cutoffs)=} but should have been 2.')
|
| 734 |
+
if len(high_cutoffs) != 2:
|
| 735 |
+
raise ValueError(f'{len(high_cutoffs)=} but should have been 2.')
|
| 736 |
+
|
| 737 |
+
low_func = (sigma - low_cutoffs[0]) / (low_cutoffs[1] - low_cutoffs[0])
|
| 738 |
+
high_func = (high_cutoffs[1] - sigma) / (high_cutoffs[1] - high_cutoffs[0])
|
| 739 |
+
|
| 740 |
+
# lower_bound is a function equal to the squasher between
|
| 741 |
+
# low_cutoffs[0] and high_cutoffs[1].
|
| 742 |
+
# It becomes negative outside that range.
|
| 743 |
+
lower_bound = jnp.minimum(1.0, jnp.minimum(low_func, high_func))
|
| 744 |
+
return jnp.maximum(0.0, lower_bound)[:, jnp.newaxis, jnp.newaxis]
|
model/legacy/model_utils.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Helper methods for constructing augmented trajectory functions."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
import functools
|
| 18 |
+
from typing import Any, Callable, Sequence, Tuple
|
| 19 |
+
from dinosaur import coordinate_systems
|
| 20 |
+
from dinosaur import pytree_utils
|
| 21 |
+
from dinosaur import typing
|
| 22 |
+
import haiku as hk
|
| 23 |
+
import jax
|
| 24 |
+
import jax.numpy as jnp
|
| 25 |
+
|
| 26 |
+
Array = typing.Array
|
| 27 |
+
DynamicalSystem = Any # to prevent circular dependency on model_builder
|
| 28 |
+
Pytree = typing.Pytree
|
| 29 |
+
|
| 30 |
+
tree_map = jax.tree_util.tree_map
|
| 31 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 32 |
+
|
| 33 |
+
# Linter confused by wrapped functions
|
| 34 |
+
# pylint: disable=g-bare-generic
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def with_preprocessing(
|
| 38 |
+
fn: Callable[..., Pytree],
|
| 39 |
+
preprocess_fn: Callable,
|
| 40 |
+
) -> Callable[..., Pytree]:
|
| 41 |
+
"""Generates a function that computes `fn` on `preprocess_fn(x)`."""
|
| 42 |
+
@functools.wraps(fn)
|
| 43 |
+
def apply_fn(x, *args, **kwargs):
|
| 44 |
+
return fn(preprocess_fn(x), *args, **kwargs)
|
| 45 |
+
|
| 46 |
+
return apply_fn
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def with_post_processing(
|
| 50 |
+
fn: Callable[..., Pytree],
|
| 51 |
+
post_process_fn: Callable,
|
| 52 |
+
) -> Callable[..., Pytree]:
|
| 53 |
+
"""Generates a function that applies `post_process_fn` to outputs of `fn`."""
|
| 54 |
+
@functools.wraps(fn)
|
| 55 |
+
def apply_fn(*args, **kwargs):
|
| 56 |
+
return post_process_fn(*fn(*args, **kwargs))
|
| 57 |
+
|
| 58 |
+
return apply_fn
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def with_forcing(
|
| 62 |
+
fn: Callable[..., Pytree],
|
| 63 |
+
forcing_fn: typing.ForcingFn,
|
| 64 |
+
forcing_data: typing.ForcingData,
|
| 65 |
+
) -> Callable[..., Pytree]:
|
| 66 |
+
"""Converts fn(x, forcing) to fn(x)."""
|
| 67 |
+
# evaluates forcing=forcing_fn(forcing_data, x.sim_time)
|
| 68 |
+
# when x does not have sim_time, forcing_fn will get sim_time=None
|
| 69 |
+
@functools.wraps(fn)
|
| 70 |
+
def wrapped(x, forcing_fn=forcing_fn):
|
| 71 |
+
# handle dataclass or dict for state data
|
| 72 |
+
if dataclasses.is_dataclass(x):
|
| 73 |
+
if isinstance(x, typing.ModelState):
|
| 74 |
+
sim_time = dataclasses.asdict(x.state).get('sim_time', None)
|
| 75 |
+
else:
|
| 76 |
+
sim_time = dataclasses.asdict(x).get('sim_time', None)
|
| 77 |
+
else:
|
| 78 |
+
sim_time = x.get('sim_time', None)
|
| 79 |
+
# handle sim_time of ndim 0 or 1
|
| 80 |
+
if sim_time is not None:
|
| 81 |
+
sim_time = jax.numpy.asarray(sim_time)
|
| 82 |
+
if sim_time.ndim:
|
| 83 |
+
forcing_fn = jax.vmap(forcing_fn, in_axes=(None, 0))
|
| 84 |
+
forcing = forcing_fn(forcing_data, sim_time) # pyrefly: ignore[bad-argument-type]
|
| 85 |
+
return fn(x, forcing=forcing)
|
| 86 |
+
return wrapped
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def with_split_input(
|
| 90 |
+
fn: Callable[..., Pytree],
|
| 91 |
+
split_index: int,
|
| 92 |
+
time_axis: int = 0,
|
| 93 |
+
) -> Callable[..., Pytree]:
|
| 94 |
+
"""Decorates `fn` to be evaluated on first `split_index` time slices.
|
| 95 |
+
|
| 96 |
+
The returned function is a generalization to pytrees of the function:
|
| 97 |
+
`fn(x[:split_index], *args, **kwargs)`
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
fn: function to be transformed.
|
| 101 |
+
split_index: number of input elements along the time axis to use.
|
| 102 |
+
time_axis: axis corresponding to time dimension in `x` to decorated `fn`.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
decorated `fn` that is evaluated on only `split_index` first time slices of
|
| 106 |
+
provided inputs.
|
| 107 |
+
"""
|
| 108 |
+
@functools.wraps(fn)
|
| 109 |
+
def apply_fn(x, *args, **kwargs):
|
| 110 |
+
init, _ = pytree_utils.split_along_axis(x, split_index, axis=time_axis)
|
| 111 |
+
return fn(init, *args, **kwargs)
|
| 112 |
+
|
| 113 |
+
return apply_fn
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def with_input_included(
|
| 117 |
+
trajectory_fn: typing.TrajectoryFn,
|
| 118 |
+
time_axis: int = 0,
|
| 119 |
+
num_last_input_frames_to_trim: int = 0,
|
| 120 |
+
) -> typing.TrajectoryFn:
|
| 121 |
+
"""Returns a `trajectory_fn` that concatenates inputs `x` to trajectory."""
|
| 122 |
+
if num_last_input_frames_to_trim > 0:
|
| 123 |
+
num_last_input_frames_to_trim = -num_last_input_frames_to_trim
|
| 124 |
+
else:
|
| 125 |
+
num_last_input_frames_to_trim = None # pyrefly: ignore[bad-assignment]
|
| 126 |
+
inputs_time_slice = slice(None, num_last_input_frames_to_trim)
|
| 127 |
+
@functools.wraps(trajectory_fn)
|
| 128 |
+
def _trajectory(x, *args, **kwargs):
|
| 129 |
+
final, unroll = trajectory_fn(x, *args, **kwargs)
|
| 130 |
+
x_concat = pytree_utils.slice_along_axis(x, time_axis, inputs_time_slice)
|
| 131 |
+
return final, pytree_utils.concat_along_axis([x_concat, unroll], time_axis)
|
| 132 |
+
|
| 133 |
+
return _trajectory
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def trajectory_with_inputs_and_forcing(
|
| 137 |
+
model: DynamicalSystem,
|
| 138 |
+
num_init_frames: int,
|
| 139 |
+
start_with_input: bool = False,
|
| 140 |
+
) -> typing.TrajectoryFn:
|
| 141 |
+
"""Returns trajectory_fn that comuptes model trajectory from target data.
|
| 142 |
+
|
| 143 |
+
Wraps the default model.trajectory_fn to operate on data representation. It
|
| 144 |
+
corresponds to slicing `num_init_frames` from the inputs, encoding and
|
| 145 |
+
unrolling the trajectory.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
model: model of a dynamical system used to obtain the trajectory.
|
| 149 |
+
num_init_frames: number of time frames used from the physics trajectory to
|
| 150 |
+
initialize the model state.
|
| 151 |
+
start_with_input: whether the firest decoded step in the output trajectory
|
| 152 |
+
should correspond to last input time or first future output.
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
Trajectory function that operates on target data trajectory by encoding
|
| 156 |
+
the `initial_frames` inputs and unrolls trajectory in a model space.
|
| 157 |
+
"""
|
| 158 |
+
def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1):
|
| 159 |
+
|
| 160 |
+
# configure the model.trajectory function with a decoder on the output.
|
| 161 |
+
trajectory_fn = functools.partial(
|
| 162 |
+
model.trajectory,
|
| 163 |
+
outer_steps=outer_steps,
|
| 164 |
+
inner_steps=inner_steps,
|
| 165 |
+
forcing_data=forcing_data,
|
| 166 |
+
start_with_input=start_with_input)
|
| 167 |
+
# add preprocessing to encode input to model state.
|
| 168 |
+
encode_fn = with_forcing(model.encode, model.forcing_fn, forcing_data)
|
| 169 |
+
trajectory_fn = with_preprocessing(trajectory_fn, encode_fn)
|
| 170 |
+
trajectory_fn = with_split_input(trajectory_fn, num_init_frames)
|
| 171 |
+
return trajectory_fn(x)
|
| 172 |
+
|
| 173 |
+
return _trajectory_fn
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def trajectory_with_inputs_and_forcing_and_stop_gradients(
|
| 177 |
+
model: DynamicalSystem,
|
| 178 |
+
num_init_frames: int,
|
| 179 |
+
start_with_input: bool = False,
|
| 180 |
+
stop_gradient_outer_steps: Sequence[int] = (),
|
| 181 |
+
) -> typing.TrajectoryFn:
|
| 182 |
+
"""Returns trajectory_fn that comuptes model trajectory from target data.
|
| 183 |
+
|
| 184 |
+
This extension of `trajectory_with_inputs_and_forcing` allows adding stop
|
| 185 |
+
gradients to the trajectory at designated steps. For example, if
|
| 186 |
+
`stop_gradient_outer_steps = [2]`, then gradients along the trajectory stop
|
| 187 |
+
at t=2. This does not mean that gradients with respect to X[2] will be zero.
|
| 188 |
+
It simply means that, for t > 2, gradients of X[t] with respect to X[2] will
|
| 189 |
+
be zero.
|
| 190 |
+
|
| 191 |
+
Wraps the default model.trajectory_fn to operate on data representation. It
|
| 192 |
+
corresponds to slicing `num_init_frames` from the inputs, encoding and
|
| 193 |
+
unrolling the trajectory.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
model: model of a dynamical system used to obtain the trajectory.
|
| 197 |
+
num_init_frames: number of time frames used from the physics trajectory to
|
| 198 |
+
initialize the model state.
|
| 199 |
+
start_with_input: whether the firest decoded step in the output trajectory
|
| 200 |
+
should correspond to last input time or first future output.
|
| 201 |
+
stop_gradient_outer_steps: Tuple (possibly empty) indicating outer steps at
|
| 202 |
+
which to place stop gradients.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
Trajectory function that operates on target data trajectory by encoding
|
| 206 |
+
the `initial_frames` inputs and unrolls trajectory in a model space.
|
| 207 |
+
Decoding is not done by this function.
|
| 208 |
+
"""
|
| 209 |
+
stop_gradient_outer_steps = list(sorted(stop_gradient_outer_steps))
|
| 210 |
+
if num_init_frames != 1:
|
| 211 |
+
raise ValueError(f'{num_init_frames=} is not supported yet.')
|
| 212 |
+
|
| 213 |
+
if stop_gradient_outer_steps and min(stop_gradient_outer_steps) <= 0:
|
| 214 |
+
raise ValueError(
|
| 215 |
+
f'{stop_gradient_outer_steps=} contained non-positive values'
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
expand_dim0 = lambda tree: tree_map(lambda x_i: x_i[jnp.newaxis], tree)
|
| 219 |
+
concat_dim0 = lambda trees: pytree_utils.concat_along_axis(trees, axis=0)
|
| 220 |
+
slice_dim0 = lambda tree, idx: pytree_utils.slice_along_axis(
|
| 221 |
+
tree, axis=0, idx=idx
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def concat_trajectories_with_stop_grads(
|
| 225 |
+
x, forcing_data, outer_steps, inner_steps=1
|
| 226 |
+
):
|
| 227 |
+
if (
|
| 228 |
+
stop_gradient_outer_steps
|
| 229 |
+
and max(stop_gradient_outer_steps) > outer_steps
|
| 230 |
+
):
|
| 231 |
+
raise ValueError(
|
| 232 |
+
f'{stop_gradient_outer_steps=} contained values > {outer_steps=}'
|
| 233 |
+
)
|
| 234 |
+
outer_steps_seq = list(stop_gradient_outer_steps)
|
| 235 |
+
if not outer_steps_seq or outer_steps_seq[-1] != outer_steps:
|
| 236 |
+
outer_steps_seq.append(outer_steps)
|
| 237 |
+
|
| 238 |
+
# The first leg needs to encode the input. So use
|
| 239 |
+
# trajectory_with_inputs_and_forcing, which does the encoding.
|
| 240 |
+
final_state, first_leg = trajectory_with_inputs_and_forcing(
|
| 241 |
+
model,
|
| 242 |
+
num_init_frames=num_init_frames,
|
| 243 |
+
start_with_input=start_with_input,
|
| 244 |
+
)(
|
| 245 |
+
x,
|
| 246 |
+
forcing_data=forcing_data,
|
| 247 |
+
outer_steps=outer_steps_seq[0],
|
| 248 |
+
inner_steps=inner_steps,
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# At this point, sections contains times [0, ..., outer_steps_seq[0]]
|
| 252 |
+
sections = [
|
| 253 |
+
first_leg,
|
| 254 |
+
]
|
| 255 |
+
|
| 256 |
+
# Subsequent legs do not need encoding, so use model.trajectory directly.
|
| 257 |
+
trajectory_fn = functools.partial(
|
| 258 |
+
model.trajectory,
|
| 259 |
+
inner_steps=inner_steps,
|
| 260 |
+
forcing_data=forcing_data,
|
| 261 |
+
start_with_input=start_with_input,
|
| 262 |
+
)
|
| 263 |
+
for i in range(1, len(outer_steps_seq)):
|
| 264 |
+
# outer_steps_seq[-1] may or may not be in stop_gradient_outer_steps.
|
| 265 |
+
# The other steps will be by construction.
|
| 266 |
+
assert set(outer_steps_seq[:-1]).issubset(stop_gradient_outer_steps)
|
| 267 |
+
stop_grad_at_start = outer_steps_seq[i - 1] in stop_gradient_outer_steps
|
| 268 |
+
|
| 269 |
+
initial_state = final_state
|
| 270 |
+
|
| 271 |
+
# this_leg contains times [outer_steps_seq[0]+1, ..., outer_steps_seq[1]]
|
| 272 |
+
final_state, this_leg = trajectory_fn(
|
| 273 |
+
jax.lax.stop_gradient(initial_state)
|
| 274 |
+
if stop_grad_at_start
|
| 275 |
+
else initial_state,
|
| 276 |
+
outer_steps=outer_steps_seq[i] - outer_steps_seq[i - 1],
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
if stop_grad_at_start and start_with_input:
|
| 280 |
+
# Replace the initial point that had a stop gradient on it.
|
| 281 |
+
this_leg = concat_dim0([
|
| 282 |
+
expand_dim0(initial_state),
|
| 283 |
+
slice_dim0(this_leg, idx=slice(1, None)),
|
| 284 |
+
])
|
| 285 |
+
sections.append(this_leg)
|
| 286 |
+
|
| 287 |
+
return final_state, concat_dim0(sections)
|
| 288 |
+
|
| 289 |
+
return concat_trajectories_with_stop_grads
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def decoded_trajectory_with_forcing(
|
| 293 |
+
model: DynamicalSystem,
|
| 294 |
+
start_with_input: bool = False,
|
| 295 |
+
) -> typing.TrajectoryFn:
|
| 296 |
+
"""Returns trajectory_fn that comuptes decoded trajectory values.
|
| 297 |
+
|
| 298 |
+
Args:
|
| 299 |
+
model: model of a dynamical system used to obtain the trajectory.
|
| 300 |
+
start_with_input: whether the firest decoded step in the output trajectory
|
| 301 |
+
should correspond to last input time or first future output.
|
| 302 |
+
|
| 303 |
+
Returns:
|
| 304 |
+
Trajectory function that additionally decodes trajectory values.
|
| 305 |
+
"""
|
| 306 |
+
def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1):
|
| 307 |
+
|
| 308 |
+
# configure the model.trajectory function with a decoder on the output.
|
| 309 |
+
trajectory_fn = functools.partial(
|
| 310 |
+
model.trajectory,
|
| 311 |
+
forcing_data=forcing_data,
|
| 312 |
+
post_process_fn=with_forcing(model.decode,
|
| 313 |
+
model.forcing_fn, forcing_data),
|
| 314 |
+
start_with_input=start_with_input)
|
| 315 |
+
return trajectory_fn(x, outer_steps, inner_steps)
|
| 316 |
+
|
| 317 |
+
return _trajectory_fn
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def decoded_trajectory_with_inputs_and_forcing(
|
| 321 |
+
model: DynamicalSystem,
|
| 322 |
+
num_init_frames: int,
|
| 323 |
+
start_with_input: bool = False,
|
| 324 |
+
) -> typing.TrajectoryFn:
|
| 325 |
+
"""Returns trajectory_fn operating on decoded input and forcing data.
|
| 326 |
+
|
| 327 |
+
The returned function uses `num_init_frames` of the physics space trajectory
|
| 328 |
+
provided as an input to model.encode_fn to initialize the model state, then
|
| 329 |
+
unrolls the trajectory of specified length that is decoded to the physics
|
| 330 |
+
space using `model.decode_fn`.
|
| 331 |
+
|
| 332 |
+
Args:
|
| 333 |
+
model: model of a dynamical system used to obtain the trajectory.
|
| 334 |
+
num_init_frames: number of time frames used from the physics trajectory to
|
| 335 |
+
initialize the model state.
|
| 336 |
+
start_with_input: whether the firest decoded step in the output trajectory
|
| 337 |
+
should correspond to last input time or first future output.
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Trajectory function that operates on physics space trajectories
|
| 341 |
+
and returns unrolls in physics space.
|
| 342 |
+
"""
|
| 343 |
+
def _trajectory_fn(x, forcing_data, outer_steps, inner_steps=1):
|
| 344 |
+
|
| 345 |
+
# configure the model.trajectory function with a decoder on the output.
|
| 346 |
+
trajectory_fn = decoded_trajectory_with_forcing(model, start_with_input)
|
| 347 |
+
trajectory_fn = functools.partial(
|
| 348 |
+
trajectory_fn,
|
| 349 |
+
forcing_data=forcing_data,
|
| 350 |
+
outer_steps=outer_steps,
|
| 351 |
+
inner_steps=inner_steps)
|
| 352 |
+
# add preprocessing to encode input to model state.
|
| 353 |
+
trajectory_fn = with_preprocessing(
|
| 354 |
+
trajectory_fn, with_forcing(model.encode,
|
| 355 |
+
model.forcing_fn, forcing_data))
|
| 356 |
+
# concatenate input trajectory to output trajectory for easier comparison.
|
| 357 |
+
trajectory_fn = with_input_included(
|
| 358 |
+
trajectory_fn, num_last_input_frames_to_trim=int(start_with_input))
|
| 359 |
+
# make trajectories operate on full examples by splitting the init.
|
| 360 |
+
trajectory_fn = with_split_input(trajectory_fn, num_init_frames)
|
| 361 |
+
return trajectory_fn(x)
|
| 362 |
+
|
| 363 |
+
return _trajectory_fn
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def process_trajectory(
|
| 367 |
+
input_trajectory: Pytree,
|
| 368 |
+
process_fn: Callable[[Pytree], Pytree],
|
| 369 |
+
) -> Pytree:
|
| 370 |
+
"""Processes trajectory by applying `process_fn` along time axis."""
|
| 371 |
+
step_fn = lambda c, x: tuple([None, hk.remat(process_fn)(x)])
|
| 372 |
+
_, out = hk.scan(step_fn, None, xs=input_trajectory)
|
| 373 |
+
return out
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def _maybe_to_nodal_with_physics_sharding(x, /, coords):
|
| 377 |
+
x = coordinate_systems.maybe_to_nodal(x, coords)
|
| 378 |
+
x = coords.with_physics_sharding(x)
|
| 379 |
+
return x
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def _maybe_to_modal_with_physics_sharding(x, /, coords):
|
| 383 |
+
x = coordinate_systems.maybe_to_modal(x, coords)
|
| 384 |
+
x = coords.with_physics_sharding(x)
|
| 385 |
+
return x
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def compute_prediction_representations(
|
| 389 |
+
predicted_trajectory: typing.Pytree,
|
| 390 |
+
forcing_data: typing.ForcingData,
|
| 391 |
+
model: DynamicalSystem,
|
| 392 |
+
) -> typing.TrajectoryRepresentations:
|
| 393 |
+
"""Computes TrajectoryRepresentations for predicted trajectory.
|
| 394 |
+
|
| 395 |
+
Args:
|
| 396 |
+
predicted_trajectory: predictions on `model.coords` coordinates.
|
| 397 |
+
forcing_data: forcing data to be used for encode/decode transformations.
|
| 398 |
+
model: model used for conversion between representations.
|
| 399 |
+
|
| 400 |
+
Returns:
|
| 401 |
+
`TrajectoryRepresentations` for predictions.
|
| 402 |
+
"""
|
| 403 |
+
decode_fn = with_forcing(model.decode, model.forcing_fn, forcing_data)
|
| 404 |
+
data_to_nodal = functools.partial(
|
| 405 |
+
_maybe_to_nodal_with_physics_sharding, coords=model.output_coords)
|
| 406 |
+
data_to_modal = functools.partial(
|
| 407 |
+
_maybe_to_modal_with_physics_sharding, coords=model.output_coords)
|
| 408 |
+
model_to_nodal = functools.partial(
|
| 409 |
+
_maybe_to_nodal_with_physics_sharding, coords=model.coords)
|
| 410 |
+
model_to_modal = functools.partial(
|
| 411 |
+
_maybe_to_modal_with_physics_sharding, coords=model.coords)
|
| 412 |
+
predicted_data_trajectory = process_trajectory(
|
| 413 |
+
predicted_trajectory, decode_fn)
|
| 414 |
+
# Note: we pass original prediction to the decoder, but use dict for outputs.
|
| 415 |
+
if isinstance(predicted_trajectory, typing.ModelState):
|
| 416 |
+
predicted_trajectory = predicted_trajectory.state
|
| 417 |
+
if dataclasses.is_dataclass(predicted_trajectory):
|
| 418 |
+
# Losses operate on dicts: convert struct to dict if needed.
|
| 419 |
+
predicted_trajectory = predicted_trajectory.asdict()
|
| 420 |
+
return typing.TrajectoryRepresentations(
|
| 421 |
+
data_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 422 |
+
predicted_data_trajectory, data_to_nodal),
|
| 423 |
+
data_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 424 |
+
predicted_data_trajectory, data_to_modal),
|
| 425 |
+
model_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 426 |
+
predicted_trajectory, model_to_nodal),
|
| 427 |
+
model_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 428 |
+
predicted_trajectory, model_to_modal),
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def compute_target_representations(
|
| 433 |
+
target_trajectory: typing.Pytree,
|
| 434 |
+
forcing_data: typing.ForcingData,
|
| 435 |
+
model: DynamicalSystem,
|
| 436 |
+
) -> typing.TrajectoryRepresentations:
|
| 437 |
+
"""Computes TrajectoryRepresentations for target trajectory.
|
| 438 |
+
|
| 439 |
+
Note: currently this method only supports models that use a single time slice
|
| 440 |
+
for initialization.
|
| 441 |
+
|
| 442 |
+
Args:
|
| 443 |
+
target_trajectory: target trajectory on `model.output_coords` coordinates.
|
| 444 |
+
forcing_data: forcing data to be used for encode/decode transformations.
|
| 445 |
+
model: model used for conversion between representations.
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
`TrajectoryRepresentations` for predictions.
|
| 449 |
+
"""
|
| 450 |
+
encode_slice_fn = with_forcing(model.encode, model.forcing_fn, forcing_data)
|
| 451 |
+
encode_fn = lambda tree: encode_slice_fn( # pylint: disable=g-long-lambda.
|
| 452 |
+
jax.tree_util.tree_map(lambda x: jnp.expand_dims(x, 0), tree))
|
| 453 |
+
data_to_nodal = functools.partial(
|
| 454 |
+
_maybe_to_nodal_with_physics_sharding, coords=model.output_coords)
|
| 455 |
+
data_to_modal = functools.partial(
|
| 456 |
+
_maybe_to_modal_with_physics_sharding, coords=model.output_coords)
|
| 457 |
+
model_to_nodal = functools.partial(
|
| 458 |
+
_maybe_to_nodal_with_physics_sharding, coords=model.coords)
|
| 459 |
+
model_to_modal = functools.partial(
|
| 460 |
+
_maybe_to_modal_with_physics_sharding, coords=model.coords)
|
| 461 |
+
target_model_trajectory = process_trajectory(
|
| 462 |
+
target_trajectory, encode_fn)
|
| 463 |
+
if isinstance(target_model_trajectory, typing.ModelState):
|
| 464 |
+
target_model_trajectory = target_model_trajectory.state
|
| 465 |
+
if dataclasses.is_dataclass(target_model_trajectory):
|
| 466 |
+
# Losses operate on dicts: convert struct to dict if needed.
|
| 467 |
+
target_model_trajectory = target_model_trajectory.asdict()
|
| 468 |
+
return typing.TrajectoryRepresentations(
|
| 469 |
+
data_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 470 |
+
target_trajectory, data_to_nodal),
|
| 471 |
+
data_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 472 |
+
target_trajectory, data_to_modal),
|
| 473 |
+
model_nodal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 474 |
+
target_model_trajectory, model_to_nodal),
|
| 475 |
+
model_modal_trajectory=process_trajectory( # pyrefly: ignore[unexpected-keyword]
|
| 476 |
+
target_model_trajectory, model_to_modal),
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def compute_prediction_and_target_representations(
|
| 481 |
+
predicted_model_trajectory: typing.Pytree,
|
| 482 |
+
target_data_trajectory: typing.Pytree,
|
| 483 |
+
forcing_data: typing.ForcingData,
|
| 484 |
+
model: DynamicalSystem,
|
| 485 |
+
) -> Tuple[typing.TrajectoryRepresentations, typing.TrajectoryRepresentations]:
|
| 486 |
+
"""Computes TrajectoryRepresentations for predicted and target trajectories.
|
| 487 |
+
|
| 488 |
+
Note: currently this method only supports models that use a single time slice
|
| 489 |
+
for initialization. While computing all terms seems wasteful, once jit-ed
|
| 490 |
+
all unused computations are optimized away. It is also tempting to compute
|
| 491 |
+
all representations at once, but as of 2023-02-28 compiler doesn't manage to
|
| 492 |
+
remove unused computation from a single primitive.
|
| 493 |
+
|
| 494 |
+
Args:
|
| 495 |
+
predicted_model_trajectory: predictions on `model.coords` coordinates.
|
| 496 |
+
target_data_trajectory: target data on `model.output_coords` coordinates.
|
| 497 |
+
forcing_data: forcing data to be used for encode/decode transformations.
|
| 498 |
+
model: model used for conversion between representations.
|
| 499 |
+
|
| 500 |
+
Returns:
|
| 501 |
+
Tuple of `TrajectoryRepresentations` for predictions and targets.
|
| 502 |
+
"""
|
| 503 |
+
prediction_representations = compute_prediction_representations(
|
| 504 |
+
predicted_model_trajectory, forcing_data, model)
|
| 505 |
+
target_representations = compute_target_representations(
|
| 506 |
+
target_data_trajectory, forcing_data, model)
|
| 507 |
+
return prediction_representations, target_representations
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
@jax.custom_jvp
|
| 511 |
+
def safe_sqrt(x: Array) -> jax.Array:
|
| 512 |
+
"""Sqrt(x) with gradient = 0 for x near 0."""
|
| 513 |
+
return jnp.sqrt(x)
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
@safe_sqrt.defjvp
|
| 517 |
+
def safe_sqrt_jvp(
|
| 518 |
+
primals: Array,
|
| 519 |
+
tangents: Array,
|
| 520 |
+
) -> tuple[jax.Array, jax.Array]:
|
| 521 |
+
(x,) = primals
|
| 522 |
+
(x_dot,) = tangents
|
| 523 |
+
primal_out = safe_sqrt(x)
|
| 524 |
+
eps = jnp.finfo(x.dtype).eps
|
| 525 |
+
safe_x = jnp.where(x > eps, x, 1.0)
|
| 526 |
+
tangent_out = jnp.where(x > eps, x_dot / (2 * safe_sqrt(safe_x)), 0)
|
| 527 |
+
return primal_out, tangent_out
|
model/legacy/optimization.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Configurable optimizers from JAX."""
|
| 15 |
+
import collections
|
| 16 |
+
import re
|
| 17 |
+
from typing import Sequence
|
| 18 |
+
|
| 19 |
+
import gin
|
| 20 |
+
import optax
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
gin.external_configurable(optax.adabelief, module='optax')
|
| 24 |
+
gin.external_configurable(optax.adam, module='optax')
|
| 25 |
+
gin.external_configurable(optax.adamw, module='optax')
|
| 26 |
+
|
| 27 |
+
gin.external_configurable(optax.constant_schedule, module='optax')
|
| 28 |
+
gin.external_configurable(optax.join_schedules, module='optax')
|
| 29 |
+
gin.external_configurable(optax.piecewise_constant_schedule, module='optax')
|
| 30 |
+
gin.external_configurable(optax.exponential_decay, module='optax')
|
| 31 |
+
gin.external_configurable(
|
| 32 |
+
optax.warmup_exponential_decay_schedule, module='optax'
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class OptimizerError(Exception):
|
| 37 |
+
"""Raised if a custom Whirl optimizer encounters an error."""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@gin.configurable
|
| 41 |
+
def optimizer(value):
|
| 42 |
+
return value
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
OptState = collections.namedtuple('OptState', ['state', 'params'])
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@gin.register
|
| 49 |
+
def piecewise_constant_schedule_specified_by_rates(
|
| 50 |
+
rates: Sequence[float],
|
| 51 |
+
boundaries: Sequence[int],
|
| 52 |
+
) -> optax.Schedule:
|
| 53 |
+
"""Schedule that is piecewise constant and specified by rates (not scales).
|
| 54 |
+
|
| 55 |
+
This is similar to optax.piecewise_constant_schedule, which requires users
|
| 56 |
+
to specify "scales" (ratio of old LR to new LR).
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
rates: Length K sequence of learning rates. `rates[i]` is used for steps
|
| 60 |
+
`0 <= step < boundaries[1]`, for i=0, and
|
| 61 |
+
`boundaries[i-1] <= step < boundaries[i]`, for 0 < i < len(boundaries)
|
| 62 |
+
`boundaries[i-1] <= step < ∞`, for i = len(boundaries)
|
| 63 |
+
boundaries: Length K-1 sequence of boundaries.
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
Schedule to pass to optax optimizers.
|
| 67 |
+
"""
|
| 68 |
+
return optax.join_schedules(
|
| 69 |
+
schedules=[optax.constant_schedule(r) for r in rates],
|
| 70 |
+
boundaries=boundaries,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@gin.register
|
| 75 |
+
def delayed_constant_schedule(
|
| 76 |
+
turn_on_step: int,
|
| 77 |
+
rate: float,
|
| 78 |
+
) -> optax.Schedule:
|
| 79 |
+
"""Schedule that is zero until `turn_on_step` then `rate` thereafter."""
|
| 80 |
+
return piecewise_constant_schedule_specified_by_rates(
|
| 81 |
+
rates=[0., rate],
|
| 82 |
+
boundaries=[turn_on_step],
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@gin.register
|
| 87 |
+
def top_level_multi_adam(
|
| 88 |
+
top_level_keys: Sequence[str] = (),
|
| 89 |
+
learning_rates: Sequence[optax.ScalarOrSchedule] = (),
|
| 90 |
+
default_learning_rate: optax.ScalarOrSchedule = 1e-4,
|
| 91 |
+
b1: float = 0.9,
|
| 92 |
+
b2: float = 0.95,
|
| 93 |
+
eps: float = 1e-6,
|
| 94 |
+
raise_if_keys_not_found: bool = True,
|
| 95 |
+
) -> optax.GradientTransformation:
|
| 96 |
+
"""Uses an Adam optimizer with different learning rates for different params.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
top_level_keys: Keys to use non-default learning rates for. A key starting
|
| 100 |
+
with 'REGEX_', such as 'REGEX_cats' will use re.search to find keys, e.g.
|
| 101 |
+
re.search('cats', key).
|
| 102 |
+
learning_rates: Learning rates to use leafs under the `top_level_keys`.
|
| 103 |
+
default_learning_rate: Learning rate to use for keys not in `learning_rates`
|
| 104 |
+
b1: Exponential decay to track the first moment of past gradients.
|
| 105 |
+
b2: Exponential decay to track the second moment of past gradients.
|
| 106 |
+
eps: A small constant applied to denominator outside of the square root to
|
| 107 |
+
avoid dividing by zero when rescaling.
|
| 108 |
+
raise_if_keys_not_found: Whether to raise if some `top_level_keys` are not
|
| 109 |
+
found in params.
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
optax optimizer with learning rate based on top level key in params dict.
|
| 113 |
+
"""
|
| 114 |
+
if len(top_level_keys) != len(learning_rates):
|
| 115 |
+
raise ValueError(
|
| 116 |
+
f'{top_level_keys=} had different length than {learning_rates=}'
|
| 117 |
+
)
|
| 118 |
+
if '' in top_level_keys:
|
| 119 |
+
raise ValueError('An empty string "" was found in `top_level_keys`.')
|
| 120 |
+
|
| 121 |
+
default_label = 'DEFAULT_LABEL'
|
| 122 |
+
if default_label in top_level_keys:
|
| 123 |
+
raise ValueError(f'{default_label=} should not be in `top_level_keys`')
|
| 124 |
+
|
| 125 |
+
def find_matching_top_level_key(param_name: str) -> str:
|
| 126 |
+
"""Searches for param_name in top_level_keys, returns the matching key."""
|
| 127 |
+
prefix = 'REGEX_'
|
| 128 |
+
matches = []
|
| 129 |
+
for k in top_level_keys:
|
| 130 |
+
if k.startswith(prefix) and re.search(k.lstrip(prefix), param_name):
|
| 131 |
+
matches.append(k)
|
| 132 |
+
elif k == param_name:
|
| 133 |
+
matches.append(k)
|
| 134 |
+
if not matches:
|
| 135 |
+
return default_label
|
| 136 |
+
elif len(matches) == 1:
|
| 137 |
+
return matches[0]
|
| 138 |
+
else:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
f'{param_name=} had more than 1 ({len(matches)}) match '
|
| 141 |
+
f'({matches}). Only one `top_level_keys` should match, or else we '
|
| 142 |
+
'cannot choose a unique learning rate for these parameters.'
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
def get_prefix_labels(params):
|
| 146 |
+
"""Makes prefix labels to help optax match params with learning rates."""
|
| 147 |
+
# E.g. if top_level_keys = ['module_A', 'REGEX_special'],
|
| 148 |
+
# and params.keys() = ['module_A', 'special_A', 'special_B', 'module_C'],
|
| 149 |
+
# labels = {
|
| 150 |
+
# 'module_A': 'module_A',
|
| 151 |
+
# 'special_A': 'REGEX_special', 'special_B': 'REGEX_special',
|
| 152 |
+
# 'module_C': 'DEFAULT_LABEL', 'module_D': 'DEFAULT_LABEL',...
|
| 153 |
+
# }
|
| 154 |
+
# E.g. labels tells optax to use the learning rate 'REGEX_special' for
|
| 155 |
+
# parameters under the prefix 'module_C'.
|
| 156 |
+
labels = {
|
| 157 |
+
param_name: find_matching_top_level_key(param_name)
|
| 158 |
+
for param_name in params
|
| 159 |
+
}
|
| 160 |
+
top_level_keys_that_matched = [
|
| 161 |
+
k for k in labels.values() if k != default_label
|
| 162 |
+
]
|
| 163 |
+
missing_keys = set(top_level_keys).difference(top_level_keys_that_matched)
|
| 164 |
+
if raise_if_keys_not_found and missing_keys:
|
| 165 |
+
raise OptimizerError(
|
| 166 |
+
f'{missing_keys=} not found in params: {sorted(params)}'
|
| 167 |
+
)
|
| 168 |
+
return labels
|
| 169 |
+
|
| 170 |
+
def make_adam(lr):
|
| 171 |
+
return optax.adam(lr, b1=b1, b2=b2, eps=eps)
|
| 172 |
+
|
| 173 |
+
return optax.multi_transform(
|
| 174 |
+
transforms={ # pyrefly: ignore[bad-argument-type]
|
| 175 |
+
k: make_adam(lr) for k, lr in zip(top_level_keys, learning_rates)
|
| 176 |
+
}
|
| 177 |
+
| {default_label: make_adam(default_learning_rate)},
|
| 178 |
+
param_labels=get_prefix_labels,
|
| 179 |
+
)
|
model/legacy/orographies.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules responsible for orography processing and initialization."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Callable, Mapping, Optional, Sequence
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import primitive_equations
|
| 19 |
+
from dinosaur import scales
|
| 20 |
+
from dinosaur import typing
|
| 21 |
+
from dinosaur import xarray_utils
|
| 22 |
+
import gin
|
| 23 |
+
import haiku as hk
|
| 24 |
+
import jax.numpy as jnp
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
units = scales.units
|
| 29 |
+
OrographyModule = Callable[..., typing.Array]
|
| 30 |
+
FilterModule = Callable[..., typing.PyTreeFilterFn]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@gin.register
|
| 34 |
+
class ClippedOrography(hk.Module):
|
| 35 |
+
"""Module that initializes orography by converting to modal and clipping."""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 40 |
+
dt: float,
|
| 41 |
+
physics_specs: Any,
|
| 42 |
+
aux_features: typing.AuxFeatures,
|
| 43 |
+
wavenumbers_to_clip: int = 1,
|
| 44 |
+
name: Optional[str] = None,
|
| 45 |
+
):
|
| 46 |
+
del dt, physics_specs
|
| 47 |
+
super().__init__(name=name)
|
| 48 |
+
self.coords = coords
|
| 49 |
+
self.wavenumbers_to_clip = wavenumbers_to_clip
|
| 50 |
+
self.nodal_orography = aux_features.get(
|
| 51 |
+
xarray_utils.OROGRAPHY, np.zeros(coords.horizontal.nodal_shape))
|
| 52 |
+
|
| 53 |
+
def __call__(self) -> typing.Array:
|
| 54 |
+
"""Returns orography converted to modal representation with clipping."""
|
| 55 |
+
return primitive_equations.truncated_modal_orography(
|
| 56 |
+
self.nodal_orography, self.coords, self.wavenumbers_to_clip)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@gin.register
|
| 60 |
+
class FilteredCustomOrography(hk.Module):
|
| 61 |
+
"""Module that initializes orography from external data."""
|
| 62 |
+
|
| 63 |
+
def __init__(
|
| 64 |
+
self,
|
| 65 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 66 |
+
dt: float,
|
| 67 |
+
physics_specs: Any,
|
| 68 |
+
aux_features: typing.AuxFeatures,
|
| 69 |
+
orography_data_path: str,
|
| 70 |
+
filter_modules: Sequence[FilterModule] = tuple(),
|
| 71 |
+
renaming_dict: Optional[Mapping[str, str]] = None,
|
| 72 |
+
name: Optional[str] = None,
|
| 73 |
+
):
|
| 74 |
+
super().__init__(name=name)
|
| 75 |
+
ds = xarray_utils.ds_from_path_or_aux(orography_data_path, aux_features)
|
| 76 |
+
if renaming_dict is not None:
|
| 77 |
+
ds = ds.rename(renaming_dict)
|
| 78 |
+
nodal_orography = xarray_utils.nodal_orography_from_ds(ds)
|
| 79 |
+
# TODO(dkochkov) Insist on having units specified in variable attrs.
|
| 80 |
+
self.nodal_orography = physics_specs.nondimensionalize(
|
| 81 |
+
nodal_orography * units.meter) # pyrefly: ignore[unsupported-operation]
|
| 82 |
+
self.coords = coords
|
| 83 |
+
# Note: here we explicitly use linear truncation to preserve full signal.
|
| 84 |
+
# Smoothing is then achieved by interpolation to self.coords and filtering.
|
| 85 |
+
self.input_coords = xarray_utils.coordinate_system_from_dataset(
|
| 86 |
+
ds, truncation=xarray_utils.LINEAR, spmd_mesh=coords.spmd_mesh,
|
| 87 |
+
spherical_harmonics_impl=self.coords.horizontal.spherical_harmonics_impl
|
| 88 |
+
)
|
| 89 |
+
self.filter_fns = [
|
| 90 |
+
module(coords, dt, physics_specs, aux_features)
|
| 91 |
+
for module in filter_modules]
|
| 92 |
+
|
| 93 |
+
def __call__(self) -> typing.Array:
|
| 94 |
+
"""Returns orography converted to modal representation with filtering."""
|
| 95 |
+
return primitive_equations.filtered_modal_orography(
|
| 96 |
+
self.nodal_orography, self.coords, self.input_coords, self.filter_fns)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@gin.register
|
| 100 |
+
class LearnedOrography(hk.Module):
|
| 101 |
+
"""Module that uses learned parameters to correct orography."""
|
| 102 |
+
|
| 103 |
+
def __init__(
|
| 104 |
+
self,
|
| 105 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 106 |
+
dt: float,
|
| 107 |
+
physics_specs: Any,
|
| 108 |
+
aux_features: typing.AuxFeatures,
|
| 109 |
+
base_orography_module: OrographyModule,
|
| 110 |
+
correction_scale: float,
|
| 111 |
+
name: Optional[str] = None,
|
| 112 |
+
):
|
| 113 |
+
super().__init__(name=name)
|
| 114 |
+
self.coords = coords
|
| 115 |
+
self.base_orography_fn = base_orography_module(
|
| 116 |
+
coords, dt, physics_specs, aux_features)
|
| 117 |
+
self.scale = correction_scale
|
| 118 |
+
# coords.horizontal.modal_shape can change based upon the required amount of
|
| 119 |
+
# padding for a particular implementation of spherical harmonics, but the
|
| 120 |
+
# mask should always have the same number of non-zero elements in the same
|
| 121 |
+
# order.
|
| 122 |
+
self.correction = hk.get_parameter(
|
| 123 |
+
'orography', (coords.horizontal.mask.sum(),), jnp.float32,
|
| 124 |
+
init=hk.initializers.Constant(0.0))
|
| 125 |
+
|
| 126 |
+
def __call__(self) -> typing.Array:
|
| 127 |
+
"""Returns orography in modal representation."""
|
| 128 |
+
mask = self.coords.horizontal.mask
|
| 129 |
+
correction_2d = jnp.zeros(self.coords.horizontal.modal_shape)
|
| 130 |
+
correction_2d = correction_2d.at[mask].set(self.correction)
|
| 131 |
+
return self.base_orography_fn() + correction_2d * self.scale # pytype: disable=not-callable # jax-ndarray
|
model/legacy/parameterizations.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Physics parameterization modules that compute non-dynamical tendencies."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Callable, Optional
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import pytree_utils
|
| 19 |
+
from dinosaur import typing
|
| 20 |
+
import gin
|
| 21 |
+
import haiku as hk
|
| 22 |
+
import jax
|
| 23 |
+
from model.legacy import features
|
| 24 |
+
from model.legacy import mappings
|
| 25 |
+
from model.legacy import transforms
|
| 26 |
+
|
| 27 |
+
FeaturesModule = features.FeaturesModule
|
| 28 |
+
Forcing = typing.Forcing
|
| 29 |
+
MappingModule = mappings.MappingModule
|
| 30 |
+
StepFilterModule = Callable[..., typing.PyTreeStepFilterFn]
|
| 31 |
+
TransformModule = typing.TransformModule
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@gin.register
|
| 35 |
+
class DirectNeuralParameterization(hk.Module):
|
| 36 |
+
"""Computes modal physics tendencies from the input state and forcing."""
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 41 |
+
dt: float,
|
| 42 |
+
physics_specs: Any,
|
| 43 |
+
aux_features: typing.AuxFeatures,
|
| 44 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 45 |
+
nodal_mapping_module: mappings.MappingModule,
|
| 46 |
+
tendency_transform_module: TransformModule,
|
| 47 |
+
prediction_mask: Optional[typing.Pytree] = None,
|
| 48 |
+
filter_module: Optional[StepFilterModule] = None,
|
| 49 |
+
name: Optional[str] = None,
|
| 50 |
+
):
|
| 51 |
+
super().__init__(name=name)
|
| 52 |
+
self.coords = coords
|
| 53 |
+
self.prediction_mask = prediction_mask
|
| 54 |
+
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
|
| 55 |
+
coords, dt, physics_specs, aux_features)
|
| 56 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 57 |
+
self.tendency_transform_fn = tendency_transform_module(
|
| 58 |
+
coords, dt, physics_specs, aux_features)
|
| 59 |
+
if filter_module is not None:
|
| 60 |
+
self.filter_fn = filter_module(
|
| 61 |
+
coords, dt, physics_specs, aux_features)
|
| 62 |
+
else:
|
| 63 |
+
self.filter_fn = lambda _, y: y # no filtering.
|
| 64 |
+
|
| 65 |
+
def __call__(
|
| 66 |
+
self,
|
| 67 |
+
inputs: typing.PyTreeState,
|
| 68 |
+
memory: Optional[typing.Pytree] = None,
|
| 69 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 70 |
+
randomness: Optional[typing.Pytree] = None,
|
| 71 |
+
forcing: Optional[Forcing] = None,
|
| 72 |
+
) -> typing.PyTreeState:
|
| 73 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 74 |
+
if memory is not None:
|
| 75 |
+
memory, _ = pytree_utils.as_dict(memory)
|
| 76 |
+
prediction_mask = self.prediction_mask
|
| 77 |
+
if prediction_mask is None:
|
| 78 |
+
prediction_mask = pytree_utils.tree_map_over_nonscalars(
|
| 79 |
+
lambda _: True, inputs, scalar_fn=lambda _: False # pyrefly: ignore[bad-argument-type]
|
| 80 |
+
)
|
| 81 |
+
prediction_shapes = jax.tree_util.tree_map(
|
| 82 |
+
lambda x, y: x if y else None,
|
| 83 |
+
coordinate_systems.get_nodal_shapes(inputs, self.coords),
|
| 84 |
+
prediction_mask,
|
| 85 |
+
)
|
| 86 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 87 |
+
nodal_inputs = self.modal_to_nodal_features_fn(
|
| 88 |
+
inputs, memory=memory, diagnostics=diagnostics, randomness=randomness,
|
| 89 |
+
forcing=forcing,
|
| 90 |
+
)
|
| 91 |
+
nodal_tendencies = net(nodal_inputs)
|
| 92 |
+
nodal_tendencies = self.tendency_transform_fn(nodal_tendencies)
|
| 93 |
+
modal_tendencies = self.coords.horizontal.to_modal(nodal_tendencies)
|
| 94 |
+
modal_tendencies = self.filter_fn(inputs, modal_tendencies)
|
| 95 |
+
return from_dict_fn(modal_tendencies)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@gin.register
|
| 99 |
+
class DivCurlNeuralParameterization(hk.Module):
|
| 100 |
+
"""Computes modal physics tendencies via `u, v` → `δ, ζ`."""
|
| 101 |
+
|
| 102 |
+
def __init__(
|
| 103 |
+
self,
|
| 104 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 105 |
+
dt: float,
|
| 106 |
+
physics_specs: Any,
|
| 107 |
+
aux_features: typing.AuxFeatures,
|
| 108 |
+
modal_to_nodal_features_module: FeaturesModule,
|
| 109 |
+
nodal_mapping_module: mappings.MappingModule,
|
| 110 |
+
tendency_transform_module: TransformModule,
|
| 111 |
+
prediction_mask: Optional[typing.Pytree] = None,
|
| 112 |
+
filter_module: Optional[StepFilterModule] = None,
|
| 113 |
+
name: Optional[str] = None,
|
| 114 |
+
):
|
| 115 |
+
super().__init__(name=name)
|
| 116 |
+
self.coords = coords
|
| 117 |
+
self.prediction_mask = prediction_mask
|
| 118 |
+
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
|
| 119 |
+
coords, dt, physics_specs, aux_features)
|
| 120 |
+
self.nodal_mapping_module = nodal_mapping_module
|
| 121 |
+
self.tendency_transform_fn = tendency_transform_module(
|
| 122 |
+
coords, dt, physics_specs, aux_features)
|
| 123 |
+
self.get_nodal_shape_fn = (
|
| 124 |
+
lambda x: coordinate_systems.get_nodal_shapes(x, coords))
|
| 125 |
+
self.to_div_curl_fn = transforms.ToModalWithDivCurlTransform(
|
| 126 |
+
coords, dt, physics_specs, aux_features)
|
| 127 |
+
if filter_module is not None:
|
| 128 |
+
self.filter_fn = filter_module(
|
| 129 |
+
coords, dt, physics_specs, aux_features)
|
| 130 |
+
else:
|
| 131 |
+
self.filter_fn = lambda _, y: y # no filtering.
|
| 132 |
+
|
| 133 |
+
def __call__(
|
| 134 |
+
self,
|
| 135 |
+
inputs: typing.PyTreeState,
|
| 136 |
+
memory: Optional[typing.Pytree] = None,
|
| 137 |
+
diagnostics: Optional[typing.Pytree] = None,
|
| 138 |
+
randomness: Optional[typing.Pytree] = None,
|
| 139 |
+
forcing: Optional[Forcing] = None,
|
| 140 |
+
) -> typing.PyTreeState:
|
| 141 |
+
inputs = self.coords.with_dycore_sharding(inputs)
|
| 142 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 143 |
+
if memory is not None:
|
| 144 |
+
memory = self.coords.with_dycore_sharding(memory)
|
| 145 |
+
memory, _ = pytree_utils.as_dict(memory)
|
| 146 |
+
prediction_mask = self.prediction_mask
|
| 147 |
+
if prediction_mask is None:
|
| 148 |
+
prediction_mask = pytree_utils.tree_map_over_nonscalars(
|
| 149 |
+
lambda _: True, inputs, scalar_fn=lambda _: False # pyrefly: ignore[bad-argument-type]
|
| 150 |
+
)
|
| 151 |
+
prediction_shapes = jax.tree_util.tree_map(
|
| 152 |
+
lambda x, y: self.get_nodal_shape_fn(x) if y else None,
|
| 153 |
+
inputs,
|
| 154 |
+
prediction_mask,
|
| 155 |
+
)
|
| 156 |
+
prediction_shapes['u'] = prediction_shapes.pop('divergence')
|
| 157 |
+
prediction_shapes['v'] = prediction_shapes.pop('vorticity')
|
| 158 |
+
net = self.nodal_mapping_module(prediction_shapes)
|
| 159 |
+
nodal_inputs = self.modal_to_nodal_features_fn(
|
| 160 |
+
inputs, memory=memory, diagnostics=diagnostics, randomness=randomness,
|
| 161 |
+
forcing=forcing,
|
| 162 |
+
)
|
| 163 |
+
nodal_inputs = self.coords.dycore_to_physics_sharding(nodal_inputs)
|
| 164 |
+
nodal_tendencies = net(nodal_inputs)
|
| 165 |
+
nodal_tendencies = self.coords.physics_to_dycore_sharding(nodal_tendencies)
|
| 166 |
+
nodal_tendencies = self.tendency_transform_fn(nodal_tendencies)
|
| 167 |
+
modal_tendencies = self.to_div_curl_fn(nodal_tendencies)
|
| 168 |
+
modal_tendencies = self.filter_fn(inputs, modal_tendencies)
|
| 169 |
+
outputs = from_dict_fn(modal_tendencies)
|
| 170 |
+
outputs = self.coords.with_dycore_sharding(outputs)
|
| 171 |
+
return outputs
|
model/legacy/perturbations.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Implementation of perturbation modules."""
|
| 15 |
+
import abc
|
| 16 |
+
from typing import Any, Callable
|
| 17 |
+
from dinosaur import coordinate_systems
|
| 18 |
+
from dinosaur import pytree_utils
|
| 19 |
+
from dinosaur import spherical_harmonic
|
| 20 |
+
from dinosaur import typing
|
| 21 |
+
import gin
|
| 22 |
+
import jax
|
| 23 |
+
import jax.numpy as jnp
|
| 24 |
+
from model.legacy import transforms
|
| 25 |
+
|
| 26 |
+
Pytree = typing.Pytree
|
| 27 |
+
PerturbationFn = Callable[..., Pytree]
|
| 28 |
+
PerturbationModule = Callable[..., PerturbationFn]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_ALLOWED_PERTURBATION_BASIS = (
|
| 32 |
+
# Converts vorticity/divergence to u/v then perturbs.
|
| 33 |
+
'uv',
|
| 34 |
+
|
| 35 |
+
# Perturbs in whatever the state is in (typically vorticity/divergence).
|
| 36 |
+
'generic',
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# We ♥ λ's
|
| 40 |
+
# pylint: disable=g-long-lambda
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@gin.register
|
| 44 |
+
class NoPerturbation:
|
| 45 |
+
"""No-op perturbation that introduces no perturbation to `inputs`."""
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 50 |
+
dt: float,
|
| 51 |
+
physics_specs: Any,
|
| 52 |
+
aux_features: Any,
|
| 53 |
+
):
|
| 54 |
+
"""Initializes a random field."""
|
| 55 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 56 |
+
|
| 57 |
+
def __call__(
|
| 58 |
+
self,
|
| 59 |
+
inputs: typing.Pytree,
|
| 60 |
+
state: typing.Pytree,
|
| 61 |
+
randomness: typing.Pytree,
|
| 62 |
+
) -> typing.Pytree:
|
| 63 |
+
"""Updates the state of a random field."""
|
| 64 |
+
del state, randomness # unused.
|
| 65 |
+
return inputs
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class BasePerturbation(abc.ABC):
|
| 69 |
+
"""Base class for perturbations."""
|
| 70 |
+
|
| 71 |
+
def __init__(
|
| 72 |
+
self,
|
| 73 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 74 |
+
dt: float,
|
| 75 |
+
physics_specs: Any,
|
| 76 |
+
aux_features: Any,
|
| 77 |
+
randomness_transform_module: transforms.TransformModule = (
|
| 78 |
+
transforms.IdentityTransform
|
| 79 |
+
),
|
| 80 |
+
return_modal: bool = True,
|
| 81 |
+
perturbation_basis: str = 'generic',
|
| 82 |
+
):
|
| 83 |
+
"""Initializes module to perturb random fields.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
coords: Model coordinate system.
|
| 87 |
+
dt: Time step.
|
| 88 |
+
physics_specs:
|
| 89 |
+
aux_features:
|
| 90 |
+
randomness_transform_module: Module that transforms jax.Array of random
|
| 91 |
+
variables before converting to nodal.
|
| 92 |
+
return_modal: Whether results should be returned in modal space.
|
| 93 |
+
perturbation_basis: Whether to perturb wind in "uv" or "generic" basis.
|
| 94 |
+
"""
|
| 95 |
+
self.coords = coords
|
| 96 |
+
self.randomness_transform_fn = randomness_transform_module(
|
| 97 |
+
coords, dt, physics_specs, aux_features
|
| 98 |
+
)
|
| 99 |
+
self.return_modal = return_modal
|
| 100 |
+
self.to_modal = coords.horizontal.to_modal
|
| 101 |
+
self.to_nodal = coords.horizontal.to_nodal
|
| 102 |
+
self.maybe_to_modal = lambda tr: coordinate_systems.maybe_to_modal(
|
| 103 |
+
tr, coords
|
| 104 |
+
)
|
| 105 |
+
self.maybe_to_nodal = lambda tr: coordinate_systems.maybe_to_nodal(
|
| 106 |
+
tr, coords
|
| 107 |
+
)
|
| 108 |
+
if perturbation_basis not in _ALLOWED_PERTURBATION_BASIS:
|
| 109 |
+
raise ValueError(
|
| 110 |
+
f'{perturbation_basis=} which was not in '
|
| 111 |
+
f'{_ALLOWED_PERTURBATION_BASIS=}'
|
| 112 |
+
)
|
| 113 |
+
self.perturbation_basis = perturbation_basis
|
| 114 |
+
|
| 115 |
+
def __call__(
|
| 116 |
+
self,
|
| 117 |
+
inputs: typing.Pytree,
|
| 118 |
+
state: typing.Pytree,
|
| 119 |
+
randomness: typing.Pytree,
|
| 120 |
+
) -> typing.Pytree:
|
| 121 |
+
"""Updates the state of a random field."""
|
| 122 |
+
del state # unused.
|
| 123 |
+
# TODO(dkochkov) allow pytree randomness in addition to broadcasting option.
|
| 124 |
+
|
| 125 |
+
if self.perturbation_basis == 'generic':
|
| 126 |
+
return self._perturb_in_generic_coordinates(inputs, randomness)
|
| 127 |
+
elif self.perturbation_basis == 'uv':
|
| 128 |
+
return self._perturb_in_uv_coordinates(inputs, randomness)
|
| 129 |
+
|
| 130 |
+
@abc.abstractmethod
|
| 131 |
+
def _perturb_core(
|
| 132 |
+
self,
|
| 133 |
+
inputs: typing.Pytree,
|
| 134 |
+
randomness: typing.Pytree,
|
| 135 |
+
) -> typing.Pytree:
|
| 136 |
+
"""Perturbs inputs using randomness."""
|
| 137 |
+
|
| 138 |
+
def _perturb_in_generic_coordinates(
|
| 139 |
+
self,
|
| 140 |
+
inputs: typing.Pytree,
|
| 141 |
+
randomness: typing.Pytree,
|
| 142 |
+
) -> typing.Pytree:
|
| 143 |
+
"""Perturb `inputs` in (vorticity, divergence) coordinate system."""
|
| 144 |
+
nodal_inputs = self.maybe_to_nodal(inputs)
|
| 145 |
+
nodal_randomness = self.maybe_to_nodal(randomness)
|
| 146 |
+
|
| 147 |
+
nodal_randomness = self.randomness_transform_fn(
|
| 148 |
+
pytree_utils.tree_map_over_nonscalars(
|
| 149 |
+
# Broadcast randomness so that self.randomness_transform_fn can use
|
| 150 |
+
# the shape of x to determine what to do.
|
| 151 |
+
lambda x: jnp.broadcast_to(nodal_randomness, x.shape),
|
| 152 |
+
nodal_inputs,
|
| 153 |
+
scalar_fn=jnp.zeros_like,
|
| 154 |
+
)
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
perturbed_nodal_inputs = self._perturb_core(nodal_inputs, nodal_randomness)
|
| 158 |
+
if self.return_modal:
|
| 159 |
+
return self.to_modal(perturbed_nodal_inputs)
|
| 160 |
+
else:
|
| 161 |
+
return perturbed_nodal_inputs
|
| 162 |
+
|
| 163 |
+
def _perturb_in_uv_coordinates(
|
| 164 |
+
self,
|
| 165 |
+
inputs: typing.Pytree,
|
| 166 |
+
randomness: typing.Pytree,
|
| 167 |
+
) -> typing.Pytree:
|
| 168 |
+
"""Perturb `inputs` in (u, v) coordinate system."""
|
| 169 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 170 |
+
|
| 171 |
+
# Remove vorticity/divergence from inputs and replace with u/v.
|
| 172 |
+
vordiv = self.maybe_to_modal({
|
| 173 |
+
'vorticity': inputs.pop('vorticity'),
|
| 174 |
+
'divergence': inputs.pop('divergence'),
|
| 175 |
+
})
|
| 176 |
+
u_nodal, v_nodal = spherical_harmonic.vor_div_to_uv_nodal(
|
| 177 |
+
grid=self.coords.horizontal,
|
| 178 |
+
vorticity=vordiv['vorticity'],
|
| 179 |
+
divergence=vordiv['divergence'],
|
| 180 |
+
clip=True,
|
| 181 |
+
)
|
| 182 |
+
nodal_inputs = self.maybe_to_nodal(inputs) # Recall we popped vor/div.
|
| 183 |
+
nodal_inputs['u'] = u_nodal
|
| 184 |
+
nodal_inputs['v'] = v_nodal
|
| 185 |
+
|
| 186 |
+
# Perturb in u/v space
|
| 187 |
+
nodal_randomness = self.maybe_to_nodal(randomness)
|
| 188 |
+
nodal_randomness = self.randomness_transform_fn(
|
| 189 |
+
pytree_utils.tree_map_over_nonscalars(
|
| 190 |
+
# Broadcast randomness so that self.randomness_transform_fn can use
|
| 191 |
+
# the shape of x to determine what to do.
|
| 192 |
+
lambda x: jnp.broadcast_to(nodal_randomness, x.shape),
|
| 193 |
+
nodal_inputs,
|
| 194 |
+
scalar_fn=jnp.zeros_like,
|
| 195 |
+
)
|
| 196 |
+
)
|
| 197 |
+
perturbed_nodal_inputs = self._perturb_core(nodal_inputs, nodal_randomness)
|
| 198 |
+
|
| 199 |
+
# Transform perturbed u/v to vor/div (modal).
|
| 200 |
+
vorticity, divergence = spherical_harmonic.uv_nodal_to_vor_div_modal(
|
| 201 |
+
grid=self.coords.horizontal,
|
| 202 |
+
u_nodal=perturbed_nodal_inputs.pop('u'),
|
| 203 |
+
v_nodal=perturbed_nodal_inputs.pop('v'),
|
| 204 |
+
clip=True,
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Insert vorticity/divergence into perturbed_inputs in the right space.
|
| 208 |
+
if self.return_modal:
|
| 209 |
+
perturbed_inputs = self.to_modal(perturbed_nodal_inputs)
|
| 210 |
+
perturbed_inputs['vorticity'] = vorticity
|
| 211 |
+
perturbed_inputs['divergence'] = divergence
|
| 212 |
+
else:
|
| 213 |
+
perturbed_inputs = perturbed_nodal_inputs.copy()
|
| 214 |
+
perturbed_inputs['vorticity'] = self.to_nodal(vorticity)
|
| 215 |
+
perturbed_inputs['divergence'] = self.to_nodal(divergence)
|
| 216 |
+
|
| 217 |
+
return from_dict_fn(perturbed_inputs)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@gin.register
|
| 221 |
+
class ProportionalPerturbation(BasePerturbation):
|
| 222 |
+
"""Perturbation that scales inputs by 1 + randomness."""
|
| 223 |
+
|
| 224 |
+
def _perturb_core(
|
| 225 |
+
self,
|
| 226 |
+
inputs: typing.Pytree,
|
| 227 |
+
randomness: typing.Pytree,
|
| 228 |
+
) -> typing.Pytree:
|
| 229 |
+
"""Multiplies inputs by (1 + randomness)."""
|
| 230 |
+
return jax.tree_util.tree_map(lambda x, y: x * (1 + y), inputs, randomness)
|
model/legacy/physics_specifications.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""PhysicsSpecifications contain physical parameters of dynamical systems.
|
| 15 |
+
|
| 16 |
+
To ensure that all model components the expected PhysicsSpecs all modules
|
| 17 |
+
(except specializing on a particular equation) must instantiate
|
| 18 |
+
PhysicsSpecs objects using `get_physics_specs`, which should be configured
|
| 19 |
+
appropriately via `gin`.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from typing import Sequence, Union
|
| 23 |
+
from dinosaur import primitive_equations
|
| 24 |
+
from dinosaur import scales
|
| 25 |
+
from dinosaur import shallow_water
|
| 26 |
+
import gin
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# TODO(jamieas): consolidate with `PrimitiveEquationSpecs`. In particular,
|
| 31 |
+
# decide whether 'specs' should have units or be nondimensionalized.
|
| 32 |
+
QuantityOrStr = Union[str, scales.Quantity]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
GET_DEFAULT_SCALE = gin.external_configurable(
|
| 36 |
+
lambda: scales.DEFAULT_SCALE, name='GET_DEFAULT_SCALE')
|
| 37 |
+
GET_ATMOSPHERIC_SCALE = gin.external_configurable(
|
| 38 |
+
lambda: scales.ATMOSPHERIC_SCALE, name='GET_ATMOSPHERIC_SCALE')
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@gin.configurable
|
| 42 |
+
def get_physics_specs(construct_fn=gin.REQUIRED):
|
| 43 |
+
"""Returns physical parameters object generated by `construct_fn`."""
|
| 44 |
+
return construct_fn() # pyrefly: ignore[not-callable]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@gin.register
|
| 48 |
+
def shallow_water_specs_constructor(
|
| 49 |
+
density_vals: Union[Sequence[float], np.ndarray],
|
| 50 |
+
density_units: QuantityOrStr = scales.WATER_DENSITY,
|
| 51 |
+
radius_si: QuantityOrStr = scales.RADIUS,
|
| 52 |
+
angular_velocity_si: QuantityOrStr = scales.ANGULAR_VELOCITY,
|
| 53 |
+
gravity_acceleration_si: QuantityOrStr = scales.GRAVITY_ACCELERATION,
|
| 54 |
+
scale: scales.Scale = scales.DEFAULT_SCALE
|
| 55 |
+
) -> shallow_water.ShallowWaterSpecs:
|
| 56 |
+
"""Constructs `ShallowWaterSpecs` using gin-configurable parameters.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
density_vals: density values for each layer of the shallow water system.
|
| 60 |
+
density_units: units in which `density_vals` are specified.
|
| 61 |
+
radius_si: radius of the domain specified with units attached.
|
| 62 |
+
angular_velocity_si: angular velocity of the domain with units attached.
|
| 63 |
+
gravity_acceleration_si: gravity on the surface with units attached.
|
| 64 |
+
scale: a scale object specifying the scales to use for nondimensionalizing.
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
ShallowWaterSpecs object containing physical parameters of the system.
|
| 68 |
+
"""
|
| 69 |
+
densities = np.asarray(density_vals) * scales.Quantity(density_units)
|
| 70 |
+
return shallow_water.ShallowWaterSpecs.from_si(
|
| 71 |
+
densities=densities, # pyrefly: ignore[unexpected-keyword]
|
| 72 |
+
radius_si=scales.Quantity(radius_si),
|
| 73 |
+
angular_velocity_si=scales.Quantity(angular_velocity_si),
|
| 74 |
+
gravity_acceleration_si=scales.Quantity(gravity_acceleration_si),
|
| 75 |
+
scale=scale)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@gin.register
|
| 79 |
+
def primitive_eq_specs_constructor(
|
| 80 |
+
radius_si: QuantityOrStr = scales.RADIUS,
|
| 81 |
+
angular_velocity_si: QuantityOrStr = scales.ANGULAR_VELOCITY,
|
| 82 |
+
gravity_acceleration_si: QuantityOrStr = scales.GRAVITY_ACCELERATION,
|
| 83 |
+
ideal_gas_constant_si: QuantityOrStr = scales.IDEAL_GAS_CONSTANT,
|
| 84 |
+
water_vapor_gas_constant_si: QuantityOrStr = scales.IDEAL_GAS_CONSTANT_H20,
|
| 85 |
+
water_vapor_isobaric_heat_capacity_si: QuantityOrStr = (
|
| 86 |
+
scales.WATER_VAPOR_CP),
|
| 87 |
+
kappa_si: QuantityOrStr = scales.KAPPA,
|
| 88 |
+
scale: scales.Scale = scales.DEFAULT_SCALE,
|
| 89 |
+
) -> primitive_equations.PrimitiveEquationsSpecs:
|
| 90 |
+
"""Constructs `PrimitiveEquationsSpecs` using gin-configurable parameters.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
radius_si: radius of the domain with units attached.
|
| 94 |
+
angular_velocity_si: angular velocity of the domain with units attached.
|
| 95 |
+
gravity_acceleration_si: gravity on the surface with units attached.
|
| 96 |
+
ideal_gas_constant_si: the gas constant with units attached.
|
| 97 |
+
water_vapor_gas_constant_si: the gas constant for vapor with units attached.
|
| 98 |
+
water_vapor_isobaric_heat_capacity_si: isobaric heat capacity of vapor with
|
| 99 |
+
units attached.
|
| 100 |
+
kappa_si: `ideal_gas_constant / Cp` where Cp is the isobaric heat capacity.
|
| 101 |
+
scale: a scale object specifying the scales to use for nondimensionalizing.
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
PrimitiveEquationsSpecs object containing physical parameters of the system.
|
| 105 |
+
"""
|
| 106 |
+
return primitive_equations.PrimitiveEquationsSpecs.from_si(
|
| 107 |
+
radius_si=scales.Quantity(radius_si),
|
| 108 |
+
angular_velocity_si=scales.Quantity(angular_velocity_si),
|
| 109 |
+
gravity_acceleration_si=scales.Quantity(gravity_acceleration_si),
|
| 110 |
+
ideal_gas_constant_si=scales.Quantity(ideal_gas_constant_si),
|
| 111 |
+
water_vapor_gas_constant_si=scales.Quantity(water_vapor_gas_constant_si),
|
| 112 |
+
water_vapor_isobaric_heat_capacity_si=scales.Quantity(
|
| 113 |
+
water_vapor_isobaric_heat_capacity_si),
|
| 114 |
+
kappa_si=scales.Quantity(kappa_si),
|
| 115 |
+
scale=scale)
|
model/legacy/steps.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Modules that parameterize composed time-steppers."""
|
| 15 |
+
|
| 16 |
+
import abc
|
| 17 |
+
import functools
|
| 18 |
+
from typing import Any, Callable, Optional, Sequence
|
| 19 |
+
from dinosaur import coordinate_systems
|
| 20 |
+
from dinosaur import primitive_equations
|
| 21 |
+
from dinosaur import time_integration
|
| 22 |
+
from dinosaur import typing
|
| 23 |
+
import gin
|
| 24 |
+
import haiku as hk
|
| 25 |
+
from model.legacy import diagnostics
|
| 26 |
+
from model.legacy import integrators
|
| 27 |
+
from model.legacy import perturbations
|
| 28 |
+
from model.legacy import stochastic
|
| 29 |
+
|
| 30 |
+
DiagnosticModule = diagnostics.DiagnosticModule
|
| 31 |
+
Forcing = typing.Forcing
|
| 32 |
+
Pytree = typing.Pytree
|
| 33 |
+
ModelState = typing.ModelState
|
| 34 |
+
EquationModule = Callable[..., time_integration.ImplicitExplicitODE]
|
| 35 |
+
CorrectorModule = typing.CorrectorModule
|
| 36 |
+
PerturbationModule = perturbations.PerturbationModule
|
| 37 |
+
RandomnessModule = stochastic.RandomnessModule
|
| 38 |
+
PyTreeStepFilterModule = typing.PyTreeStepFilterModule
|
| 39 |
+
TimeIntegrator = integrators.TimeIntegrator
|
| 40 |
+
TransformModule = typing.TransformModule
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class BaseStep(abc.ABC):
|
| 44 |
+
"""Base class for Step modules."""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 49 |
+
dt: float,
|
| 50 |
+
physics_specs: Any,
|
| 51 |
+
aux_features: typing.AuxFeatures,
|
| 52 |
+
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
|
| 53 |
+
randomness_module: RandomnessModule = stochastic.NoRandomField,
|
| 54 |
+
):
|
| 55 |
+
self.diagnostics_fn = diagnostics_module(
|
| 56 |
+
coords, dt, physics_specs, aux_features)
|
| 57 |
+
self.randomness_fn = randomness_module(
|
| 58 |
+
coords, dt, physics_specs, aux_features)
|
| 59 |
+
|
| 60 |
+
@abc.abstractmethod
|
| 61 |
+
def __call__(
|
| 62 |
+
self,
|
| 63 |
+
state: ModelState,
|
| 64 |
+
forcing: typing.Forcing,
|
| 65 |
+
) -> ModelState:
|
| 66 |
+
"""Computes the state of the system evolved in time by `self.dt`."""
|
| 67 |
+
|
| 68 |
+
def finalize_state(
|
| 69 |
+
self,
|
| 70 |
+
x: ModelState,
|
| 71 |
+
forcing: typing.Forcing,
|
| 72 |
+
) -> ModelState:
|
| 73 |
+
"""Finalizes initialization of a model state `x`, encoded from data.
|
| 74 |
+
|
| 75 |
+
This method ensures that state has all of the `ModelState` fields
|
| 76 |
+
initialized in a way compatible with this step function. This includes
|
| 77 |
+
populating initial `diagnostics`, `memory` and `randomness` fields.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
x: Initial values for the model state typically provided by the encoder.
|
| 81 |
+
forcing: Data covariates from the same time slice as `x`.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
Initialized model state.
|
| 85 |
+
"""
|
| 86 |
+
x.randomness = self.randomness_fn.unconditional_sample(
|
| 87 |
+
hk.maybe_next_rng_key()
|
| 88 |
+
)
|
| 89 |
+
x.diagnostics = self.diagnostics_fn(
|
| 90 |
+
x, physics_tendencies=None, forcing=forcing)
|
| 91 |
+
return x
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@gin.register
|
| 95 |
+
class EquationStep(BaseStep, hk.Module):
|
| 96 |
+
"""Step module that advances the state by integrating an equation in time."""
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 101 |
+
dt: float,
|
| 102 |
+
physics_specs: Any,
|
| 103 |
+
aux_features: typing.AuxFeatures,
|
| 104 |
+
equation_module: EquationModule,
|
| 105 |
+
time_integrator: TimeIntegrator = integrators.imex_rk_sil3,
|
| 106 |
+
filter_modules: Sequence[PyTreeStepFilterModule] = tuple(),
|
| 107 |
+
checkpoint_explicit_terms: bool = True,
|
| 108 |
+
name: Optional[str] = None,
|
| 109 |
+
):
|
| 110 |
+
hk.Module.__init__(self, name=name)
|
| 111 |
+
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
|
| 112 |
+
equation = equation_module(coords, dt, physics_specs, aux_features)
|
| 113 |
+
if checkpoint_explicit_terms:
|
| 114 |
+
equation = time_integration.ImplicitExplicitODE.from_functions(
|
| 115 |
+
hk.remat(equation.explicit_terms),
|
| 116 |
+
equation.implicit_terms,
|
| 117 |
+
equation.implicit_inverse) # pyrefly: ignore[bad-argument-type]
|
| 118 |
+
step_fn = time_integrator(equation, dt)
|
| 119 |
+
filter_fns = [
|
| 120 |
+
module(coords, dt, physics_specs, aux_features)
|
| 121 |
+
for module in filter_modules]
|
| 122 |
+
self.dt = dt
|
| 123 |
+
self.step_fn = time_integration.step_with_filters(step_fn, filter_fns)
|
| 124 |
+
|
| 125 |
+
def __call__(
|
| 126 |
+
self,
|
| 127 |
+
x: ModelState,
|
| 128 |
+
forcing: Optional[typing.Forcing] = None,
|
| 129 |
+
) -> ModelState:
|
| 130 |
+
"""Computes the state of the system evolved in time by `dt`."""
|
| 131 |
+
del forcing
|
| 132 |
+
next_state = time_integration.maybe_fix_sim_time_roundoff(
|
| 133 |
+
self.step_fn(x.state), self.dt)
|
| 134 |
+
return ModelState(next_state) # pyrefly: ignore[bad-argument-count]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@gin.register
|
| 138 |
+
class RepeatedStep(BaseStep, hk.Module):
|
| 139 |
+
"""Step module that consists of repeated substeps of the same form."""
|
| 140 |
+
|
| 141 |
+
def __init__(
|
| 142 |
+
self,
|
| 143 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 144 |
+
dt: float,
|
| 145 |
+
physics_specs: Any,
|
| 146 |
+
aux_features: typing.AuxFeatures,
|
| 147 |
+
inner_step_module: typing.StepModule,
|
| 148 |
+
num_inner_steps: int = 1,
|
| 149 |
+
name: Optional[str] = None,
|
| 150 |
+
):
|
| 151 |
+
hk.Module.__init__(self, name=name)
|
| 152 |
+
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
|
| 153 |
+
inner_dt = dt / num_inner_steps
|
| 154 |
+
self.step_fn = inner_step_module(
|
| 155 |
+
coords, inner_dt, physics_specs, aux_features)
|
| 156 |
+
self.num_inner_steps = num_inner_steps
|
| 157 |
+
|
| 158 |
+
def __call__(
|
| 159 |
+
self,
|
| 160 |
+
state: ModelState,
|
| 161 |
+
forcing: typing.Forcing,
|
| 162 |
+
) -> ModelState:
|
| 163 |
+
"""Computes the state of the system evolved in time by `dt`."""
|
| 164 |
+
step_fn = functools.partial(self.step_fn, forcing=forcing)
|
| 165 |
+
step_fn = time_integration.repeated(step_fn, self.num_inner_steps, hk.scan)
|
| 166 |
+
return step_fn(state)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
@gin.register
|
| 170 |
+
class CustomCoordsStep(BaseStep, hk.Module):
|
| 171 |
+
"""Step module that uses gin-configured coordinates instead of coords.
|
| 172 |
+
|
| 173 |
+
This class currently supports model states in spectral representation. It
|
| 174 |
+
could be easily extended to nodal-state models by converting to modal space
|
| 175 |
+
prior to spectral interpolation and back after the timestep if performed.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
def __init__(
|
| 179 |
+
self,
|
| 180 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 181 |
+
dt: float,
|
| 182 |
+
physics_specs: Any,
|
| 183 |
+
aux_features: typing.AuxFeatures,
|
| 184 |
+
step_module: typing.StepModule,
|
| 185 |
+
custom_coords: coordinate_systems.CoordinateSystem = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 186 |
+
name: Optional[str] = None,
|
| 187 |
+
):
|
| 188 |
+
hk.Module.__init__(self, name=name)
|
| 189 |
+
BaseStep.__init__(self, coords, dt, physics_specs, aux_features)
|
| 190 |
+
self.step_fn = step_module(
|
| 191 |
+
custom_coords, dt, physics_specs, aux_features)
|
| 192 |
+
self.to_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
|
| 193 |
+
coords, custom_coords)
|
| 194 |
+
self.from_custom_coords_fn = coordinate_systems.get_spectral_interpolate_fn(
|
| 195 |
+
custom_coords, coords)
|
| 196 |
+
|
| 197 |
+
def __call__(
|
| 198 |
+
self,
|
| 199 |
+
x: typing.PyTreeState,
|
| 200 |
+
forcing: typing.Forcing,
|
| 201 |
+
) -> typing.PyTreeState:
|
| 202 |
+
del forcing # currently not supported.
|
| 203 |
+
x = self.to_custom_coords_fn(x)
|
| 204 |
+
custom_out = self.step_fn(x, None)
|
| 205 |
+
return self.from_custom_coords_fn(custom_out)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@gin.register
|
| 209 |
+
class StochasticPhysicsParameterizationStep(BaseStep, hk.Module):
|
| 210 |
+
"""Step module that uses stochastic physics tendencies with dycore."""
|
| 211 |
+
|
| 212 |
+
def __init__(
|
| 213 |
+
self,
|
| 214 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 215 |
+
dt: float,
|
| 216 |
+
physics_specs: Any,
|
| 217 |
+
aux_features: typing.AuxFeatures,
|
| 218 |
+
corrector_module: CorrectorModule,
|
| 219 |
+
physics_parameterization_module: typing.ParameterizationModule,
|
| 220 |
+
num_substeps: int = 1,
|
| 221 |
+
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
|
| 222 |
+
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
|
| 223 |
+
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
|
| 224 |
+
checkpoint_substep: bool = False,
|
| 225 |
+
name: Optional[str] = None,
|
| 226 |
+
):
|
| 227 |
+
hk.Module.__init__(self, name=name)
|
| 228 |
+
BaseStep.__init__(
|
| 229 |
+
self, coords, dt, physics_specs, aux_features,
|
| 230 |
+
diagnostics_module=diagnostics_module,
|
| 231 |
+
randomness_module=randomness_module)
|
| 232 |
+
inner_dt = dt / num_substeps
|
| 233 |
+
self.num_substeps = num_substeps
|
| 234 |
+
self.corrector_fn = corrector_module(
|
| 235 |
+
coords, inner_dt, physics_specs, aux_features)
|
| 236 |
+
self.physics_parameterization_fn = physics_parameterization_module(
|
| 237 |
+
coords, inner_dt, physics_specs, aux_features)
|
| 238 |
+
self.perturbation_fn = perturbation_module(
|
| 239 |
+
coords, inner_dt, physics_specs, aux_features)
|
| 240 |
+
self.checkpoint_substep = checkpoint_substep
|
| 241 |
+
self.coords = coords
|
| 242 |
+
|
| 243 |
+
def finalize_state(
|
| 244 |
+
self,
|
| 245 |
+
x: ModelState,
|
| 246 |
+
forcing: typing.Forcing,
|
| 247 |
+
) -> ModelState:
|
| 248 |
+
"""Finalizes initialization of a model state `x`, encoded from data.
|
| 249 |
+
|
| 250 |
+
This method ensures that state has all of the `ModelState` fields
|
| 251 |
+
initialized in a way compatible with this step function. This includes
|
| 252 |
+
populating initial `diagnostics`, `memory` and `randomness` fields.
|
| 253 |
+
|
| 254 |
+
This is called by StochasticModularStepModel.encode, after encoding the data
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
x: Initial values for the model state typically provided by the encoder.
|
| 258 |
+
forcing: Data covariates from the same time slice as `x`.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
Initialized model state.
|
| 262 |
+
"""
|
| 263 |
+
# TODO(dkochkov) Consider adding an option of not overriding randomness.
|
| 264 |
+
x.randomness = self.randomness_fn.unconditional_sample(
|
| 265 |
+
hk.maybe_next_rng_key()
|
| 266 |
+
)
|
| 267 |
+
pp_tendency = self.physics_parameterization_fn(
|
| 268 |
+
x.state, x.memory, x.diagnostics, x.randomness.nodal_value, forcing
|
| 269 |
+
)
|
| 270 |
+
x.diagnostics = self.diagnostics_fn(x, pp_tendency, forcing)
|
| 271 |
+
return x
|
| 272 |
+
|
| 273 |
+
def __call__(
|
| 274 |
+
self,
|
| 275 |
+
state: ModelState,
|
| 276 |
+
forcing: typing.Forcing,
|
| 277 |
+
) -> ModelState:
|
| 278 |
+
"""Computes the state of the system evolved in time by `dt`."""
|
| 279 |
+
|
| 280 |
+
def step_fn(x):
|
| 281 |
+
x = self.coords.with_dycore_sharding(x)
|
| 282 |
+
# TODO(dkochkov) Consider passing `x` to physics_parameterization.
|
| 283 |
+
pp_tendency = self.physics_parameterization_fn(
|
| 284 |
+
x.state, x.memory, x.diagnostics, x.randomness.nodal_value, forcing
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
pp_tendency = self.perturbation_fn(
|
| 288 |
+
pp_tendency,
|
| 289 |
+
state=x.state,
|
| 290 |
+
randomness=x.randomness.nodal_value,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
next_state = self.corrector_fn(x.state, pp_tendency, forcing)
|
| 294 |
+
# TODO(dkochkov) update stochastic modules to take optional state.
|
| 295 |
+
next_randomness = self.randomness_fn.advance(x.randomness)
|
| 296 |
+
next_memory = x.state if x.memory is not None else None
|
| 297 |
+
next_diagnostics = self.diagnostics_fn(x, pp_tendency, forcing)
|
| 298 |
+
x_next = ModelState(
|
| 299 |
+
state=next_state, memory=next_memory, diagnostics=next_diagnostics, # pyrefly: ignore[unexpected-keyword]
|
| 300 |
+
randomness=next_randomness) # pyrefly: ignore[unexpected-keyword]
|
| 301 |
+
x_next = self.coords.with_dycore_sharding(x_next)
|
| 302 |
+
return x_next
|
| 303 |
+
|
| 304 |
+
if self.checkpoint_substep:
|
| 305 |
+
step_fn = hk.remat(step_fn)
|
| 306 |
+
step_fn = time_integration.repeated(step_fn, self.num_substeps, hk.scan)
|
| 307 |
+
return step_fn(state)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
# TODO(dkochkov) Move vertical advection step to transforms.py.
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@gin.register
|
| 314 |
+
class SemiLagrangianVerticalAdvectionStep(hk.Module):
|
| 315 |
+
"""Step module that applies vertical advection for the primitive equations."""
|
| 316 |
+
|
| 317 |
+
def __init__(
|
| 318 |
+
self,
|
| 319 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 320 |
+
dt: float,
|
| 321 |
+
physics_specs: Any,
|
| 322 |
+
aux_features: typing.AuxFeatures,
|
| 323 |
+
name: Optional[str] = None,
|
| 324 |
+
):
|
| 325 |
+
super().__init__(name=name)
|
| 326 |
+
self.coords = coords
|
| 327 |
+
self.dt = dt
|
| 328 |
+
|
| 329 |
+
def __call__(self, state):
|
| 330 |
+
return primitive_equations.semi_lagrangian_vertical_advection_step(
|
| 331 |
+
state, self.coords, self.dt
|
| 332 |
+
)
|
model/legacy/stochastic.py
ADDED
|
@@ -0,0 +1,1225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Implementation of stochastic modules."""
|
| 15 |
+
|
| 16 |
+
import abc
|
| 17 |
+
import dataclasses
|
| 18 |
+
import enum
|
| 19 |
+
import logging
|
| 20 |
+
from typing import Any, Callable, Optional, Sequence, TypeVar, Union
|
| 21 |
+
import zlib
|
| 22 |
+
|
| 23 |
+
from dinosaur import coordinate_systems
|
| 24 |
+
from dinosaur import typing
|
| 25 |
+
import gin
|
| 26 |
+
import haiku as hk
|
| 27 |
+
import jax
|
| 28 |
+
import jax.numpy as jnp
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
tree_map = jax.tree_util.tree_map
|
| 32 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 33 |
+
|
| 34 |
+
Numeric = typing.Numeric
|
| 35 |
+
Quantity = typing.Quantity
|
| 36 |
+
_SOFTPLUS_INVERSE_1 = 0.5413248546129181
|
| 37 |
+
|
| 38 |
+
# CoreRandomState is advanced by a RandomField, and .to_*_values(core_state)
|
| 39 |
+
# produces the final (usable) random Array.
|
| 40 |
+
CoreRandomState = typing.Pytree
|
| 41 |
+
RandomnessState = typing.RandomnessState
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _validate_randomness_state(state: RandomnessState) -> None:
|
| 45 |
+
"""Validates that `state.core` is not `None`, raises an error otherwise."""
|
| 46 |
+
if state.core is None:
|
| 47 |
+
raise ValueError(
|
| 48 |
+
f'Got {state.core=} when value is expected. '
|
| 49 |
+
'Check how incoming randomness is initialized.'
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def make_positive_scalar(raw_parameter: typing.Array) -> jax.Array:
|
| 54 |
+
"""Positive [batch] scalar values, maps 0 --> 1 using a softplus(...)."""
|
| 55 |
+
raw_parameter = jnp.asarray(raw_parameter)
|
| 56 |
+
return jax.nn.softplus(raw_parameter + _SOFTPLUS_INVERSE_1)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# pylint: disable=logging-fstring-interpolation
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
################################################################################
|
| 63 |
+
# Single random fields that stand on their own.
|
| 64 |
+
################################################################################
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class PreferredRepresentation(enum.Enum):
|
| 68 |
+
"""The preferred (for computational reasons) representation of a field."""
|
| 69 |
+
|
| 70 |
+
NODAL = 'NODAL'
|
| 71 |
+
MODAL = 'MODAL'
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class RandomField(abc.ABC):
|
| 75 |
+
"""Base class for random fields."""
|
| 76 |
+
|
| 77 |
+
def __init__(self, coords):
|
| 78 |
+
self.coords = coords
|
| 79 |
+
|
| 80 |
+
@property
|
| 81 |
+
@abc.abstractmethod
|
| 82 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 83 |
+
"""The PreferredRepresentation for this field, or None if no preference."""
|
| 84 |
+
|
| 85 |
+
@abc.abstractmethod
|
| 86 |
+
def unconditional_sample(self, rng: typing.PRNGKeyArray) -> RandomnessState:
|
| 87 |
+
"""Sample the random field unconditionally."""
|
| 88 |
+
|
| 89 |
+
@abc.abstractmethod
|
| 90 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 91 |
+
"""Updates the core state of a random field."""
|
| 92 |
+
|
| 93 |
+
@abc.abstractmethod
|
| 94 |
+
def to_modal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 95 |
+
"""Returns the modal rep. of the random field specified by this class."""
|
| 96 |
+
|
| 97 |
+
@abc.abstractmethod
|
| 98 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 99 |
+
"""Returns the nodal rep. of the random field specified by this class."""
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
RandomnessModule = Callable[..., RandomField]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
_ADVANCE_SALT = zlib.crc32(b'advance') # arbitrary uint32 value
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
T = TypeVar('T', typing.PRNGKeyArray, None)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _prng_key_for_current_advance_step(
|
| 112 |
+
randomness: typing.RandomnessState,
|
| 113 |
+
) -> typing.PRNGKeyArray | None:
|
| 114 |
+
"""Get a PRNG Key suitable for randomness in the current advance step."""
|
| 115 |
+
if randomness.prng_key is None:
|
| 116 |
+
return None
|
| 117 |
+
salt = jnp.uint32(_ADVANCE_SALT) + jnp.uint32(randomness.prng_step)
|
| 118 |
+
return jax.random.fold_in(randomness.prng_key, salt)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@gin.register
|
| 122 |
+
class NoRandomField(RandomField):
|
| 123 |
+
"""Module that disables randomness in a given module returning `None`."""
|
| 124 |
+
|
| 125 |
+
def __init__(
|
| 126 |
+
self,
|
| 127 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 128 |
+
dt: float,
|
| 129 |
+
physics_specs: Any,
|
| 130 |
+
aux_features: Any,
|
| 131 |
+
prefer_nodal: bool = True,
|
| 132 |
+
):
|
| 133 |
+
"""Constructs a ZerosRandomField.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
coords: horizontal and vertical grid data.
|
| 137 |
+
dt: nondimensionalized model time step.
|
| 138 |
+
physics_specs: physical constants and definition of custom units.
|
| 139 |
+
aux_features: additional static data.
|
| 140 |
+
prefer_nodal: Whether this field should prefer a nodal representation.
|
| 141 |
+
"""
|
| 142 |
+
super().__init__(coords)
|
| 143 |
+
logging.info('[NGCM] Initializing NoRandomField')
|
| 144 |
+
del dt, physics_specs, aux_features, prefer_nodal # unused.
|
| 145 |
+
|
| 146 |
+
@property
|
| 147 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
def unconditional_sample(
|
| 151 |
+
self, rng: typing.PRNGKeyArray | None
|
| 152 |
+
) -> RandomnessState:
|
| 153 |
+
"""Returns a zeros initialized state."""
|
| 154 |
+
return RandomnessState(prng_key=rng, prng_step=0) # pyrefly: ignore[unexpected-keyword]
|
| 155 |
+
|
| 156 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 157 |
+
"""Updates the state of a random gaussian field."""
|
| 158 |
+
return RandomnessState(
|
| 159 |
+
prng_key=state.prng_key, prng_step=state.prng_step + 1 # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 163 |
+
del core_state # unused.
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
def to_modal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 167 |
+
del core_state # unused.
|
| 168 |
+
return None
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@gin.register
|
| 172 |
+
class ZerosRandomField(RandomField):
|
| 173 |
+
"""Implements a constant random field identically equal to zero."""
|
| 174 |
+
|
| 175 |
+
def __init__(
|
| 176 |
+
self,
|
| 177 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 178 |
+
dt: float,
|
| 179 |
+
physics_specs: Any,
|
| 180 |
+
aux_features: Any,
|
| 181 |
+
prefer_nodal: bool = True,
|
| 182 |
+
):
|
| 183 |
+
"""Constructs a ZerosRandomField.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
coords: horizontal and vertical grid data.
|
| 187 |
+
dt: nondimensionalized model time step.
|
| 188 |
+
physics_specs: physical constants and definition of custom units.
|
| 189 |
+
aux_features: additional static data.
|
| 190 |
+
prefer_nodal: Whether this field should prefer a nodal representation.
|
| 191 |
+
"""
|
| 192 |
+
super().__init__(coords)
|
| 193 |
+
logging.info('[NGCM] Initializing ZerosRandomField')
|
| 194 |
+
del dt # unused
|
| 195 |
+
del physics_specs # unused.
|
| 196 |
+
del aux_features # unused.
|
| 197 |
+
self._prefer_nodal = prefer_nodal
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 201 |
+
if self._prefer_nodal:
|
| 202 |
+
return PreferredRepresentation.NODAL
|
| 203 |
+
else:
|
| 204 |
+
return PreferredRepresentation.MODAL
|
| 205 |
+
|
| 206 |
+
def unconditional_sample(
|
| 207 |
+
self, rng: typing.PRNGKeyArray | None
|
| 208 |
+
) -> RandomnessState:
|
| 209 |
+
"""Returns a zeros initialized state."""
|
| 210 |
+
if self._prefer_nodal:
|
| 211 |
+
core = jnp.zeros(self.coords.horizontal.nodal_shape)
|
| 212 |
+
else:
|
| 213 |
+
core = jnp.zeros(self.coords.horizontal.modal_shape)
|
| 214 |
+
return RandomnessState(
|
| 215 |
+
core=core, # pyrefly: ignore[unexpected-keyword]
|
| 216 |
+
nodal_value=jnp.zeros(self.coords.horizontal.nodal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 217 |
+
modal_value=jnp.zeros(self.coords.horizontal.modal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 218 |
+
prng_key=rng, # pyrefly: ignore[unexpected-keyword]
|
| 219 |
+
prng_step=0, # pyrefly: ignore[unexpected-keyword]
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 223 |
+
"""Updates the state of a random gaussian field."""
|
| 224 |
+
_validate_randomness_state(state)
|
| 225 |
+
return RandomnessState(
|
| 226 |
+
core=jnp.zeros_like(state.core), # pyrefly: ignore[bad-argument-type, unexpected-keyword]
|
| 227 |
+
nodal_value=jnp.zeros(self.coords.horizontal.nodal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 228 |
+
modal_value=jnp.zeros(self.coords.horizontal.modal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 229 |
+
prng_key=state.prng_key, # pyrefly: ignore[unexpected-keyword]
|
| 230 |
+
prng_step=state.prng_step + 1, # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 234 |
+
"""Returns the ready-for-use Zeros random field."""
|
| 235 |
+
return jnp.zeros(self.coords.horizontal.nodal_shape)
|
| 236 |
+
|
| 237 |
+
def to_modal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 238 |
+
"""Returns the ready-for-use Zeros random field."""
|
| 239 |
+
return jnp.zeros(self.coords.horizontal.modal_shape)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
@gin.register
|
| 243 |
+
class GaussianRandomField(RandomField):
|
| 244 |
+
"""Implements gaussian random field with spatial and temporal correlations.
|
| 245 |
+
|
| 246 |
+
This type of random fields is used in SPPT (stochastic physics
|
| 247 |
+
parameterization tendencies) schemes, where each tendency due to physics
|
| 248 |
+
parameterizations are multiplicatively perturbed by the value of such field.
|
| 249 |
+
|
| 250 |
+
For implementation details see Appendix 8 in http://shortn/_56HCcQwmSS.
|
| 251 |
+
|
| 252 |
+
With x ∈ EarthSurface, this field U is initialized at t=0 with
|
| 253 |
+
U(0, x) = Σₖ Ψₖ(x) (1 - φ²)^(-0.5) σₖ γₖ σₖ ηₖ₀,
|
| 254 |
+
where Ψₖ is the kth spherical harmonic basis function, φ² is the one timestep
|
| 255 |
+
correlation, σₖ > 0 is a scaling factor, and ηₖ₀ are iid 1D unit Gaussians.
|
| 256 |
+
|
| 257 |
+
With `variance` an init kwarg,
|
| 258 |
+
E[U(0, x)] ≡ 0,
|
| 259 |
+
1 / (4πR²) ∫ Var(U(0, x))dx = variance,
|
| 260 |
+
regardless of coords (and the radius).
|
| 261 |
+
|
| 262 |
+
Further states are generated with the recursion
|
| 263 |
+
U(t + δ) = ϕ U(t) + σₖ ηₖₜ
|
| 264 |
+
This ensures that U is stationary.
|
| 265 |
+
|
| 266 |
+
In general,
|
| 267 |
+
Cov(U(t, x), U(t + δ, y)) = ϕᵟ Σₖ Ψₖ(x) Ψₖ(y) (γₖ)².
|
| 268 |
+
"""
|
| 269 |
+
|
| 270 |
+
def __init__(
|
| 271 |
+
self,
|
| 272 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 273 |
+
dt: float,
|
| 274 |
+
physics_specs: Any,
|
| 275 |
+
aux_features: Any,
|
| 276 |
+
correlation_time: Union[jax.Array, Quantity, str] = gin.REQUIRED,
|
| 277 |
+
correlation_length: Union[jax.Array, Quantity, str] = gin.REQUIRED,
|
| 278 |
+
variance: Optional[Union[jax.Array, Quantity, str]] = gin.REQUIRED,
|
| 279 |
+
clip: float = 6.0,
|
| 280 |
+
):
|
| 281 |
+
"""Constructs a GaussianRandomField.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
coords: horizontal and vertical grid data.
|
| 285 |
+
dt: nondimensionalized model time step.
|
| 286 |
+
physics_specs: physical constants and definition of custom units.
|
| 287 |
+
aux_features: additional static data.
|
| 288 |
+
correlation_time: timescale with units over which autoregressive process
|
| 289 |
+
decorrelates. Typical values in NWP range from hours to days.
|
| 290 |
+
correlation_length: lengthscale with units over which random field is
|
| 291 |
+
correlated. Typical values in NWP range from 500-2500 km.
|
| 292 |
+
variance: The average (over EarthSurface) variance of the random field If
|
| 293 |
+
None, this GRF always returns a zeros field and no RNGS are drawn.
|
| 294 |
+
clip: number of standard deviations at which to clip randomness to ensure
|
| 295 |
+
numerical stability.
|
| 296 |
+
"""
|
| 297 |
+
del aux_features # unused.
|
| 298 |
+
super().__init__(coords)
|
| 299 |
+
logging.info(
|
| 300 |
+
'[NGCM] Initializing GaussianRandomField (possibly via'
|
| 301 |
+
f' CenteredLognormalRandomField) with {variance=}, {correlation_time=},'
|
| 302 |
+
f' {correlation_length=}'
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
tau = maybe_nondimensionalize(correlation_time, physics_specs)
|
| 306 |
+
correlation_length = maybe_nondimensionalize(
|
| 307 |
+
correlation_length, physics_specs
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# In sampling, phi appears as 1 - phi**2 = 1 - exp(-2 dt / tau)
|
| 311 |
+
self.one_minus_phi2 = -jnp.expm1(-2 * dt / tau) # pyrefly: ignore[unsupported-operation]
|
| 312 |
+
|
| 313 |
+
self.phi = jnp.exp(-dt / tau) # pyrefly: ignore[unsupported-operation]
|
| 314 |
+
|
| 315 |
+
self._variance = maybe_nondimensionalize(variance, physics_specs) # σ²
|
| 316 |
+
|
| 317 |
+
# [Palmer] states correlation_length = sqrt(2κT) / R, therefore
|
| 318 |
+
self.kt = (correlation_length / self.coords.horizontal.radius) ** 2 / 2
|
| 319 |
+
self.clip = clip
|
| 320 |
+
|
| 321 |
+
@property
|
| 322 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 323 |
+
return PreferredRepresentation.MODAL
|
| 324 |
+
|
| 325 |
+
@property
|
| 326 |
+
def _surf_area(self) -> jax.Array:
|
| 327 |
+
"""Surface area of sphere of radius self.coords.horizontal.radius."""
|
| 328 |
+
return 4 * jnp.pi * self.coords.horizontal.radius**2 # pytype: disable=bad-return-type # jnp-type
|
| 329 |
+
|
| 330 |
+
def _sigma_array(self) -> jax.Array:
|
| 331 |
+
"""Array of σₙ from Appendix 8 in [Palmer] http://shortn/_56HCcQwmSS."""
|
| 332 |
+
# n = [0, 1, ..., N]
|
| 333 |
+
n = self.coords.horizontal.modal_axes[1] # total wavenumbers.
|
| 334 |
+
|
| 335 |
+
# Number of longitudinal wavenumbers at each total wavenumber n.
|
| 336 |
+
# L = 2n + 1, except for the last entry.
|
| 337 |
+
n_longitudian_wavenumbers = self.coords.horizontal.mask.sum(axis=0)
|
| 338 |
+
|
| 339 |
+
# sigmas_unnormed[n] is proportional to the standard deviation for each
|
| 340 |
+
# longitudinal wavenumbers at each total wavenumber n.
|
| 341 |
+
sigmas_unnormed = jnp.exp(-0.5 * self.kt * n * (n + 1))
|
| 342 |
+
|
| 343 |
+
# The sum of unnormalized variance for all longitudinal wavenumbers at each
|
| 344 |
+
# total wavenumber.
|
| 345 |
+
sum_unnormed_vars = jnp.sum(n_longitudian_wavenumbers * sigmas_unnormed**2)
|
| 346 |
+
|
| 347 |
+
# This is analogous to F₀ from [Palmer].
|
| 348 |
+
# (normalization * sigmas_unnormed)² would sum to 1. The leading factor
|
| 349 |
+
# self._integrated_grf_variance * (1 - self.phi ** 2)
|
| 350 |
+
# ensures that the AR(1) process has variance self._integrated_grf_variance.
|
| 351 |
+
# We do not include the extra fator of 2 in the denominator. I do not know
|
| 352 |
+
# why [Palmer] has this factor.
|
| 353 |
+
normalization = jnp.sqrt(
|
| 354 |
+
self._integrated_grf_variance() # pyrefly: ignore[unsupported-operation]
|
| 355 |
+
* self.one_minus_phi2
|
| 356 |
+
/ sum_unnormed_vars
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
# The factor of coords.horizontal.radius appears because our basis vectors
|
| 360 |
+
# have L2 norm = radius. See http://screen/9FYVXZ5cMHoGDZk
|
| 361 |
+
return normalization * sigmas_unnormed / self.coords.horizontal.radius
|
| 362 |
+
|
| 363 |
+
def unconditional_sample(self, rng: typing.PRNGKeyArray) -> RandomnessState:
|
| 364 |
+
"""Returns a randomly initialized state for the autoregressive process."""
|
| 365 |
+
modal_shape = self.coords.horizontal.modal_shape
|
| 366 |
+
rng, next_rng = jax.random.split(rng)
|
| 367 |
+
if self.variance is None:
|
| 368 |
+
return RandomnessState(
|
| 369 |
+
core=jnp.zeros(modal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 370 |
+
nodal_value=jnp.zeros(self.coords.horizontal.nodal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 371 |
+
modal_value=jnp.zeros(modal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 372 |
+
prng_key=next_rng, # pyrefly: ignore[unexpected-keyword]
|
| 373 |
+
prng_step=0, # pyrefly: ignore[unexpected-keyword]
|
| 374 |
+
)
|
| 375 |
+
sigmas = self._sigma_array()
|
| 376 |
+
weights = jnp.where(
|
| 377 |
+
self.coords.horizontal.mask,
|
| 378 |
+
jax.random.truncated_normal(rng, -self.clip, self.clip, modal_shape),
|
| 379 |
+
jnp.zeros(modal_shape),
|
| 380 |
+
)
|
| 381 |
+
core = self.one_minus_phi2 ** (-0.5) * sigmas * weights
|
| 382 |
+
return RandomnessState(
|
| 383 |
+
core=core, # pyrefly: ignore[unexpected-keyword]
|
| 384 |
+
nodal_value=self.to_nodal_values(core), # pyrefly: ignore[unexpected-keyword]
|
| 385 |
+
modal_value=self.to_modal_values(core), # pyrefly: ignore[unexpected-keyword]
|
| 386 |
+
prng_key=next_rng, # pyrefly: ignore[unexpected-keyword]
|
| 387 |
+
prng_step=0, # pyrefly: ignore[unexpected-keyword]
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 391 |
+
"""Updates the CoreRandomState of a random gaussian field."""
|
| 392 |
+
_validate_randomness_state(state)
|
| 393 |
+
if self.variance is None:
|
| 394 |
+
return RandomnessState(
|
| 395 |
+
core=jnp.zeros_like(state.core), # pyrefly: ignore[bad-argument-type, unexpected-keyword]
|
| 396 |
+
nodal_value=jnp.zeros(self.coords.horizontal.nodal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 397 |
+
modal_value=jnp.zeros(self.coords.horizontal.modal_shape), # pyrefly: ignore[unexpected-keyword]
|
| 398 |
+
prng_key=state.prng_key, # pyrefly: ignore[unexpected-keyword]
|
| 399 |
+
prng_step=state.prng_step + 1, # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 400 |
+
)
|
| 401 |
+
modal_shape = self.coords.horizontal.modal_shape
|
| 402 |
+
rng = _prng_key_for_current_advance_step(state)
|
| 403 |
+
eta = jax.random.truncated_normal(rng, -self.clip, self.clip, modal_shape) # pyrefly: ignore[bad-argument-type]
|
| 404 |
+
next_core = state.core * self.phi + self._sigma_array() * jnp.where( # pyrefly: ignore[unsupported-operation]
|
| 405 |
+
self.coords.horizontal.mask, eta, jnp.zeros(modal_shape)
|
| 406 |
+
)
|
| 407 |
+
return RandomnessState(
|
| 408 |
+
core=next_core, # pyrefly: ignore[unexpected-keyword]
|
| 409 |
+
nodal_value=self.to_nodal_values(next_core), # pyrefly: ignore[unexpected-keyword]
|
| 410 |
+
modal_value=self.to_modal_values(next_core), # pyrefly: ignore[unexpected-keyword]
|
| 411 |
+
prng_key=state.prng_key, # pyrefly: ignore[unexpected-keyword]
|
| 412 |
+
prng_step=state.prng_step + 1, # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
@property
|
| 416 |
+
def variance(self) -> Numeric | None:
|
| 417 |
+
"""An estimate of pointwise (in nodal space) variance of this random field.
|
| 418 |
+
|
| 419 |
+
This random field is defined in spectral space, and has no precise
|
| 420 |
+
pointwise variance quantity. However, it does have a precise integrated
|
| 421 |
+
variance, which is used to define the field.
|
| 422 |
+
|
| 423 |
+
If we assume the field is stationary (with higher spectral
|
| 424 |
+
precision it is near stationary), then the average of this quantity is a
|
| 425 |
+
good pointwise estimate. So define
|
| 426 |
+
σ² := (1 / (4πR²)) ∫ Var(U(0, x))dx
|
| 427 |
+
= (1 / (4πR²)) integrated_grf_variance
|
| 428 |
+
|
| 429 |
+
Therefore the init parameter `variance` can be used to define
|
| 430 |
+
`_integrated_grf_variance := variance * surf_area`
|
| 431 |
+
and then `_integrated_grf_variance` is used to define this field. The result
|
| 432 |
+
is a field with pointwise variance close to the init kwarg `variance`.
|
| 433 |
+
|
| 434 |
+
Returns:
|
| 435 |
+
Numeric estimate of pointwise variance.
|
| 436 |
+
"""
|
| 437 |
+
return self._variance
|
| 438 |
+
|
| 439 |
+
def _integrated_grf_variance(self) -> Numeric | None:
|
| 440 |
+
"""Integral of the GRF's variance over the earth's surface."""
|
| 441 |
+
if self.variance is None:
|
| 442 |
+
return self.variance
|
| 443 |
+
return self.variance * self._surf_area
|
| 444 |
+
|
| 445 |
+
def to_modal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 446 |
+
"""Returns the ready-for-use Gaussian random field."""
|
| 447 |
+
return core_state
|
| 448 |
+
|
| 449 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 450 |
+
"""Returns the ready-for-use Gaussian random field."""
|
| 451 |
+
return self.coords.horizontal.to_nodal(core_state)
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
@gin.register
|
| 455 |
+
class GaussianRandomFieldModule(GaussianRandomField, hk.Module):
|
| 456 |
+
"""Module wrapper of GaussianRandomField with trainable parameters."""
|
| 457 |
+
|
| 458 |
+
def __init__(
|
| 459 |
+
self,
|
| 460 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 461 |
+
dt: float,
|
| 462 |
+
physics_specs: Any,
|
| 463 |
+
aux_features: Any,
|
| 464 |
+
initial_correlation_time: Union[Quantity, str] = gin.REQUIRED,
|
| 465 |
+
initial_correlation_length: Union[Quantity, str] = gin.REQUIRED,
|
| 466 |
+
initial_variance: Optional[Union[Quantity, str]] = gin.REQUIRED,
|
| 467 |
+
variance_bound: Optional[Union[Quantity, str]] = gin.REQUIRED,
|
| 468 |
+
tune_variance: bool = True,
|
| 469 |
+
clip: float = 6.0,
|
| 470 |
+
name: Optional[str] = None,
|
| 471 |
+
):
|
| 472 |
+
"""Constructs a GaussianRandomFieldModule.
|
| 473 |
+
|
| 474 |
+
Stochastic parameters are initialized at provided `initial_*` values.
|
| 475 |
+
This hk.Module can then be used to tune values.
|
| 476 |
+
|
| 477 |
+
Args:
|
| 478 |
+
coords: horizontal and vertical grid data.
|
| 479 |
+
dt: nondimensionalized model time step.
|
| 480 |
+
physics_specs: physical constants and definition of custom units.
|
| 481 |
+
aux_features: additional static data.
|
| 482 |
+
initial_correlation_time: timescale with units over which autoregressive
|
| 483 |
+
process decorrelates. Typical values in NWP range from hours to days.
|
| 484 |
+
initial_correlation_length: lengthscale with units over which random field
|
| 485 |
+
is correlated. Typical values in NWP range from 500-2500 km.
|
| 486 |
+
initial_variance: The average (over EarthSurface) variance of the random
|
| 487 |
+
field. If None, this GRF always returns a zeros field and no RNGS will
|
| 488 |
+
be drawn
|
| 489 |
+
variance_bound: If provided, an upper bound on tuned variance values.
|
| 490 |
+
tune_variance: Whether variance should be a tunable hk.parameter, or fixed
|
| 491 |
+
clip: number of standard deviations at which to clip randomness to ensure
|
| 492 |
+
numerical stability.
|
| 493 |
+
name: Something no one cares about and we just use None.
|
| 494 |
+
"""
|
| 495 |
+
# You must call hk.Module.__init__ before initializing this class.
|
| 496 |
+
hk.Module.__init__(self, name=name)
|
| 497 |
+
|
| 498 |
+
correlation_time_raw = hk.get_parameter(
|
| 499 |
+
'correlation_time_raw', shape=(), init=hk.initializers.Constant(0.0)
|
| 500 |
+
)
|
| 501 |
+
correlation_length_raw = hk.get_parameter(
|
| 502 |
+
'correlation_length_raw', shape=(), init=hk.initializers.Constant(0.0)
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
if tune_variance:
|
| 506 |
+
variance_raw = hk.get_parameter(
|
| 507 |
+
'variance_raw', shape=(), init=hk.initializers.Constant(0.0)
|
| 508 |
+
)
|
| 509 |
+
else:
|
| 510 |
+
variance_raw = 0.0
|
| 511 |
+
|
| 512 |
+
initial_variance = maybe_nondimensionalize(initial_variance, physics_specs)
|
| 513 |
+
_assert_positive_or_none(initial_variance, 'initial_variance')
|
| 514 |
+
|
| 515 |
+
if initial_variance is None:
|
| 516 |
+
variance = None
|
| 517 |
+
elif variance_bound in {None, 'None'}: # Allow strings for gin.
|
| 518 |
+
variance = convert_hk_param_to_positive_scalar(
|
| 519 |
+
variance_raw, initial_variance # pyrefly: ignore[bad-argument-type]
|
| 520 |
+
)
|
| 521 |
+
else:
|
| 522 |
+
variance_bound = maybe_nondimensionalize(variance_bound, physics_specs)
|
| 523 |
+
_assert_positive_or_none(variance_bound, 'variance_bound')
|
| 524 |
+
_assert_positive_or_none(
|
| 525 |
+
variance_bound - initial_variance, 'variance_bound - initial_variance' # pyrefly: ignore[unsupported-operation]
|
| 526 |
+
)
|
| 527 |
+
variance = convert_hk_param_to_bounded_scalar(
|
| 528 |
+
variance_raw, # pyrefly: ignore[bad-argument-type]
|
| 529 |
+
initial_variance,
|
| 530 |
+
low=0.0,
|
| 531 |
+
high=variance_bound, # pyrefly: ignore[bad-argument-type]
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
# We call GaussianRandomFieldModule.__init__ rather than super().__init__
|
| 535 |
+
# since we don't want to call hk.Module.__init__ twice... although doing
|
| 536 |
+
# that didn't hurt anything.
|
| 537 |
+
GaussianRandomField.__init__(
|
| 538 |
+
self,
|
| 539 |
+
coords=coords,
|
| 540 |
+
dt=dt,
|
| 541 |
+
physics_specs=physics_specs,
|
| 542 |
+
aux_features=aux_features,
|
| 543 |
+
correlation_time=convert_hk_param_to_positive_scalar(
|
| 544 |
+
correlation_time_raw,
|
| 545 |
+
maybe_nondimensionalize(initial_correlation_time, physics_specs), # pyrefly: ignore[bad-argument-type]
|
| 546 |
+
),
|
| 547 |
+
correlation_length=convert_hk_param_to_positive_scalar(
|
| 548 |
+
correlation_length_raw,
|
| 549 |
+
maybe_nondimensionalize(initial_correlation_length, physics_specs), # pyrefly: ignore[bad-argument-type]
|
| 550 |
+
),
|
| 551 |
+
variance=variance,
|
| 552 |
+
clip=clip,
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
################################################################################
|
| 557 |
+
# Single random fields that are derived from "stand on their own" fields.
|
| 558 |
+
################################################################################
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
@gin.register
|
| 562 |
+
class CenteredLognormalRandomField(GaussianRandomField):
|
| 563 |
+
"""A lognormal random field shifted to have mean zero."""
|
| 564 |
+
|
| 565 |
+
@property
|
| 566 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 567 |
+
return PreferredRepresentation.NODAL
|
| 568 |
+
|
| 569 |
+
def _integrated_grf_variance(self) -> jax.Array | None:
|
| 570 |
+
"""Integrated variance of the associated GRF (not this Lognormal field)."""
|
| 571 |
+
if self.variance is None:
|
| 572 |
+
return None
|
| 573 |
+
# If Z ~ Normal(μ, σ²), then X ~ exp(Z) has
|
| 574 |
+
# variance = (exp(σ²) - 1) exp(2μ + σ²).
|
| 575 |
+
# We have centered this field, which involved setting μ = -σ² / 2.
|
| 576 |
+
# => variance = exp(σ²) - 1,
|
| 577 |
+
# and thus
|
| 578 |
+
# σ² = log(1 + variance)
|
| 579 |
+
return jnp.log1p(self.variance) * self._surf_area
|
| 580 |
+
|
| 581 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> jax.Array:
|
| 582 |
+
"""Returns the ready-for-use Lognormal random field."""
|
| 583 |
+
if self.variance is None:
|
| 584 |
+
grf_variance = 0.0
|
| 585 |
+
else:
|
| 586 |
+
grf_variance = self._integrated_grf_variance() / self._surf_area # pyrefly: ignore[unsupported-operation]
|
| 587 |
+
# If Z ~ Normal(μ, σ²), then X ~ exp(Z) has mean exp(μ + σ²/2).
|
| 588 |
+
# To ensure E[X] = 1, we must set μ = -σ²/2.
|
| 589 |
+
x = self.coords.horizontal.to_nodal(core_state) # ~ Normal(0, σ²)
|
| 590 |
+
return jnp.expm1(x - grf_variance / 2) # ~ Exp(Normal(-σ²/2, σ²)) - 1
|
| 591 |
+
|
| 592 |
+
def to_modal_values(self, core_state: CoreRandomState) -> jax.Array:
|
| 593 |
+
"""Returns the ready-for-use Lognormal random field."""
|
| 594 |
+
return self.coords.horizontal.to_modal(self.to_nodal_values(core_state))
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
@gin.register
|
| 598 |
+
class CenteredLognormalRandomFieldModule(
|
| 599 |
+
CenteredLognormalRandomField, GaussianRandomFieldModule
|
| 600 |
+
):
|
| 601 |
+
"""A lognormal random hk.Module field shifted to have mean zero."""
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
################################################################################
|
| 605 |
+
# Fields made from many different fields.
|
| 606 |
+
################################################################################
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
@gin.register
|
| 610 |
+
class BatchGaussianRandomFieldModule(hk.Module):
|
| 611 |
+
"""Batch of independent GaussianRandomFieldModules.
|
| 612 |
+
|
| 613 |
+
These GRFs are meant to be fed into a neural network as generic "signals".
|
| 614 |
+
|
| 615 |
+
The state arrays have leading batch dim indexing independent GRFs.
|
| 616 |
+
"""
|
| 617 |
+
|
| 618 |
+
def __init__(
|
| 619 |
+
self,
|
| 620 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 621 |
+
dt: float,
|
| 622 |
+
physics_specs: Any,
|
| 623 |
+
aux_features: Any,
|
| 624 |
+
initial_correlation_times: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 625 |
+
initial_correlation_lengths: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 626 |
+
variances: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 627 |
+
field_subset: Optional[Sequence[int]] = None,
|
| 628 |
+
n_fixed_fields: Optional[int] = None,
|
| 629 |
+
clip: float = 6.0,
|
| 630 |
+
name: Optional[str] = None,
|
| 631 |
+
):
|
| 632 |
+
"""Constructs a BatchGaussianRandomFieldModule.
|
| 633 |
+
|
| 634 |
+
Correlation scales are initialized to `initial_*` args and will be tuned
|
| 635 |
+
by Haiku optimizers. Variance will be fixed.
|
| 636 |
+
|
| 637 |
+
Args:
|
| 638 |
+
coords: horizontal and vertical grid data.
|
| 639 |
+
dt: nondimensionalized model time step.
|
| 640 |
+
physics_specs: physical constants and definition of custom units.
|
| 641 |
+
aux_features: additional static data.
|
| 642 |
+
initial_correlation_times: timescales with units over which autoregressive
|
| 643 |
+
process decorrelates. Typical values in NWP range from hours to days.
|
| 644 |
+
initial_correlation_lengths: lengthscale with units over which random
|
| 645 |
+
field is correlated. Typical values in NWP range from 500-2500 km.
|
| 646 |
+
variances: The average (over EarthSurface) variance of the random field.
|
| 647 |
+
These are fixed arrays (not tunable hk.parameters).
|
| 648 |
+
field_subset: Optional nonempty subset of indices into initial parameters.
|
| 649 |
+
Specifies which fields to construct. If None, use all fields. E.g.,
|
| 650 |
+
field_subset=[0, 5] means form 3 GRFs from the 0th and 5th parameter
|
| 651 |
+
values.
|
| 652 |
+
n_fixed_fields: Number of fields that use fixed parameters. These will
|
| 653 |
+
be fixed at the trailing `n_fixed_fields` initial correlations. The
|
| 654 |
+
total number of fields is unchanged, since these fixed fields replace
|
| 655 |
+
learnable fields.
|
| 656 |
+
clip: number of standard deviations at which to clip randomness to ensure
|
| 657 |
+
numerical stability.
|
| 658 |
+
name: Name to show in xprof.
|
| 659 |
+
"""
|
| 660 |
+
## You must call hk.Module.__init__ before initializing this class.
|
| 661 |
+
hk.Module.__init__(self, name=name)
|
| 662 |
+
|
| 663 |
+
lengths = [
|
| 664 |
+
len(initial_correlation_times),
|
| 665 |
+
len(initial_correlation_lengths),
|
| 666 |
+
len(variances),
|
| 667 |
+
]
|
| 668 |
+
if len(set(lengths)) != 1:
|
| 669 |
+
raise ValueError(f'Argument lengths differed: {lengths=}')
|
| 670 |
+
n_fixed_fields = n_fixed_fields or 0
|
| 671 |
+
|
| 672 |
+
# Get subset of args using `field_subset`
|
| 673 |
+
if field_subset is not None:
|
| 674 |
+
if not field_subset:
|
| 675 |
+
raise ValueError(
|
| 676 |
+
'`field_subset` must be `None` or non-empty sequence. Found'
|
| 677 |
+
f' {field_subset=}'
|
| 678 |
+
)
|
| 679 |
+
get_subset = lambda seq: [seq[i] for i in field_subset]
|
| 680 |
+
initial_correlation_lengths = get_subset(initial_correlation_lengths)
|
| 681 |
+
initial_correlation_times = get_subset(initial_correlation_times)
|
| 682 |
+
variances = get_subset(variances)
|
| 683 |
+
|
| 684 |
+
logging.info(
|
| 685 |
+
'[NGCM] Initializing BatchGaussianRandomFieldModule with'
|
| 686 |
+
f' {initial_correlation_times=}, and {initial_correlation_lengths=},'
|
| 687 |
+
f' and {variances=}'
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
# Get Haiku parameters.
|
| 691 |
+
self._n_fields = len(variances)
|
| 692 |
+
self._variances = jnp.array(
|
| 693 |
+
[nondimensionalize(v, physics_specs) for v in variances]
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
initial_correlation_lengths = jnp.array([ # pyrefly: ignore[bad-assignment]
|
| 697 |
+
nondimensionalize(l, physics_specs) for l in initial_correlation_lengths
|
| 698 |
+
])
|
| 699 |
+
correlation_lengths_raw = hk.get_parameter(
|
| 700 |
+
'correlation_lengths_raw',
|
| 701 |
+
shape=(self.n_fields - n_fixed_fields,),
|
| 702 |
+
init=hk.initializers.Constant(0.0),
|
| 703 |
+
)
|
| 704 |
+
if n_fixed_fields:
|
| 705 |
+
correlation_lengths_raw = jnp.concatenate([
|
| 706 |
+
correlation_lengths_raw, jnp.zeros([n_fixed_fields])])
|
| 707 |
+
self._correlation_lengths = convert_hk_param_to_positive_scalar(
|
| 708 |
+
correlation_lengths_raw, initial_correlation_lengths # pyrefly: ignore[bad-argument-type]
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
initial_correlation_times = jnp.array( # pyrefly: ignore[bad-assignment]
|
| 712 |
+
[nondimensionalize(t, physics_specs) for t in initial_correlation_times]
|
| 713 |
+
)
|
| 714 |
+
correlation_times_raw = hk.get_parameter(
|
| 715 |
+
'correlation_times_raw',
|
| 716 |
+
shape=(self.n_fields - n_fixed_fields,),
|
| 717 |
+
init=hk.initializers.Constant(0.0),
|
| 718 |
+
)
|
| 719 |
+
if n_fixed_fields:
|
| 720 |
+
correlation_times_raw = jnp.concatenate([
|
| 721 |
+
correlation_times_raw, jnp.zeros([n_fixed_fields])])
|
| 722 |
+
self._correlation_times = convert_hk_param_to_positive_scalar(
|
| 723 |
+
correlation_times_raw, initial_correlation_times # pyrefly: ignore[bad-argument-type]
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
def make_rf(correlation_time, correlation_length, variance):
|
| 727 |
+
return GaussianRandomField(
|
| 728 |
+
coords=coords,
|
| 729 |
+
dt=dt,
|
| 730 |
+
physics_specs=physics_specs,
|
| 731 |
+
aux_features=aux_features,
|
| 732 |
+
correlation_time=correlation_time,
|
| 733 |
+
correlation_length=correlation_length,
|
| 734 |
+
variance=variance,
|
| 735 |
+
clip=clip,
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
self._make_rf = make_rf
|
| 739 |
+
|
| 740 |
+
@property
|
| 741 |
+
def n_fields(self) -> int:
|
| 742 |
+
return self._n_fields
|
| 743 |
+
|
| 744 |
+
def unconditional_sample(self, rng: typing.PRNGKeyArray) -> RandomnessState:
|
| 745 |
+
"""Sample the batch GRFs unconditionally."""
|
| 746 |
+
logging.info(
|
| 747 |
+
'[NGCM] Calling BatchGaussianRandomFieldModule.unconditional_sample'
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
def _unconditional_sample_one_rf(
|
| 751 |
+
key, correlation_time, correlation_length, variance
|
| 752 |
+
):
|
| 753 |
+
rf = self._make_rf(correlation_time, correlation_length, variance)
|
| 754 |
+
return rf.unconditional_sample(key)
|
| 755 |
+
|
| 756 |
+
rngs = jax.random.split(rng, self.n_fields + 1)
|
| 757 |
+
rngs, next_rng = rngs[:-1], rngs[-1]
|
| 758 |
+
sample = jax.vmap(_unconditional_sample_one_rf)(
|
| 759 |
+
rngs,
|
| 760 |
+
self._correlation_times,
|
| 761 |
+
self._correlation_lengths,
|
| 762 |
+
self._variances,
|
| 763 |
+
)
|
| 764 |
+
# We have RNG keys and steps associated with each field from vmap, but
|
| 765 |
+
# RandomnessState should only have a single (scalar) RNG key/step.
|
| 766 |
+
return dataclasses.replace(sample, prng_key=next_rng, prng_step=0)
|
| 767 |
+
|
| 768 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 769 |
+
"""Updates the state of the batch of GRFs."""
|
| 770 |
+
logging.info('[NGCM] Calling BatchGaussianRandomFieldModule.advance')
|
| 771 |
+
|
| 772 |
+
def _advance_one_rf(state, correlation_time, correlation_length, variance):
|
| 773 |
+
rf = self._make_rf(correlation_time, correlation_length, variance)
|
| 774 |
+
return rf.advance(state)
|
| 775 |
+
|
| 776 |
+
rng = _prng_key_for_current_advance_step(state)
|
| 777 |
+
rngs = jax.random.split(rng, self.n_fields) # pyrefly: ignore[bad-argument-type]
|
| 778 |
+
steps = jnp.ones(self.n_fields, int) * state.prng_step # pyrefly: ignore[unsupported-operation]
|
| 779 |
+
advanced = jax.vmap(_advance_one_rf)(
|
| 780 |
+
dataclasses.replace(state, prng_key=rngs, prng_step=steps), # pyrefly: ignore[bad-specialization]
|
| 781 |
+
self._correlation_times,
|
| 782 |
+
self._correlation_lengths,
|
| 783 |
+
self._variances,
|
| 784 |
+
)
|
| 785 |
+
return dataclasses.replace(
|
| 786 |
+
advanced, prng_key=state.prng_key, prng_step=state.prng_step + 1 # pyrefly: ignore[unsupported-operation]
|
| 787 |
+
)
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
@gin.register
|
| 791 |
+
class DictOfGaussianRandomFieldModules(hk.Module):
|
| 792 |
+
"""Dictionary of independent GaussianRandomFieldModules.
|
| 793 |
+
|
| 794 |
+
These GRFs are meant to be fed into a neural network as generic "signals".
|
| 795 |
+
"""
|
| 796 |
+
|
| 797 |
+
def __init__(
|
| 798 |
+
self,
|
| 799 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 800 |
+
dt: float,
|
| 801 |
+
physics_specs: Any,
|
| 802 |
+
aux_features: Any,
|
| 803 |
+
initial_correlation_times: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 804 |
+
initial_correlation_lengths: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 805 |
+
variances: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 806 |
+
field_names: Optional[Sequence[str]] = None,
|
| 807 |
+
field_subset: Optional[Sequence[int]] = None,
|
| 808 |
+
clip: float = 6.0,
|
| 809 |
+
name: Optional[str] = None,
|
| 810 |
+
):
|
| 811 |
+
"""Constructs a DictOfGaussianRandomFieldModules.
|
| 812 |
+
|
| 813 |
+
Correlation scales are initialized to `initial_*` args and will be tuned
|
| 814 |
+
by Haiku optimizers. Variance will be fixed.
|
| 815 |
+
|
| 816 |
+
Args:
|
| 817 |
+
coords: horizontal and vertical grid data.
|
| 818 |
+
dt: nondimensionalized model time step.
|
| 819 |
+
physics_specs: physical constants and definition of custom units.
|
| 820 |
+
aux_features: additional static data.
|
| 821 |
+
initial_correlation_times: timescales with units over which autoregressive
|
| 822 |
+
process decorrelates. Typical values in NWP range from hours to days.
|
| 823 |
+
initial_correlation_lengths: lengthscale with units over which random
|
| 824 |
+
field is correlated. Typical values in NWP range from 500-2500 km.
|
| 825 |
+
variances: The average (over EarthSurface) variance of the random field.
|
| 826 |
+
These are fixed arrays (not tunable hk.parameters).
|
| 827 |
+
field_names: Optional names to give the fields. If None, the fields are
|
| 828 |
+
named like "GRF0", "GRF1",...
|
| 829 |
+
field_subset: Optional nonempty subset of indices into initial parameters.
|
| 830 |
+
Specifies which fields to construct. If None, use all fields. E.g.,
|
| 831 |
+
field_subset=[0, 5] means form 3 GRFs from the 0th and 5th parameter
|
| 832 |
+
values.
|
| 833 |
+
clip: number of standard deviations at which to clip randomness to ensure
|
| 834 |
+
numerical stability.
|
| 835 |
+
name: Name to show in xprof.
|
| 836 |
+
"""
|
| 837 |
+
## You must call hk.Module.__init__ before initializing this class.
|
| 838 |
+
hk.Module.__init__(self, name=name)
|
| 839 |
+
logging.info(
|
| 840 |
+
'[NGCM] Initializing DictOfGaussianRandomFieldModules with'
|
| 841 |
+
f' {initial_correlation_times=}, and {initial_correlation_lengths=},'
|
| 842 |
+
f' and {variances=}'
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
field_names = field_names or [
|
| 846 |
+
f'GRF{i}' for i in range(len(initial_correlation_times))
|
| 847 |
+
]
|
| 848 |
+
|
| 849 |
+
lengths = [
|
| 850 |
+
len(initial_correlation_times),
|
| 851 |
+
len(initial_correlation_lengths),
|
| 852 |
+
len(variances),
|
| 853 |
+
len(field_names),
|
| 854 |
+
]
|
| 855 |
+
if len(set(lengths)) != 1:
|
| 856 |
+
raise ValueError(f'Argument lengths differed: {lengths=}')
|
| 857 |
+
|
| 858 |
+
if field_subset is not None:
|
| 859 |
+
if not field_subset:
|
| 860 |
+
raise ValueError(
|
| 861 |
+
'`field_subset` must be `None` or non-empty sequence. Found'
|
| 862 |
+
f' {field_subset=}'
|
| 863 |
+
)
|
| 864 |
+
subset = lambda seq: [seq[i] for i in field_subset]
|
| 865 |
+
field_names = subset(field_names)
|
| 866 |
+
initial_correlation_lengths = subset(initial_correlation_lengths)
|
| 867 |
+
initial_correlation_times = subset(initial_correlation_times)
|
| 868 |
+
variances = subset(variances)
|
| 869 |
+
|
| 870 |
+
self._field_names = tuple(field_names)
|
| 871 |
+
|
| 872 |
+
self._random_fields = {}
|
| 873 |
+
for tau, lam, var, field_name in zip(
|
| 874 |
+
initial_correlation_times,
|
| 875 |
+
initial_correlation_lengths,
|
| 876 |
+
variances,
|
| 877 |
+
self.field_names,
|
| 878 |
+
strict=True,
|
| 879 |
+
):
|
| 880 |
+
self._random_fields[field_name] = GaussianRandomFieldModule(
|
| 881 |
+
coords,
|
| 882 |
+
dt,
|
| 883 |
+
physics_specs,
|
| 884 |
+
aux_features,
|
| 885 |
+
initial_correlation_time=tau,
|
| 886 |
+
initial_correlation_length=lam,
|
| 887 |
+
initial_variance=var,
|
| 888 |
+
tune_variance=False,
|
| 889 |
+
variance_bound=None,
|
| 890 |
+
clip=clip,
|
| 891 |
+
name=field_name,
|
| 892 |
+
)
|
| 893 |
+
|
| 894 |
+
@property
|
| 895 |
+
def n_fields(self) -> int:
|
| 896 |
+
return len(self._random_fields)
|
| 897 |
+
|
| 898 |
+
@property
|
| 899 |
+
def field_names(self) -> tuple[str, ...]:
|
| 900 |
+
return self._field_names
|
| 901 |
+
|
| 902 |
+
def unconditional_sample(self, rng: typing.PRNGKeyArray) -> RandomnessState:
|
| 903 |
+
"""Sample the random field unconditionally."""
|
| 904 |
+
core = {}
|
| 905 |
+
nodal_values = {}
|
| 906 |
+
modal_values = {}
|
| 907 |
+
*rngs, next_rng = jax.random.split(rng, self.n_fields + 1)
|
| 908 |
+
for (name, rf), sample_key in zip(self._random_fields.items(), rngs):
|
| 909 |
+
rvs = rf.unconditional_sample(sample_key)
|
| 910 |
+
core[name] = rvs.core
|
| 911 |
+
nodal_values[name] = rvs.nodal_value
|
| 912 |
+
modal_values[name] = rvs.modal_value
|
| 913 |
+
return RandomnessState(
|
| 914 |
+
core=core, # pyrefly: ignore[unexpected-keyword]
|
| 915 |
+
nodal_value=nodal_values, # pyrefly: ignore[unexpected-keyword]
|
| 916 |
+
modal_value=modal_values, # pyrefly: ignore[unexpected-keyword]
|
| 917 |
+
prng_key=next_rng, # pyrefly: ignore[unexpected-keyword]
|
| 918 |
+
prng_step=0, # pyrefly: ignore[unexpected-keyword]
|
| 919 |
+
)
|
| 920 |
+
|
| 921 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 922 |
+
"""Updates the core state of a random field."""
|
| 923 |
+
core = {}
|
| 924 |
+
nodal_values = {}
|
| 925 |
+
modal_values = {}
|
| 926 |
+
rng = _prng_key_for_current_advance_step(state)
|
| 927 |
+
rngs = jax.random.split(rng, self.n_fields) # pyrefly: ignore[bad-argument-type]
|
| 928 |
+
for (name, rf), sample_key in zip(self._random_fields.items(), rngs):
|
| 929 |
+
# rvs is a RandomnessState.
|
| 930 |
+
rvs = rf.advance(
|
| 931 |
+
RandomnessState(state.core[name], prng_key=sample_key, prng_step=0) # pyrefly: ignore[bad-argument-count, unexpected-keyword, unsupported-operation]
|
| 932 |
+
)
|
| 933 |
+
core[name] = rvs.core
|
| 934 |
+
nodal_values[name] = rvs.nodal_value
|
| 935 |
+
modal_values[name] = rvs.modal_value
|
| 936 |
+
return RandomnessState(
|
| 937 |
+
core=core, # pyrefly: ignore[unexpected-keyword]
|
| 938 |
+
nodal_value=nodal_values, # pyrefly: ignore[unexpected-keyword]
|
| 939 |
+
modal_value=modal_values, # pyrefly: ignore[unexpected-keyword]
|
| 940 |
+
prng_key=state.prng_key, # pyrefly: ignore[unexpected-keyword]
|
| 941 |
+
prng_step=state.prng_step + 1, # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 942 |
+
)
|
| 943 |
+
|
| 944 |
+
|
| 945 |
+
class SumOfRandomFields(RandomField):
|
| 946 |
+
"""RandomField that is the sum of multiple fields."""
|
| 947 |
+
|
| 948 |
+
def __init__(self, random_fields: Sequence[RandomField]):
|
| 949 |
+
self._random_fields = list(random_fields) # Shallow copy
|
| 950 |
+
coords = self._random_fields[0].coords
|
| 951 |
+
if any(rf.coords != coords for rf in self._random_fields):
|
| 952 |
+
raise ValueError(f'All fields must have the same coords. Found {coords=}')
|
| 953 |
+
super().__init__(coords)
|
| 954 |
+
|
| 955 |
+
@property
|
| 956 |
+
def preferred_representation(self) -> PreferredRepresentation | None:
|
| 957 |
+
n_nodal = sum(
|
| 958 |
+
rf.preferred_representation == PreferredRepresentation.NODAL
|
| 959 |
+
for rf in self._random_fields
|
| 960 |
+
)
|
| 961 |
+
n_modal = sum(
|
| 962 |
+
rf.preferred_representation == PreferredRepresentation.MODAL
|
| 963 |
+
for rf in self._random_fields
|
| 964 |
+
)
|
| 965 |
+
if n_nodal > n_modal:
|
| 966 |
+
return PreferredRepresentation.NODAL
|
| 967 |
+
elif n_nodal < n_modal:
|
| 968 |
+
return PreferredRepresentation.MODAL
|
| 969 |
+
return None
|
| 970 |
+
|
| 971 |
+
def unconditional_sample(self, rng: typing.PRNGKeyArray) -> RandomnessState:
|
| 972 |
+
"""Sample the random field unconditionally."""
|
| 973 |
+
rvs = []
|
| 974 |
+
*rngs, next_rng = jax.random.split(rng, len(self._random_fields) + 1)
|
| 975 |
+
for rf, sample_key in zip(self._random_fields, rngs, strict=True):
|
| 976 |
+
rvs.append(rf.unconditional_sample(sample_key).core)
|
| 977 |
+
return RandomnessState(
|
| 978 |
+
core=rvs, # pyrefly: ignore[unexpected-keyword]
|
| 979 |
+
nodal_value=self.to_nodal_values(rvs), # pyrefly: ignore[unexpected-keyword]
|
| 980 |
+
modal_value=self.to_modal_values(rvs), # pyrefly: ignore[unexpected-keyword]
|
| 981 |
+
prng_key=next_rng, # pyrefly: ignore[unexpected-keyword]
|
| 982 |
+
prng_step=0, # pyrefly: ignore[unexpected-keyword]
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
def advance(self, state: RandomnessState) -> RandomnessState:
|
| 986 |
+
"""Updates the core state of a random field."""
|
| 987 |
+
rvs = []
|
| 988 |
+
rng = _prng_key_for_current_advance_step(state)
|
| 989 |
+
rngs = jax.random.split(rng, len(self._random_fields)) # pyrefly: ignore[bad-argument-type]
|
| 990 |
+
for rf, s, k in zip(
|
| 991 |
+
self._random_fields, state.core, rngs, strict=True # pyrefly: ignore[bad-argument-type]
|
| 992 |
+
):
|
| 993 |
+
rs = RandomnessState(s, prng_key=k, prng_step=state.prng_step) # pyrefly: ignore[bad-argument-count, unexpected-keyword]
|
| 994 |
+
rvs.append(rf.advance(rs).core)
|
| 995 |
+
return RandomnessState(
|
| 996 |
+
core=rvs, # pyrefly: ignore[unexpected-keyword]
|
| 997 |
+
nodal_value=self.to_nodal_values(rvs), # pyrefly: ignore[unexpected-keyword]
|
| 998 |
+
modal_value=self.to_modal_values(rvs), # pyrefly: ignore[unexpected-keyword]
|
| 999 |
+
prng_key=state.prng_key, # pyrefly: ignore[unexpected-keyword]
|
| 1000 |
+
prng_step=state.prng_step + 1, # pyrefly: ignore[unexpected-keyword, unsupported-operation]
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
def to_modal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 1004 |
+
"""Finishes `core_state` by summing components."""
|
| 1005 |
+
modal_sum = 0.0
|
| 1006 |
+
nodal_sum = 0.0
|
| 1007 |
+
for rf, s in zip(self._random_fields, core_state, strict=True):
|
| 1008 |
+
if rf.preferred_representation == PreferredRepresentation.NODAL:
|
| 1009 |
+
nodal_sum += rf.to_nodal_values(s) # pyrefly: ignore[unsupported-operation]
|
| 1010 |
+
elif rf.preferred_representation in [PreferredRepresentation.MODAL, None]:
|
| 1011 |
+
modal_sum += rf.to_modal_values(s) # pyrefly: ignore[unsupported-operation]
|
| 1012 |
+
return modal_sum + self.coords.horizontal.to_modal(nodal_sum)
|
| 1013 |
+
|
| 1014 |
+
def to_nodal_values(self, core_state: CoreRandomState) -> typing.Array | None:
|
| 1015 |
+
"""Finishes `core_state` by summing components."""
|
| 1016 |
+
modal_sum = 0.0
|
| 1017 |
+
nodal_sum = 0.0
|
| 1018 |
+
for rf, s in zip(self._random_fields, core_state, strict=True):
|
| 1019 |
+
if rf.preferred_representation == PreferredRepresentation.MODAL:
|
| 1020 |
+
modal_sum += rf.to_modal_values(s) # pyrefly: ignore[unsupported-operation]
|
| 1021 |
+
elif rf.preferred_representation in [PreferredRepresentation.NODAL, None]:
|
| 1022 |
+
nodal_sum += rf.to_nodal_values(s) # pyrefly: ignore[unsupported-operation]
|
| 1023 |
+
return nodal_sum + self.coords.horizontal.to_nodal(modal_sum)
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
class SumOfGaussianLikeRandomFields(SumOfRandomFields, abc.ABC):
|
| 1027 |
+
"""Base class for sum of independent Gaussian-like random fields."""
|
| 1028 |
+
|
| 1029 |
+
def __init__(
|
| 1030 |
+
self,
|
| 1031 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 1032 |
+
dt: float,
|
| 1033 |
+
physics_specs: Any,
|
| 1034 |
+
aux_features: Any,
|
| 1035 |
+
correlation_times: Sequence[
|
| 1036 |
+
Union[jax.Array, Quantity, str]
|
| 1037 |
+
] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1038 |
+
correlation_lengths: Sequence[
|
| 1039 |
+
Union[jax.Array, Quantity, str]
|
| 1040 |
+
] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1041 |
+
variances: Sequence[Union[jax.Array, Quantity, str]] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1042 |
+
clip: float = 6.0,
|
| 1043 |
+
):
|
| 1044 |
+
"""Constructs a SumOfGaussianLikeRandomFields."""
|
| 1045 |
+
n_fields = len(correlation_times)
|
| 1046 |
+
variances = variances or [None] * n_fields
|
| 1047 |
+
random_fields = []
|
| 1048 |
+
logging.info(
|
| 1049 |
+
'[NGCM] Initializing SumOfGaussianLikeRandomFields with '
|
| 1050 |
+
f'{variances=}, {correlation_times=}, {correlation_lengths=}'
|
| 1051 |
+
)
|
| 1052 |
+
for tau, lam, var in zip(
|
| 1053 |
+
correlation_times, correlation_lengths, variances, strict=True
|
| 1054 |
+
):
|
| 1055 |
+
random_fields.append(
|
| 1056 |
+
self.get_cls_constructor()(
|
| 1057 |
+
coords,
|
| 1058 |
+
dt,
|
| 1059 |
+
physics_specs,
|
| 1060 |
+
aux_features,
|
| 1061 |
+
correlation_time=tau,
|
| 1062 |
+
correlation_length=lam,
|
| 1063 |
+
variance=var,
|
| 1064 |
+
clip=clip,
|
| 1065 |
+
)
|
| 1066 |
+
)
|
| 1067 |
+
|
| 1068 |
+
super().__init__(random_fields)
|
| 1069 |
+
|
| 1070 |
+
@abc.abstractmethod
|
| 1071 |
+
def get_cls_constructor(self) -> type[GaussianRandomField]:
|
| 1072 |
+
"""Gets class constructor that is initialized with Gaussian-like kwargs."""
|
| 1073 |
+
|
| 1074 |
+
|
| 1075 |
+
class SumOfGaussianLikeRandomFieldsModule(
|
| 1076 |
+
SumOfRandomFields, hk.Module, abc.ABC
|
| 1077 |
+
):
|
| 1078 |
+
"""Base class for sums of independent Gaussian-like RandomFieldModules."""
|
| 1079 |
+
|
| 1080 |
+
def __init__(
|
| 1081 |
+
self,
|
| 1082 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 1083 |
+
dt: float,
|
| 1084 |
+
physics_specs: Any,
|
| 1085 |
+
aux_features: Any,
|
| 1086 |
+
initial_correlation_times: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1087 |
+
initial_correlation_lengths: Sequence[Quantity | str] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1088 |
+
initial_variances: Optional[Sequence[Quantity | str]] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1089 |
+
variance_bounds: Optional[Sequence[Quantity | str]] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 1090 |
+
clip: float = 6.0,
|
| 1091 |
+
name: Optional[str] = None,
|
| 1092 |
+
):
|
| 1093 |
+
"""Constructs a SumOfGaussianLikeRandomFieldsModule."""
|
| 1094 |
+
# You must call hk.Module.__init__ before initializing this class.
|
| 1095 |
+
hk.Module.__init__(self, name=name)
|
| 1096 |
+
|
| 1097 |
+
n_fields = len(initial_correlation_times)
|
| 1098 |
+
initial_variances = initial_variances or [None] * n_fields
|
| 1099 |
+
variance_bounds = variance_bounds or [None] * n_fields
|
| 1100 |
+
random_fields = []
|
| 1101 |
+
for tau, lam, var, bound in zip(
|
| 1102 |
+
initial_correlation_times,
|
| 1103 |
+
initial_correlation_lengths,
|
| 1104 |
+
initial_variances,
|
| 1105 |
+
variance_bounds,
|
| 1106 |
+
strict=True,
|
| 1107 |
+
):
|
| 1108 |
+
random_fields.append(
|
| 1109 |
+
self.get_cls_constructor()(
|
| 1110 |
+
coords,
|
| 1111 |
+
dt,
|
| 1112 |
+
physics_specs,
|
| 1113 |
+
aux_features,
|
| 1114 |
+
initial_correlation_time=tau,
|
| 1115 |
+
initial_correlation_length=lam,
|
| 1116 |
+
initial_variance=var,
|
| 1117 |
+
variance_bound=bound,
|
| 1118 |
+
clip=clip,
|
| 1119 |
+
name=name,
|
| 1120 |
+
)
|
| 1121 |
+
)
|
| 1122 |
+
# We call SumOfRandomFields.__init__ rather than super().__init__
|
| 1123 |
+
# since we don't want to call hk.Module.__init__ twice... although doing
|
| 1124 |
+
# that didn't hurt anything.
|
| 1125 |
+
SumOfRandomFields.__init__(self, random_fields)
|
| 1126 |
+
|
| 1127 |
+
@abc.abstractmethod
|
| 1128 |
+
def get_cls_constructor(self) -> type[GaussianRandomFieldModule]:
|
| 1129 |
+
"""Gets class constructor that is initialized with Gaussian-like kwargs."""
|
| 1130 |
+
|
| 1131 |
+
|
| 1132 |
+
@gin.register
|
| 1133 |
+
class SumOfGaussianRandomFields(SumOfGaussianLikeRandomFields):
|
| 1134 |
+
|
| 1135 |
+
def get_cls_constructor(self) -> type[GaussianRandomField]:
|
| 1136 |
+
return GaussianRandomField
|
| 1137 |
+
|
| 1138 |
+
|
| 1139 |
+
@gin.register
|
| 1140 |
+
class SumOfGaussianRandomFieldsModule(SumOfGaussianLikeRandomFieldsModule):
|
| 1141 |
+
"""A sum of independent GaussianRandomFieldModules."""
|
| 1142 |
+
|
| 1143 |
+
def get_cls_constructor(self) -> type[GaussianRandomFieldModule]:
|
| 1144 |
+
return GaussianRandomFieldModule
|
| 1145 |
+
|
| 1146 |
+
|
| 1147 |
+
@gin.register
|
| 1148 |
+
class SumOfCenteredLognormalRandomFields(SumOfGaussianLikeRandomFields):
|
| 1149 |
+
|
| 1150 |
+
def get_cls_constructor(self) -> type[CenteredLognormalRandomField]:
|
| 1151 |
+
return CenteredLognormalRandomField
|
| 1152 |
+
|
| 1153 |
+
|
| 1154 |
+
@gin.register
|
| 1155 |
+
class SumOfCenteredLognormalRandomFieldsModule(
|
| 1156 |
+
SumOfGaussianLikeRandomFieldsModule
|
| 1157 |
+
):
|
| 1158 |
+
"""A sum of independent CenteredLognormalRandomFieldModules."""
|
| 1159 |
+
|
| 1160 |
+
def get_cls_constructor(self) -> type[CenteredLognormalRandomFieldModule]:
|
| 1161 |
+
return CenteredLognormalRandomFieldModule
|
| 1162 |
+
|
| 1163 |
+
|
| 1164 |
+
################################################################################
|
| 1165 |
+
# Helper functions for creating fields.
|
| 1166 |
+
################################################################################
|
| 1167 |
+
|
| 1168 |
+
|
| 1169 |
+
def convert_hk_param_to_positive_scalar(
|
| 1170 |
+
param: jax.Array,
|
| 1171 |
+
initial_value: Numeric,
|
| 1172 |
+
) -> jax.Array:
|
| 1173 |
+
"""Converts [batch] scalar parameter to scalar value using Softplus."""
|
| 1174 |
+
return initial_value * make_positive_scalar(param) # pyrefly: ignore[bad-return]
|
| 1175 |
+
|
| 1176 |
+
|
| 1177 |
+
def _sigmoid(low: Numeric, high: Numeric, x: jax.Array) -> jax.Array:
|
| 1178 |
+
"""Numerically stable sigmoid, adapted from tfp.bijectors.Sigmoid."""
|
| 1179 |
+
diff = high - low
|
| 1180 |
+
left = low + diff * jax.nn.sigmoid(x)
|
| 1181 |
+
right = high - diff * jax.nn.sigmoid(-x)
|
| 1182 |
+
return jnp.where(x < 0, left, right)
|
| 1183 |
+
|
| 1184 |
+
|
| 1185 |
+
def _inv_sigmoid(low: Numeric, high: Numeric, x: jax.Array) -> jax.Array:
|
| 1186 |
+
"""Inverse sigmoid, adapted from tfp.bijectors.Sigmoid."""
|
| 1187 |
+
return jnp.log(x - low) - jnp.log(high - x)
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
+
def convert_hk_param_to_bounded_scalar(
|
| 1191 |
+
param: jax.Array,
|
| 1192 |
+
initial_value: Numeric,
|
| 1193 |
+
low: Numeric,
|
| 1194 |
+
high: Numeric,
|
| 1195 |
+
) -> jax.Array:
|
| 1196 |
+
"""Converts a [batch] scalar parameter to scalar value using Sigmoid."""
|
| 1197 |
+
offset = _inv_sigmoid(low, high, initial_value) # pyrefly: ignore[bad-argument-type]
|
| 1198 |
+
return _sigmoid(low, high, offset + param)
|
| 1199 |
+
|
| 1200 |
+
|
| 1201 |
+
def nondimensionalize(
|
| 1202 |
+
x: Union[typing.Numeric, Quantity, str],
|
| 1203 |
+
physics_specs: Any,
|
| 1204 |
+
) -> typing.Numeric:
|
| 1205 |
+
if isinstance(x, (Quantity, str)): # pyrefly: ignore[invalid-argument]
|
| 1206 |
+
return physics_specs.nondimensionalize(Quantity(x))
|
| 1207 |
+
else:
|
| 1208 |
+
return x
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
def maybe_nondimensionalize(
|
| 1212 |
+
x: Optional[Union[typing.Numeric, Quantity, str]],
|
| 1213 |
+
physics_specs: Any,
|
| 1214 |
+
) -> None | typing.Numeric:
|
| 1215 |
+
"""Calls nondimensionalize on Quantity or str, otherwise passthrough."""
|
| 1216 |
+
if x == 'None': # Allow strings for gin
|
| 1217 |
+
return None
|
| 1218 |
+
return nondimensionalize(x, physics_specs)
|
| 1219 |
+
|
| 1220 |
+
|
| 1221 |
+
def _assert_positive_or_none(x: typing.Numeric | None, name: str) -> None:
|
| 1222 |
+
if x is None:
|
| 1223 |
+
return
|
| 1224 |
+
if x <= 0:
|
| 1225 |
+
raise ValueError(f'{name}={x} but should have been positive or None')
|
model/legacy/towers.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Basic neural network towers for whirl/gcm codebase.
|
| 15 |
+
|
| 16 |
+
A tower is a neural network that operates identically over the last two
|
| 17 |
+
dimensions, i.e. (longitude, latitude).
|
| 18 |
+
"""
|
| 19 |
+
from collections import abc
|
| 20 |
+
from typing import Callable, Optional, Tuple
|
| 21 |
+
from dinosaur import typing
|
| 22 |
+
import gin
|
| 23 |
+
import haiku as hk
|
| 24 |
+
import jax
|
| 25 |
+
import jax.numpy as jnp
|
| 26 |
+
from model.legacy import layers
|
| 27 |
+
|
| 28 |
+
Array = typing.Array
|
| 29 |
+
TowerFactory = typing.TowerFactory
|
| 30 |
+
LayerFactory = typing.LayerFactory
|
| 31 |
+
MLP = gin.external_configurable(hk.nets.MLP)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@gin.register(denylist=['output_size'])
|
| 35 |
+
class ColumnTower(hk.Module):
|
| 36 |
+
"""Column tower module parameterized by column_net_factory."""
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
output_size: int,
|
| 41 |
+
column_net_factory: LayerFactory = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 42 |
+
checkpoint_tower: bool = False,
|
| 43 |
+
name: Optional[str] = None,
|
| 44 |
+
):
|
| 45 |
+
"""Tower that maps a column_net over two spatial dimensions."""
|
| 46 |
+
super().__init__(name=name)
|
| 47 |
+
column_net = column_net_factory(output_size)
|
| 48 |
+
vmap_last = lambda fn: hk.vmap(fn, in_axes=-1, out_axes=-1, split_rng=False)
|
| 49 |
+
column_tower = vmap_last(vmap_last(column_net))
|
| 50 |
+
if checkpoint_tower:
|
| 51 |
+
column_tower = hk.remat(column_tower)
|
| 52 |
+
self.column_tower = column_tower
|
| 53 |
+
|
| 54 |
+
def __call__(self, inputs: Array) -> Array:
|
| 55 |
+
"""Applies Column tower to inputs."""
|
| 56 |
+
return self.column_tower(inputs)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@gin.register(denylist=['output_size'])
|
| 60 |
+
class ColumnTransformerTower(ColumnTower):
|
| 61 |
+
"""Same as ColumnTower, but passes additional transformer inputs."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, *args, **kwargs):
|
| 64 |
+
super().__init__(*args, **kwargs)
|
| 65 |
+
|
| 66 |
+
def __call__(
|
| 67 |
+
self,
|
| 68 |
+
inputs: Array,
|
| 69 |
+
latents: Optional[Array] = None,
|
| 70 |
+
positional_encoding: Optional[Array] = None,
|
| 71 |
+
) -> Array:
|
| 72 |
+
"""Applies Column tower to inputs."""
|
| 73 |
+
return self.column_tower(inputs, latents, positional_encoding)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@gin.register(denylist=['output_size'])
|
| 77 |
+
class VerticalConvTower(hk.Module):
|
| 78 |
+
"""Tower that stacks up layers of Conv1D.
|
| 79 |
+
|
| 80 |
+
input shape: [in_channel, level, lon, lat],
|
| 81 |
+
output shape: [output_size, level, lon, lat].
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
def __init__(
|
| 85 |
+
self,
|
| 86 |
+
output_size: int, # The number of channels in the last layer
|
| 87 |
+
channels: abc.Sequence[int] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 88 |
+
kernel_shape: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 89 |
+
with_bias: bool = True,
|
| 90 |
+
activation: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.relu,
|
| 91 |
+
activate_final: bool = False,
|
| 92 |
+
checkpoint_tower: bool = False,
|
| 93 |
+
name: Optional[str] = None,
|
| 94 |
+
):
|
| 95 |
+
super().__init__(name=name)
|
| 96 |
+
self.activation = activation
|
| 97 |
+
self.output_size = output_size
|
| 98 |
+
self.activate_final = activate_final
|
| 99 |
+
self.checkpoint_tower = checkpoint_tower
|
| 100 |
+
|
| 101 |
+
self.layers = []
|
| 102 |
+
channels = list(channels) + [self.output_size]
|
| 103 |
+
for channels_i in channels:
|
| 104 |
+
self.layers.append(layers.ConvLevel(
|
| 105 |
+
output_channels=channels_i,
|
| 106 |
+
kernel_shape=kernel_shape,
|
| 107 |
+
with_bias=with_bias))
|
| 108 |
+
|
| 109 |
+
def net(self, inputs: Array) -> Array:
|
| 110 |
+
out = inputs
|
| 111 |
+
num_layers = len(self.layers)
|
| 112 |
+
for i, layer in enumerate(self.layers):
|
| 113 |
+
out = layer(out)
|
| 114 |
+
if i < (num_layers - 1) or self.activate_final:
|
| 115 |
+
out = self.activation(out)
|
| 116 |
+
return out
|
| 117 |
+
|
| 118 |
+
def __call__(self, inputs: Array) -> Array:
|
| 119 |
+
vmap_last = lambda fn: hk.vmap(fn, in_axes=-1, out_axes=-1, split_rng=False)
|
| 120 |
+
tower_fn = vmap_last(vmap_last(self.net))
|
| 121 |
+
if self.checkpoint_tower:
|
| 122 |
+
tower_fn = hk.remat(tower_fn)
|
| 123 |
+
return tower_fn(inputs)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@gin.register(denylist=['output_size'])
|
| 127 |
+
class Conv2DTower(hk.Module):
|
| 128 |
+
"""Two dimensional ConvNet tower module."""
|
| 129 |
+
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
output_size: int,
|
| 133 |
+
num_hidden_units: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 134 |
+
num_hidden_layers: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 135 |
+
kernel_shape: Tuple[int, int] = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 136 |
+
with_bias: bool = True,
|
| 137 |
+
activation: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.relu,
|
| 138 |
+
activate_final: bool = False,
|
| 139 |
+
name: Optional[str] = None,
|
| 140 |
+
):
|
| 141 |
+
"""Tower that stacks up layers of ConvLonLat."""
|
| 142 |
+
super().__init__(name=name)
|
| 143 |
+
self.activation = activation
|
| 144 |
+
self.activate_final = activate_final
|
| 145 |
+
|
| 146 |
+
output_sizes = [num_hidden_units] * num_hidden_layers + [output_size]
|
| 147 |
+
self.layers = []
|
| 148 |
+
for output_size in output_sizes:
|
| 149 |
+
self.layers.append(layers.ConvLonLat(
|
| 150 |
+
output_size=output_size,
|
| 151 |
+
kernel_shape=kernel_shape,
|
| 152 |
+
with_bias=with_bias))
|
| 153 |
+
|
| 154 |
+
def __call__(self, inputs: Array) -> Array:
|
| 155 |
+
"""Applies ConvNet tower to inputs."""
|
| 156 |
+
num_layers = len(self.layers)
|
| 157 |
+
out = inputs
|
| 158 |
+
for i, layer in enumerate(self.layers):
|
| 159 |
+
out = layer(out)
|
| 160 |
+
if i < (num_layers - 1) or self.activate_final:
|
| 161 |
+
out = self.activation(out)
|
| 162 |
+
return out
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
@gin.register(denylist=['output_size'])
|
| 166 |
+
class EpdTower(hk.Module):
|
| 167 |
+
"""EPD tower module parameterized by encode/process/decode factories."""
|
| 168 |
+
|
| 169 |
+
def __init__(
|
| 170 |
+
self,
|
| 171 |
+
output_size: int,
|
| 172 |
+
latent_size: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 173 |
+
num_process_blocks: int = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 174 |
+
encode_tower_factory: TowerFactory = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 175 |
+
process_tower_factory: TowerFactory = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 176 |
+
decode_tower_factory: TowerFactory = gin.REQUIRED, # pyrefly: ignore[bad-function-definition]
|
| 177 |
+
post_encode_activation: Optional[Callable[[Array], Array]] = None,
|
| 178 |
+
pre_decode_activation: Optional[Callable[[Array], Array]] = None,
|
| 179 |
+
final_activation: Optional[Callable[[jnp.ndarray], jnp.ndarray]] = None,
|
| 180 |
+
name: Optional[str] = None,
|
| 181 |
+
):
|
| 182 |
+
super().__init__(name=name)
|
| 183 |
+
self.output_size = output_size
|
| 184 |
+
self.latent_size = latent_size
|
| 185 |
+
self.num_process_blocks = num_process_blocks
|
| 186 |
+
self.encode_tower_factory = encode_tower_factory
|
| 187 |
+
self.process_tower_factory = process_tower_factory
|
| 188 |
+
self.decode_tower_factory = decode_tower_factory
|
| 189 |
+
self.post_encode_activation = post_encode_activation
|
| 190 |
+
self.pre_decode_activation = pre_decode_activation
|
| 191 |
+
self.final_activation = final_activation
|
| 192 |
+
|
| 193 |
+
def __call__(self, inputs: Array) -> Array:
|
| 194 |
+
"""Applies EPD tower to inputs."""
|
| 195 |
+
encoded = self.encode_tower_factory(self.latent_size)(inputs)
|
| 196 |
+
if self.post_encode_activation is not None:
|
| 197 |
+
encoded = self.post_encode_activation(encoded)
|
| 198 |
+
current = encoded
|
| 199 |
+
for _ in range(self.num_process_blocks):
|
| 200 |
+
current = current + self.process_tower_factory(self.latent_size)(current)
|
| 201 |
+
if self.pre_decode_activation is not None:
|
| 202 |
+
current = self.pre_decode_activation(current)
|
| 203 |
+
out = self.decode_tower_factory(self.output_size)(current)
|
| 204 |
+
if self.final_activation is not None:
|
| 205 |
+
return self.final_activation(out)
|
| 206 |
+
return out
|
model/legacy/transforms.py
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Transformation modules that convert or pre/post process data structures."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
import functools
|
| 18 |
+
import re
|
| 19 |
+
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple
|
| 20 |
+
from dinosaur import coordinate_systems
|
| 21 |
+
from dinosaur import pytree_utils
|
| 22 |
+
from dinosaur import sigma_coordinates
|
| 23 |
+
from dinosaur import typing
|
| 24 |
+
import gin
|
| 25 |
+
import haiku as hk
|
| 26 |
+
import jax
|
| 27 |
+
import jax.numpy as jnp
|
| 28 |
+
from model.legacy import filters
|
| 29 |
+
import numpy as np
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
KeyWithCosLatFactor = typing.KeyWithCosLatFactor
|
| 33 |
+
TransformModule = typing.TransformModule
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@gin.register
|
| 37 |
+
class EmptyTransform(hk.Module):
|
| 38 |
+
"""Transform returns an empty dict."""
|
| 39 |
+
|
| 40 |
+
def __init__(self, *args, name: Optional[str] = None):
|
| 41 |
+
del args # unused.
|
| 42 |
+
super().__init__(name=name)
|
| 43 |
+
|
| 44 |
+
def __call__(self, inputs) -> typing.Pytree:
|
| 45 |
+
return {}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@gin.register
|
| 49 |
+
class IdentityTransform(hk.Module):
|
| 50 |
+
"""Transform does not modify inputs."""
|
| 51 |
+
|
| 52 |
+
def __init__(self, *args, name: Optional[str] = None, **kwargs):
|
| 53 |
+
del args, kwargs # unused.
|
| 54 |
+
super().__init__(name=name)
|
| 55 |
+
|
| 56 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 57 |
+
return inputs
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@gin.register
|
| 61 |
+
class ShiftAndNormalize(hk.Module):
|
| 62 |
+
"""Transforms inputs by shifting and normalizing values by `shifts/scales`."""
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 67 |
+
dt: float,
|
| 68 |
+
physics_specs: Any,
|
| 69 |
+
aux_features: typing.AuxFeatures,
|
| 70 |
+
shifts: typing.Pytree,
|
| 71 |
+
scales: typing.Pytree,
|
| 72 |
+
features_to_exclude: Sequence[str] = tuple(),
|
| 73 |
+
global_scale: Optional[float] = None,
|
| 74 |
+
name: Optional[str] = None,
|
| 75 |
+
):
|
| 76 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 77 |
+
super().__init__(name=name)
|
| 78 |
+
self.shifts = shifts
|
| 79 |
+
if global_scale is not None:
|
| 80 |
+
scales = jax.tree_util.tree_map(lambda x: x * global_scale, scales)
|
| 81 |
+
self.scales = scales
|
| 82 |
+
|
| 83 |
+
def __call__(self, inputs):
|
| 84 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 85 |
+
shifts = pytree_utils.replace_with_matching_or_default(
|
| 86 |
+
inputs, self.shifts, default=None, check_used_all_replace_keys=False)
|
| 87 |
+
scales = pytree_utils.replace_with_matching_or_default(
|
| 88 |
+
inputs, self.scales, default=None, check_used_all_replace_keys=False)
|
| 89 |
+
# if shifts/scales have missing values present in `inputs`, we insert `None`
|
| 90 |
+
# for the default. If corresponding `inputs` is not `None`, this will raise
|
| 91 |
+
# an error, as expected. This works because tree_map skips `None` values in
|
| 92 |
+
# the first argument, as long as all dictionary keys match.
|
| 93 |
+
result = jax.tree_util.tree_map(
|
| 94 |
+
lambda x, y, z: (x - y) / z, inputs, shifts, scales)
|
| 95 |
+
return from_dict_fn(result)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@gin.register
|
| 99 |
+
class InverseShiftAndNormalize(hk.Module):
|
| 100 |
+
"""Inverse of the `ShiftAndNormalize` for the same `shifts/scales`."""
|
| 101 |
+
|
| 102 |
+
def __init__(
|
| 103 |
+
self,
|
| 104 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 105 |
+
dt: float,
|
| 106 |
+
physics_specs: Any,
|
| 107 |
+
aux_features: typing.AuxFeatures,
|
| 108 |
+
shifts: typing.Pytree,
|
| 109 |
+
scales: typing.Pytree,
|
| 110 |
+
global_scale: Optional[float] = None,
|
| 111 |
+
name: Optional[str] = None,
|
| 112 |
+
):
|
| 113 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 114 |
+
super().__init__(name=name)
|
| 115 |
+
self.shifts = shifts
|
| 116 |
+
if global_scale is not None:
|
| 117 |
+
scales = jax.tree_util.tree_map(lambda x: x * global_scale, scales)
|
| 118 |
+
self.scales = scales
|
| 119 |
+
|
| 120 |
+
def __call__(self, inputs):
|
| 121 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 122 |
+
shifts = pytree_utils.replace_with_matching_or_default(
|
| 123 |
+
inputs, self.shifts, default=None, check_used_all_replace_keys=False)
|
| 124 |
+
scales = pytree_utils.replace_with_matching_or_default(
|
| 125 |
+
inputs, self.scales, default=None, check_used_all_replace_keys=False)
|
| 126 |
+
# if shifts/scales have missing values present in `inputs`, we insert `None`
|
| 127 |
+
# for the default. If corresponding `inputs` is not `None`, this will raise
|
| 128 |
+
# an error, as expected. This works because tree_map skips `None` values in
|
| 129 |
+
# the first argument, as long as all dictionary keys match.
|
| 130 |
+
result = jax.tree_util.tree_map(
|
| 131 |
+
lambda x, y, z: (None if x is None else x * z + y),
|
| 132 |
+
inputs,
|
| 133 |
+
shifts,
|
| 134 |
+
scales,
|
| 135 |
+
is_leaf=lambda x: x is None,
|
| 136 |
+
)
|
| 137 |
+
return from_dict_fn(result)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@gin.register
|
| 141 |
+
class ToModalWithDivCurlTransform(hk.Module):
|
| 142 |
+
"""Module that converts inputs to modal replacing velocity with div/curl."""
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 147 |
+
dt: float,
|
| 148 |
+
physics_specs: Any,
|
| 149 |
+
aux_features: typing.AuxFeatures,
|
| 150 |
+
name: Optional[str] = None,
|
| 151 |
+
):
|
| 152 |
+
del dt, physics_specs, aux_features # unused.
|
| 153 |
+
super().__init__(name=name)
|
| 154 |
+
self.coords = coords
|
| 155 |
+
|
| 156 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 157 |
+
if 'u' not in inputs or 'v' not in inputs:
|
| 158 |
+
raise ValueError('Inputs to ToModalWithDivCurlTransform must include `u, '
|
| 159 |
+
f'v`, got keys: {inputs.keys()}')
|
| 160 |
+
sec_lat = 1 / self.coords.horizontal.cos_lat
|
| 161 |
+
u, v = inputs.pop('u'), inputs.pop('v')
|
| 162 |
+
# here u,v stand for velocity / cos(lat), but the cos(lat) is cancelled in
|
| 163 |
+
# divergence and curl operators below.
|
| 164 |
+
inputs['u'] = u * sec_lat
|
| 165 |
+
inputs['v'] = v * sec_lat
|
| 166 |
+
to_modal_fn = lambda x: (self.coords.horizontal.to_modal(x) # pylint: disable=g-long-lambda
|
| 167 |
+
if x is not None else None)
|
| 168 |
+
modal_outputs = jax.tree_util.tree_map(to_modal_fn, inputs)
|
| 169 |
+
u, v = modal_outputs.pop('u'), modal_outputs.pop('v')
|
| 170 |
+
modal_outputs['divergence'] = self.coords.horizontal.div_cos_lat((u, v))
|
| 171 |
+
modal_outputs['vorticity'] = self.coords.horizontal.curl_cos_lat((u, v))
|
| 172 |
+
return modal_outputs
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@gin.register
|
| 176 |
+
class ToModalDiffOperators(hk.Module):
|
| 177 |
+
"""Module that returns grad and laplacian features of inputs fields.
|
| 178 |
+
|
| 179 |
+
To avoid accidental accumulation of the cos(lat) factors, features must be
|
| 180 |
+
keyed using typing.KeyWithCosLatFactor namedtuple.
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
def __init__(
|
| 184 |
+
self,
|
| 185 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 186 |
+
dt: float,
|
| 187 |
+
physics_specs: Any,
|
| 188 |
+
aux_features: typing.AuxFeatures,
|
| 189 |
+
name: Optional[str] = None,
|
| 190 |
+
):
|
| 191 |
+
del dt, physics_specs, aux_features # unused.
|
| 192 |
+
super().__init__(name=name)
|
| 193 |
+
self.coords = coords
|
| 194 |
+
|
| 195 |
+
def __call__(
|
| 196 |
+
self,
|
| 197 |
+
inputs: Mapping[typing.KeyWithCosLatFactor, typing.Array],
|
| 198 |
+
) -> Mapping[typing.KeyWithCosLatFactor, typing.Array]:
|
| 199 |
+
features = {}
|
| 200 |
+
for k, value in inputs.items():
|
| 201 |
+
name, cos_lat_order = k.name, k.factor_order
|
| 202 |
+
d_value_dlon, d_value_dlat = self.coords.horizontal.cos_lat_grad(value)
|
| 203 |
+
laplacian_value = self.coords.horizontal.laplacian(value)
|
| 204 |
+
dlon_key = typing.KeyWithCosLatFactor(name + '_dlon', cos_lat_order + 1)
|
| 205 |
+
dlat_key = typing.KeyWithCosLatFactor(name + '_dlat', cos_lat_order + 1)
|
| 206 |
+
del2_key = typing.KeyWithCosLatFactor(name + '_del2', cos_lat_order)
|
| 207 |
+
features[dlon_key] = d_value_dlon
|
| 208 |
+
features[dlat_key] = d_value_dlat
|
| 209 |
+
features[del2_key] = laplacian_value
|
| 210 |
+
return features
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@gin.register
|
| 214 |
+
class ModalToNodalTransform(hk.Module):
|
| 215 |
+
"""Transform that converts modal inputs to nodal representation."""
|
| 216 |
+
|
| 217 |
+
def __init__(
|
| 218 |
+
self,
|
| 219 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 220 |
+
dt: float,
|
| 221 |
+
physics_specs: Any,
|
| 222 |
+
aux_features: typing.AuxFeatures,
|
| 223 |
+
name: Optional[str] = None,
|
| 224 |
+
):
|
| 225 |
+
del dt, physics_specs, aux_features # unused.
|
| 226 |
+
super().__init__(name=name)
|
| 227 |
+
self.coords = coords
|
| 228 |
+
|
| 229 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 230 |
+
return self.coords.horizontal.to_nodal(inputs)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@gin.register
|
| 234 |
+
class NodalToModalTransform(hk.Module):
|
| 235 |
+
"""Transform that converts nodal inputs to modal representation."""
|
| 236 |
+
|
| 237 |
+
def __init__(
|
| 238 |
+
self,
|
| 239 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 240 |
+
dt: float,
|
| 241 |
+
physics_specs: Any,
|
| 242 |
+
aux_features: typing.AuxFeatures,
|
| 243 |
+
name: Optional[str] = None,
|
| 244 |
+
):
|
| 245 |
+
del dt, physics_specs, aux_features # unused.
|
| 246 |
+
super().__init__(name=name)
|
| 247 |
+
self.coords = coords
|
| 248 |
+
|
| 249 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 250 |
+
return self.coords.horizontal.to_modal(inputs)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@gin.register
|
| 254 |
+
class ClipTransform(hk.Module):
|
| 255 |
+
"""Transform that clips highest total wavenumber in inputs."""
|
| 256 |
+
|
| 257 |
+
def __init__(
|
| 258 |
+
self,
|
| 259 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 260 |
+
dt: float,
|
| 261 |
+
physics_specs: Any,
|
| 262 |
+
aux_features: typing.AuxFeatures,
|
| 263 |
+
wavenumbers_to_clip: int = 1,
|
| 264 |
+
name: Optional[str] = None,
|
| 265 |
+
):
|
| 266 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 267 |
+
del dt, physics_specs, aux_features # unused.
|
| 268 |
+
super().__init__(name=name)
|
| 269 |
+
self.coords = coords
|
| 270 |
+
self.wavenumbers_to_clip = wavenumbers_to_clip
|
| 271 |
+
|
| 272 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 273 |
+
return self.coords.horizontal.clip_wavenumbers(
|
| 274 |
+
inputs, self.wavenumbers_to_clip
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@gin.register
|
| 279 |
+
class NondimensionalizeTransform(hk.Module):
|
| 280 |
+
"""Transform that nondimensionalizes inputs."""
|
| 281 |
+
|
| 282 |
+
def __init__(
|
| 283 |
+
self,
|
| 284 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 285 |
+
dt: float,
|
| 286 |
+
physics_specs: Any,
|
| 287 |
+
aux_features: typing.AuxFeatures,
|
| 288 |
+
input_coords: coordinate_systems.CoordinateSystem,
|
| 289 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 290 |
+
name: Optional[str] = None,
|
| 291 |
+
):
|
| 292 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 293 |
+
del coords, dt, aux_features, input_coords # unused.
|
| 294 |
+
super().__init__(name=name)
|
| 295 |
+
self.inputs_to_units_mapping = inputs_to_units_mapping
|
| 296 |
+
self.nondimensionalize = physics_specs.nondimensionalize
|
| 297 |
+
|
| 298 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 299 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 300 |
+
inputs_to_units_mapping = pytree_utils.replace_with_matching_or_default(
|
| 301 |
+
inputs, self.inputs_to_units_mapping, default=None,
|
| 302 |
+
check_used_all_replace_keys=False,
|
| 303 |
+
)
|
| 304 |
+
nondim_fn = lambda x, y: self.nondimensionalize(x * typing.Quantity(y))
|
| 305 |
+
result = jax.tree_util.tree_map(nondim_fn, inputs, inputs_to_units_mapping)
|
| 306 |
+
return from_dict_fn(result)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@gin.register
|
| 310 |
+
class RedimensionalizeTransform(hk.Module):
|
| 311 |
+
"""Transform that redimensionalizes inputs."""
|
| 312 |
+
|
| 313 |
+
def __init__(
|
| 314 |
+
self,
|
| 315 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 316 |
+
dt: float,
|
| 317 |
+
physics_specs: Any,
|
| 318 |
+
aux_features: typing.AuxFeatures,
|
| 319 |
+
output_coords: coordinate_systems.CoordinateSystem,
|
| 320 |
+
inputs_to_units_mapping: Dict[str, str],
|
| 321 |
+
name: Optional[str] = None,
|
| 322 |
+
):
|
| 323 |
+
"""See `time_integration.exponential_filter` for details."""
|
| 324 |
+
del coords, dt, aux_features, output_coords # unused.
|
| 325 |
+
super().__init__(name=name)
|
| 326 |
+
self.inputs_to_units_mapping = inputs_to_units_mapping
|
| 327 |
+
self.dimensionalize = physics_specs.dimensionalize
|
| 328 |
+
|
| 329 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 330 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 331 |
+
inputs_to_units_mapping = pytree_utils.replace_with_matching_or_default(
|
| 332 |
+
inputs, self.inputs_to_units_mapping, default=None,
|
| 333 |
+
check_used_all_replace_keys=False,
|
| 334 |
+
)
|
| 335 |
+
dim_fn = lambda x, y: self.dimensionalize(x, typing.Quantity(y)).m
|
| 336 |
+
result = jax.tree_util.tree_map(dim_fn, inputs, inputs_to_units_mapping)
|
| 337 |
+
return from_dict_fn(result)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@gin.register
|
| 341 |
+
class SequentialTransform(hk.Module):
|
| 342 |
+
"""Transform module that combines multiple transforms applied sequentially."""
|
| 343 |
+
|
| 344 |
+
def __init__(
|
| 345 |
+
self,
|
| 346 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 347 |
+
dt: float,
|
| 348 |
+
physics_specs: Any,
|
| 349 |
+
aux_features: typing.AuxFeatures,
|
| 350 |
+
transform_modules: Sequence[TransformModule],
|
| 351 |
+
name: Optional[str] = None,
|
| 352 |
+
):
|
| 353 |
+
super().__init__(name=name)
|
| 354 |
+
self.transform_fns = [module(coords, dt, physics_specs, aux_features)
|
| 355 |
+
for module in transform_modules]
|
| 356 |
+
|
| 357 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 358 |
+
for transform_fn in self.transform_fns:
|
| 359 |
+
inputs = transform_fn(inputs)
|
| 360 |
+
return inputs
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@gin.register
|
| 364 |
+
class LevelScale(hk.Module):
|
| 365 |
+
"""Transforms inputs by scaling different vertical levels."""
|
| 366 |
+
|
| 367 |
+
def __init__(
|
| 368 |
+
self,
|
| 369 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 370 |
+
dt: float,
|
| 371 |
+
physics_specs: Any,
|
| 372 |
+
aux_features: typing.AuxFeatures,
|
| 373 |
+
scales: Sequence[float],
|
| 374 |
+
keys_to_scale: Sequence[str] = tuple(),
|
| 375 |
+
name: Optional[str] = None,
|
| 376 |
+
):
|
| 377 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 378 |
+
super().__init__(name=name)
|
| 379 |
+
self.scale_fn = functools.partial(
|
| 380 |
+
coordinate_systems.scale_levels_for_matching_keys,
|
| 381 |
+
scales=np.asarray(scales),
|
| 382 |
+
keys_to_scale=keys_to_scale)
|
| 383 |
+
|
| 384 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 385 |
+
return self.scale_fn(inputs)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@gin.register
|
| 389 |
+
class InverseLevelScale(hk.Module):
|
| 390 |
+
"""Transforms inputs by inverse scaling different vertical levels."""
|
| 391 |
+
|
| 392 |
+
def __init__(
|
| 393 |
+
self,
|
| 394 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 395 |
+
dt: float,
|
| 396 |
+
physics_specs: Any,
|
| 397 |
+
aux_features: typing.AuxFeatures,
|
| 398 |
+
scales: Sequence[float],
|
| 399 |
+
keys_to_scale: Sequence[str] = tuple(),
|
| 400 |
+
name: Optional[str] = None,
|
| 401 |
+
):
|
| 402 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 403 |
+
super().__init__(name=name)
|
| 404 |
+
self.scale_fn = functools.partial(
|
| 405 |
+
coordinate_systems.scale_levels_for_matching_keys,
|
| 406 |
+
scales=1/np.asarray(scales),
|
| 407 |
+
keys_to_scale=keys_to_scale)
|
| 408 |
+
|
| 409 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 410 |
+
return self.scale_fn(inputs)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
@gin.register
|
| 414 |
+
class HardClip(hk.Module):
|
| 415 |
+
"""Transforms inputs by hard clipping inputs to (-max_value, max_value)."""
|
| 416 |
+
|
| 417 |
+
def __init__(
|
| 418 |
+
self,
|
| 419 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 420 |
+
dt: float,
|
| 421 |
+
physics_specs: Any,
|
| 422 |
+
aux_features: typing.AuxFeatures,
|
| 423 |
+
max_value: float,
|
| 424 |
+
name: Optional[str] = None,
|
| 425 |
+
):
|
| 426 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 427 |
+
super().__init__(name=name)
|
| 428 |
+
self.clip_fn = functools.partial(
|
| 429 |
+
jnp.clip, min=-max_value, max=max_value)
|
| 430 |
+
|
| 431 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 432 |
+
return jax.tree_util.tree_map(self.clip_fn, inputs)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@gin.register
|
| 436 |
+
class SoftClip(hk.Module):
|
| 437 |
+
"""Transforms inputs by clipping values to a range with smooth boundaries.
|
| 438 |
+
|
| 439 |
+
Attributes:
|
| 440 |
+
coords: horizontal and vertical descritization.
|
| 441 |
+
dt: time step of the model.
|
| 442 |
+
physics_specs: object describing the scales and physical constants.
|
| 443 |
+
aux_features: dictionary holding static features that the model may use.
|
| 444 |
+
max_value: specifies the range (-max_value, max_value) of return values.
|
| 445 |
+
hinge_softness: controls the softness of the smoothing at the boundaries;
|
| 446 |
+
values outside of the max_value range are mapped into intervals of width
|
| 447 |
+
approximately `log(2) * hinge_softness` on the interior of each boundary.
|
| 448 |
+
name: optional name of the module.
|
| 449 |
+
"""
|
| 450 |
+
|
| 451 |
+
def __init__(
|
| 452 |
+
self,
|
| 453 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 454 |
+
dt: float,
|
| 455 |
+
physics_specs: Any,
|
| 456 |
+
aux_features: typing.AuxFeatures,
|
| 457 |
+
max_value: float,
|
| 458 |
+
hinge_softness: float = 1.0,
|
| 459 |
+
name: Optional[str] = None,
|
| 460 |
+
):
|
| 461 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 462 |
+
if max_value < 0 or hinge_softness < 0:
|
| 463 |
+
raise ValueError('max_value and hinge_softness must be positive, '
|
| 464 |
+
f'{max_value=}, {hinge_softness=}')
|
| 465 |
+
super().__init__(name=name)
|
| 466 |
+
low = -max_value
|
| 467 |
+
high = max_value
|
| 468 |
+
hinge = hinge_softness
|
| 469 |
+
softplus_fn = lambda x: hinge * jax.nn.softplus(x / hinge)
|
| 470 |
+
self.clip_fn = lambda x: ( # pylint: disable=g-long-lambda
|
| 471 |
+
-softplus_fn(high - low - softplus_fn(x - low)) *
|
| 472 |
+
(high - low) / (softplus_fn(high - low)) + high)
|
| 473 |
+
|
| 474 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 475 |
+
return jax.tree_util.tree_map(self.clip_fn, inputs)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
@gin.register
|
| 479 |
+
class ToModalDiffOperatorsWithFiltering(hk.Module):
|
| 480 |
+
"""Module that returns filtered grad and laplacian features of inputs fields.
|
| 481 |
+
|
| 482 |
+
To avoid accidental accumulation of the cos(lat) factors, features must be
|
| 483 |
+
keyed using typing.KeyWithCosLatFactor namedtuple.
|
| 484 |
+
"""
|
| 485 |
+
|
| 486 |
+
def __init__(
|
| 487 |
+
self,
|
| 488 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 489 |
+
dt: float,
|
| 490 |
+
physics_specs: Any,
|
| 491 |
+
aux_features: typing.AuxFeatures,
|
| 492 |
+
filter_attenuations: Tuple[float, ...] = tuple(),
|
| 493 |
+
name: Optional[str] = None,
|
| 494 |
+
):
|
| 495 |
+
super().__init__(name=name)
|
| 496 |
+
self.coords = coords
|
| 497 |
+
self.attenuations = filter_attenuations
|
| 498 |
+
feature_filters = []
|
| 499 |
+
for attenuation in filter_attenuations:
|
| 500 |
+
feature_filters.append(
|
| 501 |
+
filters.DataExponentialFilter(
|
| 502 |
+
coords, dt, physics_specs, aux_features,
|
| 503 |
+
order=1, attenuation=attenuation))
|
| 504 |
+
self.feature_filters = feature_filters
|
| 505 |
+
|
| 506 |
+
def __call__(
|
| 507 |
+
self,
|
| 508 |
+
inputs: Mapping[KeyWithCosLatFactor, typing.Array],
|
| 509 |
+
) -> Mapping[KeyWithCosLatFactor, typing.Array]:
|
| 510 |
+
features = {}
|
| 511 |
+
for k, value in inputs.items():
|
| 512 |
+
name, cos_lat_order = k.name, k.factor_order
|
| 513 |
+
for filter_fn, att in zip(self.feature_filters, self.attenuations):
|
| 514 |
+
filtered_value = filter_fn(value)
|
| 515 |
+
d_value_dlon, d_value_dlat = self.coords.horizontal.cos_lat_grad(
|
| 516 |
+
filtered_value)
|
| 517 |
+
laplacian_value = self.coords.horizontal.laplacian(filtered_value)
|
| 518 |
+
# since gradient values picked up cos_lat factor we increment the
|
| 519 |
+
# corresponding key. This factor is adjusted at the caller level.
|
| 520 |
+
dlon_key = KeyWithCosLatFactor(
|
| 521 |
+
name + f'_dlon_{att}', cos_lat_order + 1, att)
|
| 522 |
+
dlat_key = KeyWithCosLatFactor(
|
| 523 |
+
name + f'_dlat_{att}', cos_lat_order + 1, att)
|
| 524 |
+
del2_key = KeyWithCosLatFactor(
|
| 525 |
+
name + f'_del2_{att}', cos_lat_order, att)
|
| 526 |
+
features[dlon_key] = d_value_dlon
|
| 527 |
+
features[dlat_key] = d_value_dlat
|
| 528 |
+
features[del2_key] = laplacian_value
|
| 529 |
+
return features
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
@gin.register
|
| 533 |
+
class TruncateSigmaLevels(hk.Module):
|
| 534 |
+
"""Transform module that truncates vertical levels for specified variables."""
|
| 535 |
+
|
| 536 |
+
def __init__(
|
| 537 |
+
self,
|
| 538 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 539 |
+
dt: float,
|
| 540 |
+
physics_specs: Any,
|
| 541 |
+
aux_features: typing.AuxFeatures,
|
| 542 |
+
sigma_ranges: dict[str, Tuple[float, float]],
|
| 543 |
+
name: Optional[str] = None,
|
| 544 |
+
):
|
| 545 |
+
super().__init__(name=name)
|
| 546 |
+
del dt, physics_specs, aux_features # unused.
|
| 547 |
+
self.sigma_ranges = sigma_ranges
|
| 548 |
+
self.sigma_levels = coords.vertical.centers
|
| 549 |
+
|
| 550 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 551 |
+
"""Returns `inputs` where only specified levels are retained."""
|
| 552 |
+
|
| 553 |
+
def _slice_fn(x, sigma_range):
|
| 554 |
+
"""Returns `x` sliced to include values in `sigma_range`."""
|
| 555 |
+
sigma_min_slice, sigma_max_slice = sigma_range
|
| 556 |
+
lower_index = np.argmax((self.sigma_levels - sigma_min_slice) > 0)
|
| 557 |
+
if sigma_max_slice > np.max(self.sigma_levels):
|
| 558 |
+
upper_index = len(self.sigma_levels)
|
| 559 |
+
else:
|
| 560 |
+
upper_index = np.argmin((self.sigma_levels - sigma_max_slice) < 0)
|
| 561 |
+
return x[slice(lower_index, upper_index), ...]
|
| 562 |
+
|
| 563 |
+
def recurse_and_replace(x: dict[str, Any],
|
| 564 |
+
y: dict[str, Any],
|
| 565 |
+
default=None) -> dict[str, Any]:
|
| 566 |
+
"""Copy x, setting leaf values to `default` or value from y if keys match."""
|
| 567 |
+
return {
|
| 568 |
+
k: (
|
| 569 |
+
y.get(k, default)
|
| 570 |
+
if not isinstance(v, dict)
|
| 571 |
+
else recurse_and_replace(v, y, default)
|
| 572 |
+
)
|
| 573 |
+
for k, v in x.items()
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 577 |
+
sigma_ranges_extended = recurse_and_replace(
|
| 578 |
+
inputs, self.sigma_ranges, default=(0, 1)
|
| 579 |
+
)
|
| 580 |
+
outputs = jax.tree_util.tree_map(_slice_fn, inputs, sigma_ranges_extended)
|
| 581 |
+
return from_dict_fn(outputs)
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
@gin.register
|
| 585 |
+
class TakeSurfaceAdjacentSigmaLevel(hk.Module):
|
| 586 |
+
"""Transform module that retains only the vertical level nearest to Earth surface for all variables."""
|
| 587 |
+
|
| 588 |
+
def __init__(
|
| 589 |
+
self,
|
| 590 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 591 |
+
dt: float,
|
| 592 |
+
physics_specs: Any,
|
| 593 |
+
aux_features: typing.AuxFeatures,
|
| 594 |
+
name: Optional[str] = None,
|
| 595 |
+
):
|
| 596 |
+
super().__init__(name=name)
|
| 597 |
+
del coords, dt, physics_specs, aux_features # unused.
|
| 598 |
+
|
| 599 |
+
def __call__(self, inputs: typing.PyTreeState) -> typing.PyTreeState:
|
| 600 |
+
"""Returns `inputs` where only last sigma level is retained."""
|
| 601 |
+
|
| 602 |
+
def _slice_fn(x):
|
| 603 |
+
return x[slice(-1, None), ...]
|
| 604 |
+
|
| 605 |
+
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
|
| 606 |
+
outputs = jax.tree_util.tree_map(_slice_fn, inputs)
|
| 607 |
+
return from_dict_fn(outputs)
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
@gin.register
|
| 611 |
+
@dataclasses.dataclass
|
| 612 |
+
class FeatureSelector:
|
| 613 |
+
"""Features transform that retains items whose keys match against regex.
|
| 614 |
+
|
| 615 |
+
Attributes:
|
| 616 |
+
regex_patterns: regular expression pattern that specifies the set of keys
|
| 617 |
+
from `inputs` that will be returned by __call__ method.
|
| 618 |
+
"""
|
| 619 |
+
regex_patterns: str
|
| 620 |
+
|
| 621 |
+
def __call__(
|
| 622 |
+
self,
|
| 623 |
+
inputs: Dict[str, typing.Array],
|
| 624 |
+
) -> Dict[str, typing.Array]:
|
| 625 |
+
outputs = {}
|
| 626 |
+
for k, v in inputs.items():
|
| 627 |
+
if re.fullmatch(self.regex_patterns, k):
|
| 628 |
+
outputs[k] = v
|
| 629 |
+
return outputs
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
@gin.register
|
| 633 |
+
class BroadcastTransform:
|
| 634 |
+
"""Features transform that broadcasts all features."""
|
| 635 |
+
|
| 636 |
+
def __init__(self, *args, **kwargs):
|
| 637 |
+
del args, kwargs # unused.
|
| 638 |
+
|
| 639 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 640 |
+
leaves, tree_def = jax.tree_util.tree_flatten(inputs)
|
| 641 |
+
leaves = jnp.broadcast_arrays(*leaves)
|
| 642 |
+
return jax.tree_util.tree_unflatten(tree_def, leaves)
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
@gin.register
|
| 646 |
+
class SquashLevelsTransform:
|
| 647 |
+
"""Transform that "squashes" values of inputs depending on their sigma level.
|
| 648 |
+
|
| 649 |
+
Multiplies inputs by the piecewise linear values used to "squash" inputs
|
| 650 |
+
by sigma level. See function χ definition at: http://screen/5V3jzU7ZFA4vVJP
|
| 651 |
+
|
| 652 |
+
The squash is paramtereizaed by low_cutoffs and high_cutoffs.
|
| 653 |
+
On Palmer 2009 (http://shortn/_56HCcQwmSS) page 4, the cutoffs for
|
| 654 |
+
perturbations are given. Below are translated to sigma levels values:
|
| 655 |
+
low_cutoffs: (100hPa, 50hPa)
|
| 656 |
+
low_cutoffs: (0.05, 0.1),
|
| 657 |
+
high_cutoffs: (1300m, 300m)
|
| 658 |
+
high_cutoffs: (0.86, 0.965)
|
| 659 |
+
|
| 660 |
+
Inputs that have a singleton or no level dimension are assumed defined at
|
| 661 |
+
the highest value of sigma ("surface level").
|
| 662 |
+
|
| 663 |
+
Attributes:
|
| 664 |
+
coords: horizontal and vertical descritization.
|
| 665 |
+
dt: time step of the model.
|
| 666 |
+
physics_specs: object describing the scales and physical constants.
|
| 667 |
+
aux_features: dictionary holding static features that the model may use.
|
| 668 |
+
low_cutoffs: σ=low_cutoffs[0] is when χ starts linearly increasing from 0.
|
| 669 |
+
σ=low_cutoffs[1] is when χ levels out at 1
|
| 670 |
+
high_cutoffs: σ=high_cutoffs[0] is when χ starts linearly decreasing from 1.
|
| 671 |
+
σ=high_cutoffs[1] is when χ reaches 0.
|
| 672 |
+
"""
|
| 673 |
+
|
| 674 |
+
def __init__(
|
| 675 |
+
self,
|
| 676 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 677 |
+
dt: float,
|
| 678 |
+
physics_specs: Any,
|
| 679 |
+
aux_features: typing.AuxFeatures,
|
| 680 |
+
low_cutoffs: Sequence[float] = (0.05, 0.1),
|
| 681 |
+
high_cutoffs: Sequence[float] = (0.86, 0.965),
|
| 682 |
+
):
|
| 683 |
+
del dt, physics_specs, aux_features # unused.
|
| 684 |
+
if not isinstance(coords.vertical, sigma_coordinates.SigmaCoordinates):
|
| 685 |
+
raise ValueError(f'Cannot apply sigma_squash on {coords.vertical=}')
|
| 686 |
+
sigma = coords.vertical.centers
|
| 687 |
+
if len(low_cutoffs) != 2:
|
| 688 |
+
raise ValueError(f'{len(low_cutoffs)=} but should have been 2.')
|
| 689 |
+
if len(high_cutoffs) != 2:
|
| 690 |
+
raise ValueError(f'{len(high_cutoffs)=} but should have been 2.')
|
| 691 |
+
|
| 692 |
+
low_func = (sigma - low_cutoffs[0]) / (low_cutoffs[1] - low_cutoffs[0])
|
| 693 |
+
high_func = (high_cutoffs[1] - sigma) / (high_cutoffs[1] - high_cutoffs[0])
|
| 694 |
+
|
| 695 |
+
# lower_bound is a function equal to the squasher between
|
| 696 |
+
# low_cutoffs[0] and high_cutoffs[1].
|
| 697 |
+
# It becomes negative outside that range.
|
| 698 |
+
lower_bound = np.minimum(1., np.minimum(low_func, high_func))
|
| 699 |
+
self._sigma_squash = np.maximum(0., lower_bound)[:, np.newaxis, np.newaxis]
|
| 700 |
+
|
| 701 |
+
def __call__(self, inputs: typing.Pytree) -> typing.Pytree:
|
| 702 |
+
def squash_per_level_only(x):
|
| 703 |
+
shape = jnp.shape(x)
|
| 704 |
+
ndim = len(shape)
|
| 705 |
+
if ndim >= 3 and shape[-3] > 1: # If defined per-level
|
| 706 |
+
return x * self._sigma_squash
|
| 707 |
+
elif ndim in {2, 3}:
|
| 708 |
+
return x * self._sigma_squash[-1] # If defined at surface level
|
| 709 |
+
else:
|
| 710 |
+
return x
|
| 711 |
+
return jax.tree_util.tree_map(squash_per_level_only, inputs)
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
@gin.register
|
| 715 |
+
def add_prefix(features: dict[str, Any], prefix: str) -> dict[str, Any]:
|
| 716 |
+
"""Adds prefix to keys in features."""
|
| 717 |
+
return {prefix + k: v for k, v in features.items()}
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
def straight_through(
|
| 721 |
+
f: Callable[[typing.Array], typing.Array],
|
| 722 |
+
) -> Callable[[typing.Array], typing.Array]:
|
| 723 |
+
"""Straight-through estimator of `func`.
|
| 724 |
+
|
| 725 |
+
The "straight-through" estimator is a trick that fools auto-diff into
|
| 726 |
+
assigning a constant gradient (≡ 1) to a function.
|
| 727 |
+
See http://shortn/_kRQjMbF2QF
|
| 728 |
+
|
| 729 |
+
Args:
|
| 730 |
+
f: Callable mapping arrays to arrays. May be non-differentiable.
|
| 731 |
+
|
| 732 |
+
Returns:
|
| 733 |
+
g: Function g such that g(x) ≡ f(x) and g'(x) ≡ 1.
|
| 734 |
+
"""
|
| 735 |
+
def straight_through_f(x):
|
| 736 |
+
zero = x - jax.lax.stop_gradient(x)
|
| 737 |
+
return zero + jax.lax.stop_gradient(f(x))
|
| 738 |
+
return straight_through_f
|
model/reference_code/datasets.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Loads datasets."""
|
| 15 |
+
|
| 16 |
+
import functools
|
| 17 |
+
import itertools
|
| 18 |
+
import json
|
| 19 |
+
import logging
|
| 20 |
+
import math
|
| 21 |
+
import multiprocessing
|
| 22 |
+
import random
|
| 23 |
+
from typing import Any, Callable, Iterator, Mapping, Optional, Tuple
|
| 24 |
+
|
| 25 |
+
import jax
|
| 26 |
+
import numpy as np
|
| 27 |
+
import pandas as pd
|
| 28 |
+
import tensorflow.compat.v2 as tf
|
| 29 |
+
import xarray
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
Pytree = Any
|
| 33 |
+
# pylint: disable=g-bare-generic
|
| 34 |
+
# pylint: disable=logging-fstring-interpolation
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def drop_static_vars(dataset: xarray.Dataset) -> xarray.Dataset:
|
| 38 |
+
"""Drop fields that are static and do not vary with time."""
|
| 39 |
+
has_sample_dim = 'sample' in dataset.coords
|
| 40 |
+
vars_to_drop = []
|
| 41 |
+
for name, var in dataset.items():
|
| 42 |
+
if 'time' not in var.dims:
|
| 43 |
+
vars_to_drop.append(name)
|
| 44 |
+
elif has_sample_dim and var.dims[:2] != ('sample', 'time'):
|
| 45 |
+
raise ValueError(f'dimensions for variable {name} do not start with '
|
| 46 |
+
f"'sample' and 'time': {var.dims}")
|
| 47 |
+
elif not has_sample_dim and var.dims[0] != 'time':
|
| 48 |
+
raise ValueError(f'dimensions for variable {name} do not start with '
|
| 49 |
+
f"'time': {var.dims}")
|
| 50 |
+
return dataset.drop_vars(vars_to_drop)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def attrs_from_dataset(
|
| 54 |
+
dataset: xarray.Dataset,
|
| 55 |
+
time_series_length: int,
|
| 56 |
+
subsample_rate: int = 1,
|
| 57 |
+
) -> dict:
|
| 58 |
+
"""Extracts attributes from `dataset`."""
|
| 59 |
+
attrs = dict(dataset.attrs)
|
| 60 |
+
attrs['trajectory_length'] = time_series_length
|
| 61 |
+
attrs['time_subsample_rate'] = subsample_rate
|
| 62 |
+
delta_t = (dataset.time[1] - dataset.time[0]).data
|
| 63 |
+
if not np.issubdtype(dataset.time.dtype, np.floating):
|
| 64 |
+
logging.info(f'converting non-float {delta_t=} to seconds')
|
| 65 |
+
delta_t = np.timedelta64(delta_t, 's') / np.timedelta64(1, 's')
|
| 66 |
+
attrs['save_dt_units'] = 's'
|
| 67 |
+
else:
|
| 68 |
+
attrs['save_dt_units'] = 'dimensionless'
|
| 69 |
+
attrs['save_dt'] = float(delta_t) * subsample_rate
|
| 70 |
+
return attrs
|
model/reference_code/experiment.py
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
# pylint: disable=line-too-long
|
| 15 |
+
r"""Pseudocode for training NeuralGCM models."""
|
| 16 |
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
| 17 |
+
import dataclasses
|
| 18 |
+
import functools
|
| 19 |
+
import logging
|
| 20 |
+
import math
|
| 21 |
+
from typing import Any, Callable, NamedTuple, Optional, cast
|
| 22 |
+
|
| 23 |
+
from absl import app
|
| 24 |
+
import datasets
|
| 25 |
+
from dinosaur import typing
|
| 26 |
+
from dinosaur import xarray_utils
|
| 27 |
+
import gin
|
| 28 |
+
from google_proprietary_code import checkpoint
|
| 29 |
+
from google_proprietary_code import experiment
|
| 30 |
+
from google_proprietary_code import experiment_utils
|
| 31 |
+
# proprietary imports
|
| 32 |
+
from google_proprietary_code import profiling_util
|
| 33 |
+
from google_proprietary_code import streaming
|
| 34 |
+
from google_proprietary_code import timing_util
|
| 35 |
+
import haiku as hk
|
| 36 |
+
import jax
|
| 37 |
+
import jax.numpy as jnp
|
| 38 |
+
import jax.sharding
|
| 39 |
+
import model.reference_code.metrics as metrics
|
| 40 |
+
import model.reference_code.metrics_base as metrics_base
|
| 41 |
+
import model.reference_code.metrics_util as metrics_util
|
| 42 |
+
from ml_collections import config_dict
|
| 43 |
+
from model.legacy import model_builder
|
| 44 |
+
from model.legacy import model_utils
|
| 45 |
+
from model.legacy import optimization
|
| 46 |
+
from model.legacy import physics_specifications
|
| 47 |
+
import numpy as np
|
| 48 |
+
import optax
|
| 49 |
+
import pandas as pd
|
| 50 |
+
import model.reference_code.reader as reader
|
| 51 |
+
import model.reference_code.stochastic_losses as stochastic_losses
|
| 52 |
+
import tensorflow as tf
|
| 53 |
+
import model.reference_code.train_utils as train_utils
|
| 54 |
+
import xarray
|
| 55 |
+
|
| 56 |
+
Params = typing.Params
|
| 57 |
+
PyTree = Any
|
| 58 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 59 |
+
TrajectoryFn = Callable[
|
| 60 |
+
[Params, jax.Array, PyTree, PyTree],
|
| 61 |
+
tuple[TrajectoryRepresentations, TrajectoryRepresentations],
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
tree_map = jax.tree_util.tree_map
|
| 65 |
+
|
| 66 |
+
# pylint: disable=logging-format-interpolation
|
| 67 |
+
# pylint: disable=logging-fstring-interpolation
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@gin.configurable(allowlist=['constructor'])
|
| 71 |
+
def get_loss_obj(
|
| 72 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 73 |
+
constructor: Callable[..., metrics_base.Loss] = gin.REQUIRED,
|
| 74 |
+
) -> metrics_base.Loss:
|
| 75 |
+
"""Returns configured loss_fn on first `trajectory_length` time slices."""
|
| 76 |
+
return constructor(trajectory_spec)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
EvaluatorDict = dict[str, metrics_base.Evaluator]
|
| 80 |
+
TrainEvalIteratorTuple = tuple[
|
| 81 |
+
Iterator[Any],
|
| 82 |
+
train_utils.TrainStepFunction,
|
| 83 |
+
Callable[..., Any],
|
| 84 |
+
dict[str, Any],
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@gin.configurable(allowlist=['constructor'])
|
| 89 |
+
def get_metrics_dict(
|
| 90 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 91 |
+
eval_time_steps: Sequence[int],
|
| 92 |
+
loss: metrics_base.Loss,
|
| 93 |
+
constructor: Callable[..., EvaluatorDict] = metrics.default_metrics,
|
| 94 |
+
is_ensemble_data: bool = False,
|
| 95 |
+
) -> EvaluatorDict:
|
| 96 |
+
"""Returns configured loss_fn on first `trajectory_length` time slices."""
|
| 97 |
+
return constructor(
|
| 98 |
+
trajectory_spec, eval_time_steps, loss, is_ensemble_data=is_ensemble_data
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# start legacy configurables
|
| 103 |
+
#
|
| 104 |
+
# Keep these around for now (even though they are no-ops) so we can run
|
| 105 |
+
# inference on old models.
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@gin.configurable
|
| 109 |
+
def get_loss_fn(loss_fn):
|
| 110 |
+
raise NotImplementedError
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@gin.register
|
| 114 |
+
def weighted_l2_cumulative_loss(weights, scale):
|
| 115 |
+
raise NotImplementedError
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# end legacy configurables
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def ema_params_tree(num_steps):
|
| 122 |
+
"""Creates an EMAParamsTree object based on num_steps.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
num_steps: average number of optimization steps to include in the
|
| 126 |
+
exponential moving average of model weights.
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
Haiku module.
|
| 130 |
+
"""
|
| 131 |
+
# https://en.wikipedia.org/wiki/Moving_average#Relationship_between_SMA_and_EMA
|
| 132 |
+
decay = 1 - 2 / (num_steps + 1)
|
| 133 |
+
return hk.EMAParamsTree(decay)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _model_inner_steps_for_data(
|
| 137 |
+
ds: xarray.Dataset,
|
| 138 |
+
model_specs: model_builder.ModelSpecs,
|
| 139 |
+
rtol: float = 1e-6,
|
| 140 |
+
) -> int:
|
| 141 |
+
"""Calculates model inner steps based on data time step."""
|
| 142 |
+
data_dt = xarray_utils.nondim_time_delta_from_time_axis(
|
| 143 |
+
ds.time.data, model_specs.physics_specs
|
| 144 |
+
)
|
| 145 |
+
inner_steps = round(data_dt / model_specs.dt)
|
| 146 |
+
if abs(inner_steps * model_specs.dt - data_dt) / data_dt > rtol:
|
| 147 |
+
raise RuntimeError( # pylint: disable=g-doc-exception
|
| 148 |
+
f'{model_specs.dt=} does not divide evenly into {data_dt=}'
|
| 149 |
+
)
|
| 150 |
+
return inner_steps
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _get_datetime_forecast_starts(
|
| 154 |
+
sample_count: int,
|
| 155 |
+
first_start: pd.Timestamp,
|
| 156 |
+
last_start: pd.Timestamp,
|
| 157 |
+
) -> pd.DatetimeIndex:
|
| 158 |
+
"""Get equispaced forecast start times for evaluating against ERA5."""
|
| 159 |
+
if first_start.hour != 0:
|
| 160 |
+
raise ValueError(f'dataset times must start at midnight: {first_start=}')
|
| 161 |
+
# Round-up to midnight following the last forecast day (e.g., the start of
|
| 162 |
+
# the next year).
|
| 163 |
+
stop = last_start.ceil('1D')
|
| 164 |
+
# Equally spaced from start (inclusive) to stop (exclusive).
|
| 165 |
+
start_times = pd.date_range(first_start, stop, periods=sample_count + 1)[:-1]
|
| 166 |
+
# To match ECMWF, all forecasts should be initialized at 0z or 12z. Here we
|
| 167 |
+
# alternate start times.
|
| 168 |
+
parity = np.arange(sample_count) % 2
|
| 169 |
+
return start_times.round('1D') + parity * pd.Timedelta('12H')
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
P = jax.sharding.PartitionSpec
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class ExperimentState(NamedTuple):
|
| 176 |
+
opt_state: PyTree
|
| 177 |
+
params: PyTree
|
| 178 |
+
ema_params: PyTree
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class Experiment(experiment.AbstractExperiment):
|
| 182 |
+
"""Training experiment based on trajectory loss minimization."""
|
| 183 |
+
|
| 184 |
+
def __init__(
|
| 185 |
+
self,
|
| 186 |
+
experiment_dir: str,
|
| 187 |
+
config: Optional[config_dict.ConfigDict] = None,
|
| 188 |
+
):
|
| 189 |
+
"""Creates an instance of a training scheme class.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
experiment_dir: Path to experiment directory.
|
| 193 |
+
config: config struct setting up the experiment.
|
| 194 |
+
"""
|
| 195 |
+
if config is None:
|
| 196 |
+
config = experiment_config.get_config()
|
| 197 |
+
|
| 198 |
+
super().__init__(
|
| 199 |
+
experiment_dir,
|
| 200 |
+
config.distributed_training,
|
| 201 |
+
writer_names=['train', 'eval', 'eval_ema'],
|
| 202 |
+
)
|
| 203 |
+
logging.info('Experiment config:\n%s', config)
|
| 204 |
+
self.config = config
|
| 205 |
+
|
| 206 |
+
self.train_ds = xarray_utils.open_dataset(config.train_dataset_path)
|
| 207 |
+
self.eval_ds = xarray_utils.open_dataset(config.eval_dataset_path)
|
| 208 |
+
|
| 209 |
+
if 'sample' in self.train_ds.dims:
|
| 210 |
+
logging.warning('only using the first sample!')
|
| 211 |
+
self.train_ds = self.train_ds.isel(sample=0, drop=True)
|
| 212 |
+
self.eval_ds = self.eval_ds.isel(sample=0, drop=True)
|
| 213 |
+
|
| 214 |
+
train_attrs = self.train_ds.attrs
|
| 215 |
+
|
| 216 |
+
# Model instantiation and trajectory unroll functions
|
| 217 |
+
# Note: we use interactive mode in experiments to split gin-configurations
|
| 218 |
+
# into separate, distinct parts provided in the config_dict.
|
| 219 |
+
gin.enter_interactive_mode()
|
| 220 |
+
self.is_nodal = self.config.is_nodal
|
| 221 |
+
|
| 222 |
+
# parse and override all the gin things
|
| 223 |
+
gin.parse_config(config.model_gin_config)
|
| 224 |
+
gin.parse_config(config.optimizer_gin_config)
|
| 225 |
+
experiment_utils.parse_config_dict(config.gin_overrides)
|
| 226 |
+
|
| 227 |
+
logging.info('Parsed gin config string:\n%s', gin.config_str())
|
| 228 |
+
|
| 229 |
+
full_model_gin_config = gin.config_str() # do not include physics config.
|
| 230 |
+
logging.info('With overrides gin config string:\n%s', gin.config_str())
|
| 231 |
+
|
| 232 |
+
self.data_coords = model_builder.coordinate_system_from_dataset(
|
| 233 |
+
self.train_ds
|
| 234 |
+
)
|
| 235 |
+
logging.info(f'{self.model_parallel_training=}')
|
| 236 |
+
|
| 237 |
+
if self.model_parallel_training:
|
| 238 |
+
# It does not make sense to use spatial parallelism with batch size per
|
| 239 |
+
# device larger than 1. Instead, you would get better performance from
|
| 240 |
+
# using less model parallelism.
|
| 241 |
+
if self.spatial_parallelism > 1 and self.config.batch_size_per_device > 1:
|
| 242 |
+
raise NotImplementedError(
|
| 243 |
+
f'{self.config.batch_size_per_device=} is not supported for model '
|
| 244 |
+
'parallel training'
|
| 245 |
+
)
|
| 246 |
+
self.data_coords = dataclasses.replace(
|
| 247 |
+
self.data_coords, spmd_mesh=self.spmd_mesh
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# try getting aux_features from dataset, if not included we rely on
|
| 251 |
+
# `model_builder.get_model_specs` to supply necessary values.
|
| 252 |
+
try:
|
| 253 |
+
data_aux_features = xarray_utils.aux_features_from_xarray(self.train_ds)
|
| 254 |
+
except KeyError:
|
| 255 |
+
data_aux_features = {}
|
| 256 |
+
|
| 257 |
+
# when available, we parse physics_config_str from metadata in train_attrs.
|
| 258 |
+
if 'physics_config_str' in train_attrs:
|
| 259 |
+
physics_config_str = train_attrs['physics_config_str']
|
| 260 |
+
experiment_utils.parse_gin_config_without_imports(physics_config_str)
|
| 261 |
+
else:
|
| 262 |
+
logging.info(
|
| 263 |
+
'physics_config_str was not provided in the dataset, '
|
| 264 |
+
'hence it is expected to be specified in model_gin_config.'
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
self.physics_specs = physics_specifications.get_physics_specs()
|
| 268 |
+
self.model_specs = model_builder.get_model_specs(
|
| 269 |
+
self.data_coords, self.physics_specs, data_aux_features
|
| 270 |
+
)
|
| 271 |
+
logging.info(f'{self.model_specs=}')
|
| 272 |
+
|
| 273 |
+
self.train_inner_steps = _model_inner_steps_for_data(
|
| 274 |
+
self.train_ds, self.model_specs
|
| 275 |
+
)
|
| 276 |
+
self.eval_inner_steps = _model_inner_steps_for_data(
|
| 277 |
+
self.eval_ds, self.model_specs
|
| 278 |
+
)
|
| 279 |
+
if (
|
| 280 |
+
len(self.config.train_schedule_time_steps)
|
| 281 |
+
!= len(self.config.train_schedule_boundaries) + 1
|
| 282 |
+
):
|
| 283 |
+
raise ValueError(
|
| 284 |
+
f'{self.config.train_schedule_time_steps} should be one longer '
|
| 285 |
+
f'than {self.config.train_schedule_boundaries} but was not.'
|
| 286 |
+
)
|
| 287 |
+
if any(
|
| 288 |
+
t % self.train_inner_steps
|
| 289 |
+
for t in self.config.train_schedule_time_steps
|
| 290 |
+
):
|
| 291 |
+
raise ValueError(
|
| 292 |
+
f'{self.train_inner_steps=} does not divide '
|
| 293 |
+
f'{self.config.train_schedule_time_steps=}'
|
| 294 |
+
)
|
| 295 |
+
if any(t % self.eval_inner_steps for t in self.config.eval_time_steps):
|
| 296 |
+
raise ValueError(
|
| 297 |
+
f'{self.eval_inner_steps=} does not divide '
|
| 298 |
+
f'{self.config.eval_time_steps=}'
|
| 299 |
+
)
|
| 300 |
+
if max(self.config.eval_time_steps) < max(
|
| 301 |
+
self.config.train_schedule_time_steps
|
| 302 |
+
):
|
| 303 |
+
raise ValueError(
|
| 304 |
+
f'Training will not work since {max(self.config.eval_time_steps)=} <'
|
| 305 |
+
f' {max(self.config.train_schedule_time_steps)=}'
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
self._eval_trajectory_length = (
|
| 309 |
+
max(self.config.eval_time_steps) // self.eval_inner_steps + 1
|
| 310 |
+
)
|
| 311 |
+
self._trajectory_lengths = [
|
| 312 |
+
self.config.num_init_frames + n // self.train_inner_steps
|
| 313 |
+
for n in self.config.train_schedule_time_steps
|
| 314 |
+
]
|
| 315 |
+
self._max_trajectory_length = max(
|
| 316 |
+
self._trajectory_lengths + [self._eval_trajectory_length]
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
self.reference_datetime = self.model_specs.aux_features[
|
| 320 |
+
xarray_utils.REFERENCE_DATETIME_KEY
|
| 321 |
+
]
|
| 322 |
+
|
| 323 |
+
self.whirl_model = model_builder.WhirlModel(
|
| 324 |
+
**self.model_specs,
|
| 325 |
+
input_coords=self.data_coords,
|
| 326 |
+
output_coords=self.data_coords,
|
| 327 |
+
)
|
| 328 |
+
self.from_xarray_fn = self.whirl_model.from_xarray_fn
|
| 329 |
+
|
| 330 |
+
def trajectory_fwd(x, forcing_data, model, outer_steps, inner_steps):
|
| 331 |
+
trajectory_fn = model_utils.trajectory_with_inputs_and_forcing(
|
| 332 |
+
model, config.num_init_frames, start_with_input=True
|
| 333 |
+
)
|
| 334 |
+
return trajectory_fn(x, forcing_data, outer_steps, inner_steps)
|
| 335 |
+
|
| 336 |
+
self._trajectory_fwd = trajectory_fwd
|
| 337 |
+
|
| 338 |
+
# Checkpoint items.
|
| 339 |
+
self._model_dt = self.model_specs.dt
|
| 340 |
+
self._model_gin_config = full_model_gin_config
|
| 341 |
+
|
| 342 |
+
# optimizer configuration.
|
| 343 |
+
self.optimizer = optimization.optimizer()
|
| 344 |
+
|
| 345 |
+
# exponentially moving average params tracking.
|
| 346 |
+
ema_fn = hk.without_apply_rng(
|
| 347 |
+
hk.transform_with_state(
|
| 348 |
+
lambda x: ema_params_tree(config.ema_num_steps)(x) # pylint: disable=unnecessary-lambda
|
| 349 |
+
)
|
| 350 |
+
)
|
| 351 |
+
self._ema_init = jax.jit(ema_fn.init)
|
| 352 |
+
|
| 353 |
+
def ema_update(params, ema_state):
|
| 354 |
+
return ema_fn.apply(None, ema_state, params)
|
| 355 |
+
|
| 356 |
+
self._ema_update = jax.jit(ema_update)
|
| 357 |
+
|
| 358 |
+
logging.info('Final active config string:\n%s', gin.config_str())
|
| 359 |
+
|
| 360 |
+
#
|
| 361 |
+
# Data inputs methods.
|
| 362 |
+
#
|
| 363 |
+
|
| 364 |
+
@functools.cached_property
|
| 365 |
+
def spmd_mesh(self) -> jax.sharding.Mesh:
|
| 366 |
+
n = self.config.model_parallelism.ensemble_shards
|
| 367 |
+
z = self.config.model_parallelism.z_shards
|
| 368 |
+
x = self.config.model_parallelism.x_shards
|
| 369 |
+
y = self.config.model_parallelism.y_shards
|
| 370 |
+
global_batch = jax.device_count() // (n * z * x * y)
|
| 371 |
+
if global_batch == 0:
|
| 372 |
+
raise ValueError(
|
| 373 |
+
f'{jax.device_count()=} is insufficient for '
|
| 374 |
+
f'{self.config.model_parallelism=}'
|
| 375 |
+
)
|
| 376 |
+
return train_utils.create_spmd_mesh(
|
| 377 |
+
{'batch': global_batch, 'ensemble': n, 'z': z, 'x': x, 'y': y}
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
@functools.cached_property
|
| 381 |
+
def degree_of_model_parallelism(self) -> int:
|
| 382 |
+
return math.prod(v for k, v in self.spmd_mesh.shape.items() if k != 'batch')
|
| 383 |
+
|
| 384 |
+
@functools.cached_property
|
| 385 |
+
def model_parallel_training(self) -> bool:
|
| 386 |
+
return self.degree_of_model_parallelism > 1
|
| 387 |
+
|
| 388 |
+
@functools.cached_property
|
| 389 |
+
def spatial_parallelism(self) -> int:
|
| 390 |
+
return math.prod(self.spmd_mesh.shape[k] for k in 'zxy')
|
| 391 |
+
|
| 392 |
+
def to_global_array(self, pytree: PyTree, global_batch_size: int) -> PyTree:
|
| 393 |
+
"""Create a pytree of global JAX arrays from a pytree of NumPy arrays."""
|
| 394 |
+
# partition arrays along batch and spatial dimensions
|
| 395 |
+
return train_utils.make_distributed_array_from_local_arrays(
|
| 396 |
+
pytree,
|
| 397 |
+
self.spmd_mesh,
|
| 398 |
+
self.data_coords.physics_partition_spec,
|
| 399 |
+
global_batch_size,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
def num_eval_batches(self, large_eval: bool) -> int:
|
| 403 |
+
if large_eval:
|
| 404 |
+
return self.config.num_eval_batches[-1]
|
| 405 |
+
else:
|
| 406 |
+
return self.config.num_eval_batches[0]
|
| 407 |
+
|
| 408 |
+
def steps_between_evals(self, large_eval: bool) -> int:
|
| 409 |
+
if large_eval:
|
| 410 |
+
return self.config.steps_between_evals[-1]
|
| 411 |
+
else:
|
| 412 |
+
return self.config.steps_between_evals[0]
|
| 413 |
+
|
| 414 |
+
def eval_batch_size_per_device(self, large_eval: bool) -> int:
|
| 415 |
+
if large_eval:
|
| 416 |
+
return self.config.eval_batch_size_per_device[-1]
|
| 417 |
+
else:
|
| 418 |
+
return self.config.eval_batch_size_per_device[0]
|
| 419 |
+
|
| 420 |
+
@functools.cached_property
|
| 421 |
+
def global_batch_size(self) -> int:
|
| 422 |
+
return (
|
| 423 |
+
jax.device_count()
|
| 424 |
+
// self.degree_of_model_parallelism
|
| 425 |
+
* self.config.batch_size_per_device
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
def global_eval_batch_size(self, large_eval: bool) -> int:
|
| 429 |
+
return (
|
| 430 |
+
jax.device_count()
|
| 431 |
+
// self.degree_of_model_parallelism
|
| 432 |
+
* self.eval_batch_size_per_device(large_eval)
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
def local_eval_batch_size(self, large_eval: bool) -> int:
|
| 436 |
+
return jax.local_device_count() * self.eval_batch_size_per_device(
|
| 437 |
+
large_eval
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
def _to_dataset_iter(
|
| 441 |
+
self, data: tf.data.Dataset, template: xarray.Dataset
|
| 442 |
+
) -> Callable[[], Iterable[Any]]:
|
| 443 |
+
"""Convert a tf.data.Dataset into a function that makes a data iterator."""
|
| 444 |
+
leading_dims_set = {x.shape[:2] for x in data.element_spec.values()}
|
| 445 |
+
assert len(leading_dims_set) == 1, leading_dims_set
|
| 446 |
+
local_batch_size, time_series_length = leading_dims_set.pop()
|
| 447 |
+
template = (
|
| 448 |
+
template.drop_vars('time')
|
| 449 |
+
.head(time=time_series_length)
|
| 450 |
+
.pipe(xarray.zeros_like) # replace data with zeros
|
| 451 |
+
.pipe(datasets.drop_static_vars)
|
| 452 |
+
.transpose('time', ...)
|
| 453 |
+
.expand_dims(batch=local_batch_size)
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
def make_iterator():
|
| 457 |
+
for example_dict in data.as_numpy_iterator():
|
| 458 |
+
yield self.from_xarray_fn(template.copy(data=example_dict))
|
| 459 |
+
|
| 460 |
+
return make_iterator
|
| 461 |
+
|
| 462 |
+
def _read_shuffled_shard(
|
| 463 |
+
self,
|
| 464 |
+
dataset: xarray.Dataset,
|
| 465 |
+
time_series_length: int,
|
| 466 |
+
min_buffer_blocks: int,
|
| 467 |
+
shard_index: int,
|
| 468 |
+
shard_count: int,
|
| 469 |
+
) -> tf.data.Dataset:
|
| 470 |
+
sampler = reader.Windower(
|
| 471 |
+
window_size=time_series_length,
|
| 472 |
+
stride_between_windows=self.config.train_time_sample_offset,
|
| 473 |
+
)
|
| 474 |
+
local_shard_count = max(
|
| 475 |
+
self.degree_of_model_parallelism, jax.local_device_count()
|
| 476 |
+
)
|
| 477 |
+
seed = train_utils.combine_rng_seeds(
|
| 478 |
+
self.config.dataset_rng_seed, shard_index, time_series_length
|
| 479 |
+
)
|
| 480 |
+
data = reader.read_shuffled_shard(
|
| 481 |
+
dataset,
|
| 482 |
+
sampler,
|
| 483 |
+
block_size_in_bytes=self.config.block_size_in_bytes / local_shard_count,
|
| 484 |
+
buffer_size_in_bytes=(
|
| 485 |
+
self.config.shuffle_buffer_size_in_bytes / local_shard_count
|
| 486 |
+
),
|
| 487 |
+
min_buffer_blocks=min_buffer_blocks,
|
| 488 |
+
shard_index=shard_index,
|
| 489 |
+
shard_count=shard_count,
|
| 490 |
+
seed=seed,
|
| 491 |
+
)
|
| 492 |
+
return data
|
| 493 |
+
|
| 494 |
+
def _get_train_dataset(self) -> xarray.Dataset:
|
| 495 |
+
train_dataset = xarray_utils.ds_with_sim_time(
|
| 496 |
+
self.train_ds, self.physics_specs, self.reference_datetime
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
if self.config.train_dataset_time_slice:
|
| 500 |
+
time_slice = slice(*self.config.train_dataset_time_slice)
|
| 501 |
+
train_dataset = train_dataset.sel(time=time_slice)
|
| 502 |
+
|
| 503 |
+
if self.config.time_subsample_rate != 1:
|
| 504 |
+
raise NotImplementedError('subsampling on the fly is not supported yet')
|
| 505 |
+
if self.config.add_noise_to_input:
|
| 506 |
+
raise NotImplementedError('add_noise_to_input not supported yet')
|
| 507 |
+
|
| 508 |
+
return train_dataset
|
| 509 |
+
|
| 510 |
+
def _spatial_addressable_indices_map(
|
| 511 |
+
self, spatial_dim_sizes: tuple[int, int, int]
|
| 512 |
+
) -> Mapping[jax.Device, tuple[slice, slice, slice, slice]]:
|
| 513 |
+
"""Get slices for indexing global arrays to local devices."""
|
| 514 |
+
spec = P('batch', *self.data_coords.physics_partition_spec)
|
| 515 |
+
sharding = jax.sharding.NamedSharding(self.spmd_mesh, spec)
|
| 516 |
+
global_shape = (self.global_batch_size,) + spatial_dim_sizes
|
| 517 |
+
indices_map = sharding.addressable_devices_indices_map(global_shape)
|
| 518 |
+
indices_map = cast(
|
| 519 |
+
Mapping[jax.Device, tuple[slice, slice, slice, slice]], indices_map
|
| 520 |
+
)
|
| 521 |
+
return indices_map
|
| 522 |
+
|
| 523 |
+
def _read_model_parallel_dataset(
|
| 524 |
+
self,
|
| 525 |
+
dataset: xarray.Dataset,
|
| 526 |
+
read_shard: Callable[[xarray.Dataset, int], tf.data.Dataset],
|
| 527 |
+
) -> tuple[tf.data.Dataset, xarray.Dataset]:
|
| 528 |
+
"""Read a shard of a training dataset into tf.data.Dataset."""
|
| 529 |
+
indices_map = self._spatial_addressable_indices_map(
|
| 530 |
+
tuple(dataset.sizes[k] for k in ['level', 'longitude', 'latitude'])
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
shard_data: list[tf.data.Dataset] = []
|
| 534 |
+
for device in jax.local_devices():
|
| 535 |
+
indices = indices_map[device]
|
| 536 |
+
batch_index = indices[0].start or 0
|
| 537 |
+
selection = dict(zip(['level', 'longitude', 'latitude'], indices[1:]))
|
| 538 |
+
shard_dataset = dataset.isel(selection)
|
| 539 |
+
shard_data.append(read_shard(shard_dataset, batch_index))
|
| 540 |
+
|
| 541 |
+
choices = tf.data.Dataset.range(jax.local_device_count()).repeat()
|
| 542 |
+
data = tf.data.Dataset.choose_from_datasets(shard_data, choices)
|
| 543 |
+
template = shard_dataset
|
| 544 |
+
return data, template
|
| 545 |
+
|
| 546 |
+
def _build_train_inputs(
|
| 547 |
+
self, time_series_length: int
|
| 548 |
+
) -> tuple[Callable[[], Any], dict[str, Any]]:
|
| 549 |
+
"""Loads the training dataset and returns an iterator and train_attrs."""
|
| 550 |
+
train_dataset = self._get_train_dataset()
|
| 551 |
+
local_batch_size = (
|
| 552 |
+
# Size of data needed to satisfy batch_size_per_device.
|
| 553 |
+
self.config.batch_size_per_device
|
| 554 |
+
* jax.local_device_count()
|
| 555 |
+
)
|
| 556 |
+
if self.model_parallel_training:
|
| 557 |
+
|
| 558 |
+
def read_shard(shard_dataset, batch_index):
|
| 559 |
+
return self._read_shuffled_shard(
|
| 560 |
+
shard_dataset,
|
| 561 |
+
time_series_length,
|
| 562 |
+
min_buffer_blocks=1,
|
| 563 |
+
shard_index=batch_index,
|
| 564 |
+
shard_count=self.global_batch_size,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
data, template = self._read_model_parallel_dataset(
|
| 568 |
+
train_dataset, read_shard
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
else:
|
| 572 |
+
data = self._read_shuffled_shard(
|
| 573 |
+
train_dataset,
|
| 574 |
+
time_series_length,
|
| 575 |
+
shard_index=jax.process_index(),
|
| 576 |
+
shard_count=jax.process_count(),
|
| 577 |
+
min_buffer_blocks=local_batch_size,
|
| 578 |
+
)
|
| 579 |
+
template = train_dataset
|
| 580 |
+
|
| 581 |
+
data = data.repeat()
|
| 582 |
+
data = data.batch(local_batch_size, drop_remainder=True)
|
| 583 |
+
data = data.prefetch(tf.data.AUTOTUNE)
|
| 584 |
+
train_iter = self._to_dataset_iter(data, template)
|
| 585 |
+
data_attrs = datasets.attrs_from_dataset(train_dataset, time_series_length)
|
| 586 |
+
return train_iter, data_attrs
|
| 587 |
+
|
| 588 |
+
def build_train_and_eval_iterators(
|
| 589 |
+
self,
|
| 590 |
+
schedule_idx: int,
|
| 591 |
+
start_step: int,
|
| 592 |
+
large_eval: bool,
|
| 593 |
+
) -> TrainEvalIteratorTuple:
|
| 594 |
+
"""Build new iterators for training at schedule_idx.
|
| 595 |
+
|
| 596 |
+
Args:
|
| 597 |
+
schedule_idx: Index into the rollout schedule.
|
| 598 |
+
start_step: Step at which this training run started at. This does not
|
| 599 |
+
change unless the Borg job dies and restarts.
|
| 600 |
+
large_eval: Whether this evaluation should be done over a larger set of
|
| 601 |
+
data.
|
| 602 |
+
|
| 603 |
+
Returns:
|
| 604 |
+
TrainEvalIteratorTuple: Tuple consisting of
|
| 605 |
+
get_train_data. Iterator providing next set of training data.
|
| 606 |
+
train_step_fn. train_utils.TrainStepFunction to update weights.
|
| 607 |
+
evaluate_fn. Callable to evalate metrics and write results.
|
| 608 |
+
ckpt_kwargs. dict[str, Any] of kwargs to add to the checkpoint.
|
| 609 |
+
"""
|
| 610 |
+
num_train_time_steps = self.config.train_schedule_time_steps[schedule_idx]
|
| 611 |
+
trajectory_length = self._trajectory_lengths[schedule_idx]
|
| 612 |
+
|
| 613 |
+
train_traj_spec = metrics_util.TrajectorySpec(
|
| 614 |
+
trajectory_length,
|
| 615 |
+
self._max_trajectory_length,
|
| 616 |
+
self.train_inner_steps,
|
| 617 |
+
coords=self.model_specs.coords,
|
| 618 |
+
data_coords=self.data_coords,
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
# These only change on the first and last rollout, but re-make them anyways.
|
| 622 |
+
get_eval_data, eval_attrs = self.build_eval_inputs(
|
| 623 |
+
self.config.eval_dataset_time_slice,
|
| 624 |
+
large_eval,
|
| 625 |
+
)
|
| 626 |
+
get_eval_on_train, _ = self.build_eval_inputs(
|
| 627 |
+
self.config.train_dataset_time_slice,
|
| 628 |
+
large_eval,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
evaluate_fn = functools.partial(
|
| 632 |
+
self.evaluate,
|
| 633 |
+
eval_batch_fn=self.get_eval_batch_fn(train_traj_spec),
|
| 634 |
+
get_eval_data=get_eval_data,
|
| 635 |
+
get_train_data=get_eval_on_train,
|
| 636 |
+
large_eval=large_eval,
|
| 637 |
+
)
|
| 638 |
+
get_train_data, train_attrs = self._build_train_inputs(trajectory_length)
|
| 639 |
+
ckpt_kwargs = {
|
| 640 |
+
'train_attrs': train_attrs,
|
| 641 |
+
'eval_attrs': eval_attrs,
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
train_step_fn = self.get_train_step_fn(
|
| 645 |
+
num_train_time_steps, train_traj_spec
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
if (
|
| 649 |
+
self.config.profile_with_xprof
|
| 650 |
+
and schedule_idx == 0
|
| 651 |
+
and experiment_utils.is_coordinator()
|
| 652 |
+
and start_step == 0
|
| 653 |
+
):
|
| 654 |
+
train_step_fn = profiling_util.Traced(
|
| 655 |
+
train_step_fn, # only the initial train_step is profiled.
|
| 656 |
+
trace_name='train step',
|
| 657 |
+
skip_steps=2, # avoid JIT compilation
|
| 658 |
+
num_trace_steps=3,
|
| 659 |
+
enable_python_tracer=True,
|
| 660 |
+
host_trace_level=3,
|
| 661 |
+
)
|
| 662 |
+
return get_train_data(), train_step_fn, evaluate_fn, ckpt_kwargs
|
| 663 |
+
|
| 664 |
+
def _get_eval_dataset(self) -> xarray.Dataset:
|
| 665 |
+
return xarray_utils.ds_with_sim_time(
|
| 666 |
+
self.eval_ds, self.physics_specs, self.reference_datetime
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
def build_eval_inputs(
|
| 670 |
+
self,
|
| 671 |
+
dataset_time_slice: tuple[str, str] | None,
|
| 672 |
+
large_eval: bool,
|
| 673 |
+
) -> tuple[Callable[[], Any], Any]:
|
| 674 |
+
"""Returns an iterable over the data and data attrs for evaluation."""
|
| 675 |
+
eval_dataset = self._get_eval_dataset()
|
| 676 |
+
|
| 677 |
+
num_eval_batches = self.num_eval_batches(large_eval)
|
| 678 |
+
eval_batch_size_per_device = self.eval_batch_size_per_device(large_eval)
|
| 679 |
+
time_series_length = (
|
| 680 |
+
self._eval_trajectory_length + self.config.num_init_frames - 1
|
| 681 |
+
)
|
| 682 |
+
local_batch_size = eval_batch_size_per_device * jax.local_device_count()
|
| 683 |
+
|
| 684 |
+
if isinstance(eval_dataset.indexes['time'], pd.DatetimeIndex):
|
| 685 |
+
# For real world training data from ERA5, carefully sample starting
|
| 686 |
+
# times to ensure they are equally spaced across the year.
|
| 687 |
+
assert 'sample' not in eval_dataset.dims
|
| 688 |
+
logging.info('Using eval data loader for build_eval_inputs')
|
| 689 |
+
sample_count = self.global_eval_batch_size(large_eval) * num_eval_batches
|
| 690 |
+
if dataset_time_slice:
|
| 691 |
+
time_source = eval_dataset.time.loc[slice(*dataset_time_slice)]
|
| 692 |
+
else:
|
| 693 |
+
time_source = eval_dataset
|
| 694 |
+
first_start = time_source.indexes['time'][0]
|
| 695 |
+
last_start = time_source.indexes['time'][-1]
|
| 696 |
+
starts = _get_datetime_forecast_starts(
|
| 697 |
+
sample_count, first_start, last_start
|
| 698 |
+
)
|
| 699 |
+
logging.info(f'determined evaluation data for {sample_count=}: {starts=}')
|
| 700 |
+
offsets = eval_dataset.indexes['time'].get_indexer(starts)
|
| 701 |
+
sampler = reader.WindowerAtOffsets(
|
| 702 |
+
window_size=time_series_length, window_offsets=offsets
|
| 703 |
+
)
|
| 704 |
+
if self.model_parallel_training:
|
| 705 |
+
|
| 706 |
+
def read_shard(shard_dataset, batch_index):
|
| 707 |
+
selector = reader.ShardSelector(batch_index, len(starts))
|
| 708 |
+
return reader.read_timeseries(shard_dataset, sampler, selector)
|
| 709 |
+
|
| 710 |
+
data, template = self._read_model_parallel_dataset(
|
| 711 |
+
eval_dataset, read_shard
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
else:
|
| 715 |
+
selector = reader.ShardSelector(
|
| 716 |
+
jax.process_index(), jax.process_count()
|
| 717 |
+
)
|
| 718 |
+
data = reader.read_timeseries(eval_dataset, sampler, selector)
|
| 719 |
+
template = eval_dataset
|
| 720 |
+
|
| 721 |
+
data = data.batch(local_batch_size, drop_remainder=True)
|
| 722 |
+
data = data.cache()
|
| 723 |
+
else:
|
| 724 |
+
# For synthetic datasets (e.g., from Held-Suarez), use the same shuffling
|
| 725 |
+
# we use for reading training data.
|
| 726 |
+
assert not self.model_parallel_training
|
| 727 |
+
if dataset_time_slice:
|
| 728 |
+
eval_dataset = eval_dataset.sel(time=slice(*dataset_time_slice))
|
| 729 |
+
data = self._read_shuffled_shard(
|
| 730 |
+
eval_dataset,
|
| 731 |
+
time_series_length,
|
| 732 |
+
shard_index=jax.process_index(),
|
| 733 |
+
shard_count=jax.process_count(),
|
| 734 |
+
min_buffer_blocks=local_batch_size * num_eval_batches,
|
| 735 |
+
)
|
| 736 |
+
data = data.batch(local_batch_size, drop_remainder=True)
|
| 737 |
+
data = data.take(num_eval_batches)
|
| 738 |
+
template = eval_dataset
|
| 739 |
+
|
| 740 |
+
logging.info(f'created eval data: {data}')
|
| 741 |
+
eval_iter = self._to_dataset_iter(data, template)
|
| 742 |
+
data_attrs = datasets.attrs_from_dataset(eval_dataset, time_series_length)
|
| 743 |
+
return eval_iter, data_attrs
|
| 744 |
+
|
| 745 |
+
#
|
| 746 |
+
# Training and evaluation methods.
|
| 747 |
+
#
|
| 748 |
+
|
| 749 |
+
def _make_initial_experiment_state(
|
| 750 |
+
self,
|
| 751 |
+
rng: typing.PRNGKeyArray,
|
| 752 |
+
init_example,
|
| 753 |
+
init_forcing_data: typing.ForcingData,
|
| 754 |
+
init_params: Optional[typing.Params] = None,
|
| 755 |
+
) -> ExperimentState:
|
| 756 |
+
"""Makes initial parameters (via hk.Module.init)."""
|
| 757 |
+
if self.eval_inner_steps != self.train_inner_steps:
|
| 758 |
+
raise ValueError(
|
| 759 |
+
'KroneckerCorrelatedL2LossModule stddev will be ill defined since '
|
| 760 |
+
f'{self.eval_inner_steps=} != {self.train_inner_steps=}'
|
| 761 |
+
)
|
| 762 |
+
trajectory_length = list(init_example.values())[0].shape[0]
|
| 763 |
+
|
| 764 |
+
@jax.jit
|
| 765 |
+
def init(rng, init_example, init_forcing_data):
|
| 766 |
+
outer_steps = (trajectory_length - self.config.num_init_frames) + 1
|
| 767 |
+
# We need an "ensemble" dimension for stochastic losses, but parameters
|
| 768 |
+
# are fully replicated across the ensemble.
|
| 769 |
+
if init_params is None:
|
| 770 |
+
init_fn = jax.vmap(
|
| 771 |
+
self._make_unbatched_trajectory_fn(outer_steps).init,
|
| 772 |
+
in_axes=None,
|
| 773 |
+
out_axes=0,
|
| 774 |
+
spmd_axis_name='ensemble',
|
| 775 |
+
axis_size=1,
|
| 776 |
+
)
|
| 777 |
+
unsqueezd_params = init_fn(rng, init_example, init_forcing_data)
|
| 778 |
+
params = tree_map(lambda x: jnp.squeeze(x, axis=0), unsqueezd_params)
|
| 779 |
+
else:
|
| 780 |
+
params = init_params
|
| 781 |
+
opt_state = self.optimizer.init(params)
|
| 782 |
+
_, ema_state = self._ema_init(None, params)
|
| 783 |
+
experiment_state = ExperimentState(opt_state, params, ema_state)
|
| 784 |
+
experiment_state = train_utils.ensure_replicated(
|
| 785 |
+
experiment_state, mesh=self.spmd_mesh
|
| 786 |
+
)
|
| 787 |
+
return experiment_state
|
| 788 |
+
|
| 789 |
+
return init(rng, init_example, init_forcing_data)
|
| 790 |
+
|
| 791 |
+
def _make_unbatched_trajectory_fn(self, outer_steps: int):
|
| 792 |
+
"""Haiku transformation of func giving (prediction, target) trajectories.
|
| 793 |
+
|
| 794 |
+
Args:
|
| 795 |
+
outer_steps: Number of outer steps the trajectory should take.
|
| 796 |
+
|
| 797 |
+
Returns:
|
| 798 |
+
hk transformed object. The .apply member maps
|
| 799 |
+
(params, rng, target, forcing_data) --> (prediction, target)
|
| 800 |
+
"""
|
| 801 |
+
if self.train_inner_steps != self.eval_inner_steps:
|
| 802 |
+
# We share a trajectory for train/eval...so the spacing better be equal.
|
| 803 |
+
raise ValueError(f'{self.train_inner_steps=} != {self.eval_inner_steps=}')
|
| 804 |
+
|
| 805 |
+
@hk.transform
|
| 806 |
+
def unbatched_trajectory_fn(target, forcing_data):
|
| 807 |
+
"""Compute Fwd(target[0]) on one single batch/device."""
|
| 808 |
+
# Shapes(target) ~ (n_t, n_z, n_m, n_l)
|
| 809 |
+
model = self.whirl_model.model_cls()
|
| 810 |
+
_, predicted_trajectory = self._trajectory_fwd(
|
| 811 |
+
x=target,
|
| 812 |
+
forcing_data=forcing_data,
|
| 813 |
+
model=model,
|
| 814 |
+
outer_steps=outer_steps,
|
| 815 |
+
inner_steps=self.train_inner_steps,
|
| 816 |
+
)
|
| 817 |
+
prediction, target = (
|
| 818 |
+
model_utils.compute_prediction_and_target_representations(
|
| 819 |
+
predicted_trajectory, target, forcing_data, model
|
| 820 |
+
)
|
| 821 |
+
)
|
| 822 |
+
return prediction, target
|
| 823 |
+
|
| 824 |
+
return unbatched_trajectory_fn
|
| 825 |
+
|
| 826 |
+
def _make_batch_trajectory_fn(
|
| 827 |
+
self,
|
| 828 |
+
outer_steps: int,
|
| 829 |
+
) -> TrajectoryFn:
|
| 830 |
+
"""Target, prediction representations with shape (batch, ensemble, ...)."""
|
| 831 |
+
|
| 832 |
+
ensembled_fn = jax.vmap(
|
| 833 |
+
# (params, rng, target, forcing_data) --> (prediction, target)
|
| 834 |
+
self._make_unbatched_trajectory_fn(outer_steps).apply,
|
| 835 |
+
in_axes=(None, 0, None, None),
|
| 836 |
+
spmd_axis_name='ensemble',
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
batch_ensembled_fn = jax.vmap(
|
| 840 |
+
# (params, rng, target, forcing_data) --> (prediction, target)
|
| 841 |
+
# Input shapes are:
|
| 842 |
+
# params: (...)
|
| 843 |
+
# rng: (batch, ensemble, ...)
|
| 844 |
+
# target: (batch, time, ...)
|
| 845 |
+
# forcing_data: (batch, time, ...)
|
| 846 |
+
ensembled_fn,
|
| 847 |
+
in_axes=(None, 0, 0, 0),
|
| 848 |
+
spmd_axis_name='batch',
|
| 849 |
+
)
|
| 850 |
+
return batch_ensembled_fn
|
| 851 |
+
|
| 852 |
+
def get_train_step_fn(
|
| 853 |
+
self,
|
| 854 |
+
num_train_time_steps: int,
|
| 855 |
+
traj_spec: metrics_util.TrajectorySpec,
|
| 856 |
+
) -> train_utils.TrainStepFunction:
|
| 857 |
+
"""Makes a function to update weights via gradient descent.
|
| 858 |
+
|
| 859 |
+
This function makes use of on-device batching. Multiple devices are combined
|
| 860 |
+
via an all reduce step whereby the average (across devices) gradient is
|
| 861 |
+
applied to each (on device) params.
|
| 862 |
+
|
| 863 |
+
Args:
|
| 864 |
+
num_train_time_steps: Number of time steps for the trajectory in this
|
| 865 |
+
training step.
|
| 866 |
+
traj_spec: Specification of training trajectory.
|
| 867 |
+
|
| 868 |
+
Returns:
|
| 869 |
+
all_reduced_train_step: Function to exectute one training step.
|
| 870 |
+
Params are injected into self._trajectory_fwd and Loss (if Loss requires
|
| 871 |
+
Haiku params).
|
| 872 |
+
"""
|
| 873 |
+
|
| 874 |
+
batch_trajectory_fn = self._make_batch_trajectory_fn(
|
| 875 |
+
# (params, rng, target, forcing_data) --> [prediction, target]
|
| 876 |
+
outer_steps=num_train_time_steps // self.train_inner_steps
|
| 877 |
+
+ 1,
|
| 878 |
+
)
|
| 879 |
+
|
| 880 |
+
loss_fn = get_loss_obj(traj_spec).evaluate
|
| 881 |
+
ensembled_loss_fn = jax.vmap(
|
| 882 |
+
loss_fn,
|
| 883 |
+
axis_name='ensemble',
|
| 884 |
+
spmd_axis_name='ensemble',
|
| 885 |
+
)
|
| 886 |
+
batch_ensembled_loss_fn = jax.vmap(
|
| 887 |
+
ensembled_loss_fn,
|
| 888 |
+
axis_name='batch',
|
| 889 |
+
spmd_axis_name='batch',
|
| 890 |
+
)
|
| 891 |
+
|
| 892 |
+
def batched_parameter_loss_fn(params, rng, target, forcing_data):
|
| 893 |
+
"""Mean (over on-device batch members) of loss w.r.t parameters."""
|
| 894 |
+
# Input shapes are:
|
| 895 |
+
# params: (...)
|
| 896 |
+
# rng: (batch, ensemble, ...)
|
| 897 |
+
# target: (batch, time, ...)
|
| 898 |
+
# forcing_data: (batch, time, ...)
|
| 899 |
+
prediction, target = batch_trajectory_fn(
|
| 900 |
+
# The `target` and prediction returned are TrajectoryRepresentations.
|
| 901 |
+
# So don't just re-use the arg `target`.
|
| 902 |
+
params,
|
| 903 |
+
rng,
|
| 904 |
+
target,
|
| 905 |
+
forcing_data,
|
| 906 |
+
)
|
| 907 |
+
# dimensions (batch, ensemble)
|
| 908 |
+
per_example_loss = batch_ensembled_loss_fn(prediction, target)
|
| 909 |
+
# Average over ensemble and batch dimensions (technically, we don't have
|
| 910 |
+
# to average over ensemble with our current stochastic losses, but these
|
| 911 |
+
# values are already identical and this is cleaner than using array
|
| 912 |
+
# indexing)
|
| 913 |
+
overall_loss = jnp.mean(per_example_loss, axis=(0, 1))
|
| 914 |
+
assert overall_loss.ndim == 0
|
| 915 |
+
return overall_loss
|
| 916 |
+
|
| 917 |
+
# We would use donate_argnums here to update experiment_state in-place, but
|
| 918 |
+
# that would mean we could not save the checkpoint in a separable thread.
|
| 919 |
+
# Fortunately experiment_state is usually not too big (~100 MB).
|
| 920 |
+
@train_utils.jit_once
|
| 921 |
+
def train_step(experiment_state, rng, target_trajectory, forcing_data):
|
| 922 |
+
opt_state, params, ema_state = experiment_state
|
| 923 |
+
rng = train_utils.ensure_sharded_rng_key(rng, mesh=self.spmd_mesh)
|
| 924 |
+
loss, grad = jax.value_and_grad(batched_parameter_loss_fn)(
|
| 925 |
+
params, rng, target_trajectory, forcing_data
|
| 926 |
+
)
|
| 927 |
+
updates, opt_state = self.optimizer.update(grad, opt_state, params)
|
| 928 |
+
params = optax.apply_updates(params, updates)
|
| 929 |
+
_, ema_state = self._ema_update(params, ema_state)
|
| 930 |
+
experiment_state = ExperimentState(opt_state, params, ema_state)
|
| 931 |
+
experiment_state = train_utils.ensure_replicated(
|
| 932 |
+
experiment_state, mesh=self.spmd_mesh
|
| 933 |
+
)
|
| 934 |
+
return experiment_state, loss
|
| 935 |
+
|
| 936 |
+
return train_step
|
| 937 |
+
|
| 938 |
+
def get_eval_batch_fn(
|
| 939 |
+
self,
|
| 940 |
+
train_traj_spec: metrics_util.TrajectorySpec,
|
| 941 |
+
) -> train_utils.EvalStepFunction:
|
| 942 |
+
"""Makes a function that performs a single evaluation pass.
|
| 943 |
+
|
| 944 |
+
Args:
|
| 945 |
+
train_traj_spec: TrajectorySpec for training. Used to add the "loss"
|
| 946 |
+
evaluation metrics.
|
| 947 |
+
|
| 948 |
+
Returns:
|
| 949 |
+
Function mapping (params, rng, target, forcing_data) to dictionary of
|
| 950 |
+
scalar metric values. Parameters are injected into self._trajectory_fwd
|
| 951 |
+
and Loss (if Loss requires Haiku params).
|
| 952 |
+
"""
|
| 953 |
+
eval_traj_spec = metrics_util.TrajectorySpec(
|
| 954 |
+
self._eval_trajectory_length,
|
| 955 |
+
self._max_trajectory_length,
|
| 956 |
+
steps_per_save=self.eval_inner_steps,
|
| 957 |
+
coords=self.model_specs.coords,
|
| 958 |
+
data_coords=self.data_coords,
|
| 959 |
+
)
|
| 960 |
+
|
| 961 |
+
eval_time_steps = [
|
| 962 |
+
t // self.eval_inner_steps for t in self.config.eval_time_steps
|
| 963 |
+
]
|
| 964 |
+
if any(t % self.eval_inner_steps for t in self.config.eval_time_steps):
|
| 965 |
+
raise ValueError(
|
| 966 |
+
f'cannot evaluate {self.config.eval_time_steps=} with '
|
| 967 |
+
f'{self.eval_inner_steps=}'
|
| 968 |
+
)
|
| 969 |
+
|
| 970 |
+
batch_trajectory_fn = self._make_batch_trajectory_fn(
|
| 971 |
+
# (params, rng, target, forcing_data) --> [prediction, target]
|
| 972 |
+
outer_steps=max(
|
| 973 |
+
self._eval_trajectory_length,
|
| 974 |
+
train_traj_spec.trajectory_length,
|
| 975 |
+
),
|
| 976 |
+
)
|
| 977 |
+
|
| 978 |
+
def unbatched_eval_fn(
|
| 979 |
+
prediction: TrajectoryRepresentations, target: TrajectoryRepresentations
|
| 980 |
+
):
|
| 981 |
+
"""Evaluate(target, Fwd(target[0])) on one single batch/device."""
|
| 982 |
+
metrics_dict = get_metrics_dict(
|
| 983 |
+
eval_traj_spec,
|
| 984 |
+
eval_time_steps,
|
| 985 |
+
get_loss_obj(train_traj_spec),
|
| 986 |
+
is_ensemble_data=bool(self.config.ensemble_size),
|
| 987 |
+
)
|
| 988 |
+
return train_utils.flatten_dict({
|
| 989 |
+
k: metric.evaluate(prediction, target)
|
| 990 |
+
for k, metric in metrics_dict.items()
|
| 991 |
+
})
|
| 992 |
+
|
| 993 |
+
ensembled_fn = jax.vmap(
|
| 994 |
+
unbatched_eval_fn, axis_name='ensemble', spmd_axis_name='ensemble'
|
| 995 |
+
)
|
| 996 |
+
|
| 997 |
+
batch_ensembled_fn = jax.vmap(
|
| 998 |
+
ensembled_fn,
|
| 999 |
+
axis_name='batch',
|
| 1000 |
+
spmd_axis_name='batch',
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
@train_utils.jit_once
|
| 1004 |
+
def batch_mean_eval_fn(params, rng, target, forcing_data):
|
| 1005 |
+
"""Computes mean (over batch members) of evaluation."""
|
| 1006 |
+
# Input shapes for this function are:
|
| 1007 |
+
# params: (...)
|
| 1008 |
+
# rng: (batch, ensemble, ...)
|
| 1009 |
+
# target: (batch, time, ...)
|
| 1010 |
+
# forcing_data: (batch, time, ...)
|
| 1011 |
+
rng = train_utils.ensure_sharded_rng_key(rng, mesh=self.spmd_mesh)
|
| 1012 |
+
prediction, target = batch_trajectory_fn(
|
| 1013 |
+
# The `target` and prediction returned are TrajectoryRepresentations.
|
| 1014 |
+
# So don't just re-use the arg `target`.
|
| 1015 |
+
params,
|
| 1016 |
+
rng,
|
| 1017 |
+
target,
|
| 1018 |
+
forcing_data,
|
| 1019 |
+
)
|
| 1020 |
+
batch_eval_values = batch_ensembled_fn(prediction, target)
|
| 1021 |
+
return tree_map(jnp.mean, batch_eval_values)
|
| 1022 |
+
|
| 1023 |
+
return batch_mean_eval_fn
|
| 1024 |
+
|
| 1025 |
+
def run_training(self):
|
| 1026 |
+
"""See base class."""
|
| 1027 |
+
(
|
| 1028 |
+
start_step,
|
| 1029 |
+
times_restarted_on_nan,
|
| 1030 |
+
step_auto_restart_began_at,
|
| 1031 |
+
experiment_state,
|
| 1032 |
+
) = self.initialize_experiment(
|
| 1033 |
+
initial_checkpoint_path=self.config.initial_checkpoint_path
|
| 1034 |
+
)
|
| 1035 |
+
|
| 1036 |
+
if start_step >= self.config.num_training_steps:
|
| 1037 |
+
logging.warning(
|
| 1038 |
+
f'Attempting to start training at {start_step=} >='
|
| 1039 |
+
f' {self.config.num_training_steps=}. Will simply return'
|
| 1040 |
+
)
|
| 1041 |
+
return
|
| 1042 |
+
|
| 1043 |
+
def logging_callback(step, loss, times_restarted_on_nan):
|
| 1044 |
+
loss = float(jax.device_get(loss))
|
| 1045 |
+
if step % max(self.steps_between_evals(False) // 100, 1) == 0:
|
| 1046 |
+
logging.info(f'{step=}, {loss=}')
|
| 1047 |
+
if (
|
| 1048 |
+
self.config.error_with_nan_loss
|
| 1049 |
+
and times_restarted_on_nan > self.config.max_nan_restarts
|
| 1050 |
+
):
|
| 1051 |
+
raise RuntimeError(
|
| 1052 |
+
f'NaN loss detected at {step=}, after too many restarts since'
|
| 1053 |
+
f' {times_restarted_on_nan=} >'
|
| 1054 |
+
f' {self.config.max_nan_restarts=}. Aborting.'
|
| 1055 |
+
)
|
| 1056 |
+
|
| 1057 |
+
# monitor loss using a separate thread, so it doesn't block execution
|
| 1058 |
+
logging_stream = streaming.SingleThreadExecutor(logging_callback)
|
| 1059 |
+
logging.info('starting training from step=%s', start_step)
|
| 1060 |
+
train_step_timer = timing_util.Timer()
|
| 1061 |
+
|
| 1062 |
+
rng_stream = train_utils.BatchedPRNGSequence(
|
| 1063 |
+
jax.random.PRNGKey(self.config.init_rng_seed),
|
| 1064 |
+
batch_shape=(self.global_batch_size, self.config.ensemble_size or 1),
|
| 1065 |
+
)
|
| 1066 |
+
|
| 1067 |
+
loss = 1.0 # setting to a non-nan value when starting an experiment.
|
| 1068 |
+
schedule_idx = None
|
| 1069 |
+
ckpt_kwargs = {}
|
| 1070 |
+
|
| 1071 |
+
step = start_step
|
| 1072 |
+
while step < self.config.num_training_steps:
|
| 1073 |
+
old_schedule_idx = schedule_idx
|
| 1074 |
+
schedule_idx = np.sum( # compute which leg of the schedule we are at.
|
| 1075 |
+
step > np.asarray(self.config.train_schedule_boundaries)
|
| 1076 |
+
)
|
| 1077 |
+
large_eval = (
|
| 1078 |
+
schedule_idx == len(self.config.train_schedule_time_steps) - 1
|
| 1079 |
+
)
|
| 1080 |
+
if schedule_idx != old_schedule_idx:
|
| 1081 |
+
(
|
| 1082 |
+
train_iter,
|
| 1083 |
+
train_step_fn,
|
| 1084 |
+
evaluate_fn,
|
| 1085 |
+
ckpt_kwargs,
|
| 1086 |
+
) = self.build_train_and_eval_iterators(
|
| 1087 |
+
schedule_idx=schedule_idx,
|
| 1088 |
+
start_step=start_step,
|
| 1089 |
+
large_eval=large_eval,
|
| 1090 |
+
)
|
| 1091 |
+
|
| 1092 |
+
if (
|
| 1093 |
+
np.isnan(loss)
|
| 1094 |
+
# No sense re-initializing if we've restarted a bunch already. Also,
|
| 1095 |
+
# note that if error_with_nan_loss=True, we should raise and not have
|
| 1096 |
+
# worry about the times_restarted_on_nan < max_nan_restarts here.
|
| 1097 |
+
and times_restarted_on_nan <= self.config.max_nan_restarts
|
| 1098 |
+
):
|
| 1099 |
+
# See also logging_callback, which may raise RuntimeError for NaN loss.
|
| 1100 |
+
logging.warning(
|
| 1101 |
+
f'NaN loss encountered at {step=}. Re-initializing and incrementing'
|
| 1102 |
+
f' times_restarted_on_nan to {times_restarted_on_nan + 1}'
|
| 1103 |
+
)
|
| 1104 |
+
times_restarted_on_nan += 1
|
| 1105 |
+
step, _, _, experiment_state = self.initialize_experiment(
|
| 1106 |
+
target_step=step
|
| 1107 |
+
- times_restarted_on_nan * self.config.restart_lookback_steps
|
| 1108 |
+
)
|
| 1109 |
+
step_auto_restart_began_at = step_auto_restart_began_at or step
|
| 1110 |
+
|
| 1111 |
+
# checkpoint
|
| 1112 |
+
if step % self.steps_between_evals(large_eval) == 0:
|
| 1113 |
+
self.save_checkpoint(
|
| 1114 |
+
step,
|
| 1115 |
+
experiment_state,
|
| 1116 |
+
checkpoint_buffer_size=self.config.checkpoint_buffer_size,
|
| 1117 |
+
times_restarted_on_nan=times_restarted_on_nan,
|
| 1118 |
+
step_auto_restart_began_at=step_auto_restart_began_at,
|
| 1119 |
+
**ckpt_kwargs,
|
| 1120 |
+
)
|
| 1121 |
+
elif step % self.config.steps_between_checkpoints == 0:
|
| 1122 |
+
max_lookback_step = (
|
| 1123 |
+
step
|
| 1124 |
+
- self.config.max_nan_restarts * self.config.restart_lookback_steps
|
| 1125 |
+
)
|
| 1126 |
+
if (
|
| 1127 |
+
# If there is no chance of a restart sequence overlapping with
|
| 1128 |
+
# previously used checkpoints...
|
| 1129 |
+
times_restarted_on_nan
|
| 1130 |
+
and max_lookback_step > step_auto_restart_began_at
|
| 1131 |
+
):
|
| 1132 |
+
logging.info(
|
| 1133 |
+
f'Significant progress made since {step_auto_restart_began_at=}.'
|
| 1134 |
+
f' In particular, {step=} Therefore set times_restarted_on_nan'
|
| 1135 |
+
' to 0'
|
| 1136 |
+
)
|
| 1137 |
+
times_restarted_on_nan = 0
|
| 1138 |
+
step_auto_restart_began_at = None
|
| 1139 |
+
self.save_checkpoint(
|
| 1140 |
+
step,
|
| 1141 |
+
experiment_state,
|
| 1142 |
+
update_latest_only=True,
|
| 1143 |
+
checkpoint_buffer_size=self.config.checkpoint_buffer_size,
|
| 1144 |
+
times_restarted_on_nan=times_restarted_on_nan,
|
| 1145 |
+
step_auto_restart_began_at=step_auto_restart_began_at,
|
| 1146 |
+
**ckpt_kwargs,
|
| 1147 |
+
)
|
| 1148 |
+
|
| 1149 |
+
# evaluate
|
| 1150 |
+
if (step + 1) % self.steps_between_evals(large_eval) == 0:
|
| 1151 |
+
if step > start_step:
|
| 1152 |
+
with train_step_timer:
|
| 1153 |
+
# train_step is non-blocking, so we need to block on the output
|
| 1154 |
+
# of the previous training step to reliably time it.
|
| 1155 |
+
experiment_state = jax.block_until_ready(experiment_state)
|
| 1156 |
+
eval_interval = self.steps_between_evals(large_eval)
|
| 1157 |
+
training_time = train_step_timer.total
|
| 1158 |
+
logging.info(
|
| 1159 |
+
f'training for {eval_interval} steps took '
|
| 1160 |
+
f'{training_time:.1f} seconds'
|
| 1161 |
+
)
|
| 1162 |
+
self.record_scalar(
|
| 1163 |
+
'train',
|
| 1164 |
+
tag='seconds_per_train_step',
|
| 1165 |
+
step=step,
|
| 1166 |
+
value=training_time / eval_interval,
|
| 1167 |
+
)
|
| 1168 |
+
train_step_timer = timing_util.Timer() # reset
|
| 1169 |
+
|
| 1170 |
+
if isinstance(train_step_fn, profiling_util.Traced):
|
| 1171 |
+
memory_usage = train_step_fn.tracer.memory_usage # pytype: disable=attribute-error
|
| 1172 |
+
if memory_usage is not None:
|
| 1173 |
+
self.record_scalar(
|
| 1174 |
+
'train',
|
| 1175 |
+
tag='peak_memory_usage_mib',
|
| 1176 |
+
step=step,
|
| 1177 |
+
value=memory_usage,
|
| 1178 |
+
)
|
| 1179 |
+
|
| 1180 |
+
with timing_util.Timer() as eval_timer:
|
| 1181 |
+
evaluate_fn(step, experiment_state, seed=step)
|
| 1182 |
+
self.record_scalar(
|
| 1183 |
+
'train',
|
| 1184 |
+
tag='seconds_per_evaluation',
|
| 1185 |
+
step=step,
|
| 1186 |
+
value=eval_timer.average,
|
| 1187 |
+
)
|
| 1188 |
+
logging.info('evaluation pass took %.1f seconds', eval_timer.average)
|
| 1189 |
+
self.flush_writers() # flush all writers for this training step.
|
| 1190 |
+
|
| 1191 |
+
# train
|
| 1192 |
+
with train_step_timer:
|
| 1193 |
+
# go/xprof-instrument-jax
|
| 1194 |
+
with jax.profiler.StepTraceAnnotation('train', step_num=step):
|
| 1195 |
+
batch, forcing_data = self.to_global_array(
|
| 1196 |
+
next(train_iter), self.global_batch_size
|
| 1197 |
+
)
|
| 1198 |
+
# This is necessary else ValueError.
|
| 1199 |
+
# See http://sponge2/960c64a7-703f-4a19-8572-2c97dd9c01f3
|
| 1200 |
+
with self.spmd_mesh:
|
| 1201 |
+
experiment_state, loss = train_step_fn(
|
| 1202 |
+
experiment_state, next(rng_stream), batch, forcing_data
|
| 1203 |
+
)
|
| 1204 |
+
# If we don't device_get (or similar), asynchronous execution
|
| 1205 |
+
# resultsin this timed block taking almost no time. device_get does
|
| 1206 |
+
# not result in longer runs, since each loop must eventually compute
|
| 1207 |
+
# the loss, one way or another.
|
| 1208 |
+
loss = jax.device_get(loss)
|
| 1209 |
+
logging_stream.wait()
|
| 1210 |
+
logging_stream.put(step, loss, times_restarted_on_nan)
|
| 1211 |
+
step += 1
|
| 1212 |
+
# End of while step < self.config.num_training_steps:
|
| 1213 |
+
|
| 1214 |
+
evaluate_fn(self.config.num_training_steps, experiment_state)
|
| 1215 |
+
self.finalize_training(
|
| 1216 |
+
self.config.num_training_steps,
|
| 1217 |
+
experiment_state,
|
| 1218 |
+
times_restarted_on_nan=times_restarted_on_nan,
|
| 1219 |
+
step_auto_restart_began_at=step_auto_restart_began_at,
|
| 1220 |
+
**ckpt_kwargs,
|
| 1221 |
+
)
|
| 1222 |
+
|
| 1223 |
+
def make_dummy_inputs(self) -> tuple[Any, Any]:
|
| 1224 |
+
train_dataset = xarray_utils.ds_with_sim_time(
|
| 1225 |
+
self.train_ds, self.physics_specs, self.reference_datetime
|
| 1226 |
+
)
|
| 1227 |
+
dummy_ds = (
|
| 1228 |
+
train_dataset.drop_vars('time')
|
| 1229 |
+
.head(time=self.config.num_init_frames)
|
| 1230 |
+
.pipe(xarray.zeros_like) # replace data with zeros
|
| 1231 |
+
.pipe(datasets.drop_static_vars)
|
| 1232 |
+
.transpose('time', ...)
|
| 1233 |
+
)
|
| 1234 |
+
return self.from_xarray_fn(dummy_ds)
|
| 1235 |
+
|
| 1236 |
+
def initialize_experiment(
|
| 1237 |
+
self,
|
| 1238 |
+
initial_checkpoint_path: Optional[str] = None,
|
| 1239 |
+
target_step: Optional[int] = None,
|
| 1240 |
+
) -> tuple[int, int, int | None, ExperimentState]:
|
| 1241 |
+
"""Returns training step and experiment state from checkpoint or init."""
|
| 1242 |
+
|
| 1243 |
+
if target_step:
|
| 1244 |
+
ckpt = self.load_buffered_checkpoint(target_step=target_step)
|
| 1245 |
+
if ckpt is None:
|
| 1246 |
+
logging.info(
|
| 1247 |
+
'No acceptable buffered checkpoint found for {target_step=}'
|
| 1248 |
+
)
|
| 1249 |
+
else:
|
| 1250 |
+
logging.info(
|
| 1251 |
+
f'Using buffered checkpoint, which has step={ckpt.step}. Ideally '
|
| 1252 |
+
f'would have used {target_step=}'
|
| 1253 |
+
)
|
| 1254 |
+
else:
|
| 1255 |
+
ckpt = self.load_latest_checkpoint()
|
| 1256 |
+
if ckpt is None:
|
| 1257 |
+
logging.info('No latest checkpoint found')
|
| 1258 |
+
else:
|
| 1259 |
+
logging.info(f'Using latest checkpoint, which has step={ckpt.step}')
|
| 1260 |
+
|
| 1261 |
+
init_params = None
|
| 1262 |
+
if ckpt is None and initial_checkpoint_path is not None:
|
| 1263 |
+
ckpt = checkpoint.load_checkpoint(initial_checkpoint_path)
|
| 1264 |
+
if self.config.reset_initial_optimizer_state:
|
| 1265 |
+
init_params = ckpt.eval_params
|
| 1266 |
+
ckpt = None # if resetting optimizer, carry over only init_params.
|
| 1267 |
+
|
| 1268 |
+
if ckpt is not None:
|
| 1269 |
+
start_step = ckpt.step
|
| 1270 |
+
logging.info(f'resuming from checkpoint at step={start_step}')
|
| 1271 |
+
times_restarted_on_nan = getattr(ckpt, 'times_restarted_on_nan', 0)
|
| 1272 |
+
step_auto_restart_began_at = getattr(
|
| 1273 |
+
ckpt, 'step_auto_restart_began_at', None
|
| 1274 |
+
)
|
| 1275 |
+
experiment_state = ExperimentState(
|
| 1276 |
+
ckpt.opt_state, ckpt.train_params, ckpt.ema_state
|
| 1277 |
+
)
|
| 1278 |
+
else:
|
| 1279 |
+
logging.info('starting training with new weights')
|
| 1280 |
+
start_step = 0
|
| 1281 |
+
times_restarted_on_nan = 0
|
| 1282 |
+
step_auto_restart_began_at = None
|
| 1283 |
+
rng = jax.random.PRNGKey(self.config.init_rng_seed)
|
| 1284 |
+
init_example, init_forcing_data = self.make_dummy_inputs()
|
| 1285 |
+
experiment_state = self._make_initial_experiment_state(
|
| 1286 |
+
rng, init_example, init_forcing_data, init_params=init_params
|
| 1287 |
+
)
|
| 1288 |
+
|
| 1289 |
+
return (
|
| 1290 |
+
start_step,
|
| 1291 |
+
times_restarted_on_nan,
|
| 1292 |
+
step_auto_restart_began_at,
|
| 1293 |
+
experiment_state,
|
| 1294 |
+
)
|
| 1295 |
+
|
| 1296 |
+
def _checkpoint_state(
|
| 1297 |
+
self,
|
| 1298 |
+
step: int,
|
| 1299 |
+
experiment_state: ExperimentState,
|
| 1300 |
+
times_restarted_on_nan: int,
|
| 1301 |
+
step_auto_restart_began_at: int,
|
| 1302 |
+
train_attrs: dict[str, Any],
|
| 1303 |
+
eval_attrs: dict[str, Any],
|
| 1304 |
+
) -> checkpoint.CheckpointState:
|
| 1305 |
+
"""Returns a checkpoint state for a given experiment_state."""
|
| 1306 |
+
opt_state, params, ema_state = experiment_state
|
| 1307 |
+
ema_params, _ = self._ema_update(params, ema_state)
|
| 1308 |
+
ckpt_state = checkpoint.CheckpointState(
|
| 1309 |
+
train_params=params,
|
| 1310 |
+
eval_params=ema_params,
|
| 1311 |
+
opt_state=opt_state,
|
| 1312 |
+
ema_state=ema_state,
|
| 1313 |
+
step=step,
|
| 1314 |
+
model_time_step=self._model_dt,
|
| 1315 |
+
model_config_str=self._model_gin_config,
|
| 1316 |
+
train_dataset_path=self.config.train_dataset_path,
|
| 1317 |
+
eval_dataset_path=self.config.eval_dataset_path,
|
| 1318 |
+
times_restarted_on_nan=times_restarted_on_nan,
|
| 1319 |
+
step_auto_restart_began_at=step_auto_restart_began_at,
|
| 1320 |
+
train_attrs=train_attrs,
|
| 1321 |
+
eval_attrs=eval_attrs,
|
| 1322 |
+
)
|
| 1323 |
+
return ckpt_state
|
| 1324 |
+
|
| 1325 |
+
def evaluate(
|
| 1326 |
+
self,
|
| 1327 |
+
step,
|
| 1328 |
+
experiment_state,
|
| 1329 |
+
eval_batch_fn,
|
| 1330 |
+
get_eval_data,
|
| 1331 |
+
get_train_data,
|
| 1332 |
+
large_eval: bool,
|
| 1333 |
+
seed=0,
|
| 1334 |
+
):
|
| 1335 |
+
"""Evaluates the model on train and eval data and writes summaries.
|
| 1336 |
+
|
| 1337 |
+
Args:
|
| 1338 |
+
step: global training step.
|
| 1339 |
+
experiment_state: tuple of replicated step, optimizer state and EMA
|
| 1340 |
+
(exponentially moving average) state for model parameters.
|
| 1341 |
+
eval_batch_fn: function that, given parameters; rng; batch of data,
|
| 1342 |
+
computes evaluation metric of interest on the given samples.
|
| 1343 |
+
get_eval_data: callable that returns an iterator over evaluation data that
|
| 1344 |
+
is used for produce summaries on unseen evaluation data.
|
| 1345 |
+
get_train_data: callable that returns an iterator over training data that
|
| 1346 |
+
is used for produce summaries on training data.
|
| 1347 |
+
large_eval: Whether this evaluation is on the larger size eval data.
|
| 1348 |
+
seed: seed for the random number generator to be used for evaluation.
|
| 1349 |
+
"""
|
| 1350 |
+
num_eval_batches = self.num_eval_batches(large_eval)
|
| 1351 |
+
if num_eval_batches == 0:
|
| 1352 |
+
logging.warning(f'skipping evaluation: {num_eval_batches=}')
|
| 1353 |
+
return
|
| 1354 |
+
|
| 1355 |
+
_, params, ema_state = experiment_state
|
| 1356 |
+
ema_params, _ = self._ema_update(params, ema_state)
|
| 1357 |
+
|
| 1358 |
+
global_batch_size = self.global_eval_batch_size(large_eval)
|
| 1359 |
+
rng_stream = train_utils.BatchedPRNGSequence(
|
| 1360 |
+
jax.random.PRNGKey(seed),
|
| 1361 |
+
batch_shape=(global_batch_size, self.config.ensemble_size or 1),
|
| 1362 |
+
)
|
| 1363 |
+
to_global_array = functools.partial(
|
| 1364 |
+
self.to_global_array, global_batch_size=global_batch_size
|
| 1365 |
+
)
|
| 1366 |
+
|
| 1367 |
+
# In theory, the mesh context manager should not be necessary because we use
|
| 1368 |
+
# jit with sharded arrays (rather than xmap or pjit), but it seems to be
|
| 1369 |
+
# required to avoid triggering bugs in JAX.
|
| 1370 |
+
with self.spmd_mesh:
|
| 1371 |
+
logging.info('evaluating on train dataset')
|
| 1372 |
+
metrics_ = train_utils.streaming_mean(
|
| 1373 |
+
rng_stream,
|
| 1374 |
+
map(to_global_array, get_train_data()),
|
| 1375 |
+
functools.partial(eval_batch_fn, params),
|
| 1376 |
+
)
|
| 1377 |
+
for tag, value in metrics_.items():
|
| 1378 |
+
self.record_scalar('train', tag=tag, value=value, step=step)
|
| 1379 |
+
|
| 1380 |
+
logging.info('evaluating on test dataset')
|
| 1381 |
+
metrics_ = train_utils.streaming_mean(
|
| 1382 |
+
rng_stream,
|
| 1383 |
+
map(to_global_array, get_eval_data()),
|
| 1384 |
+
functools.partial(eval_batch_fn, params),
|
| 1385 |
+
)
|
| 1386 |
+
for tag, value in metrics_.items():
|
| 1387 |
+
self.record_scalar('eval', tag=tag, value=value, step=step)
|
| 1388 |
+
|
| 1389 |
+
logging.info('evaluating EMA model on test dataset')
|
| 1390 |
+
metrics_ = train_utils.streaming_mean(
|
| 1391 |
+
rng_stream,
|
| 1392 |
+
map(to_global_array, get_eval_data()),
|
| 1393 |
+
functools.partial(eval_batch_fn, ema_params),
|
| 1394 |
+
)
|
| 1395 |
+
for tag, value in metrics_.items():
|
| 1396 |
+
self.record_scalar('eval_ema', tag=tag, value=value, step=step)
|
| 1397 |
+
|
| 1398 |
+
|
| 1399 |
+
if __name__ == '__main__':
|
| 1400 |
+
app.run(functools.partial(run_training.main, Experiment))
|
model/reference_code/linear_transforms.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""LinearTransforms for use in Metrics."""
|
| 15 |
+
import dataclasses
|
| 16 |
+
import functools
|
| 17 |
+
from typing import Callable, Mapping, Optional, Sequence
|
| 18 |
+
from dinosaur import coordinate_systems
|
| 19 |
+
from dinosaur import filtering
|
| 20 |
+
from dinosaur import horizontal_interpolation
|
| 21 |
+
from dinosaur import pytree_utils
|
| 22 |
+
from dinosaur import spherical_harmonic
|
| 23 |
+
from dinosaur import typing
|
| 24 |
+
import gin
|
| 25 |
+
import jax
|
| 26 |
+
import jax.numpy as jnp
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
import model.reference_code.metrics_util as metrics_util
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
Pytree = typing.Pytree
|
| 33 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 34 |
+
|
| 35 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 36 |
+
tree_map = jax.tree_util.tree_map
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclasses.dataclass
|
| 40 |
+
class LinearTransform:
|
| 41 |
+
"""A linear transformation, for TransformedL2Loss."""
|
| 42 |
+
|
| 43 |
+
trajectory_spec: metrics_util.TrajectorySpec
|
| 44 |
+
|
| 45 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 46 |
+
raise NotImplementedError
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
LinearTransformConstructor = Callable[
|
| 50 |
+
[metrics_util.TrajectorySpec], LinearTransform
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclasses.dataclass
|
| 55 |
+
class ComposedTransformForLoss(LinearTransform):
|
| 56 |
+
"""Composition of multiple linear transformations for computation of loss.
|
| 57 |
+
|
| 58 |
+
Attributes:
|
| 59 |
+
components: components[i](self.trajectory_spec) initializes the i + 1 member
|
| 60 |
+
of self.transforms.
|
| 61 |
+
transforms: errors are transformed as error --> transforms[0](error) -->
|
| 62 |
+
transforms[1](error) --> ⋯. The 0th transform is inserted by this class as
|
| 63 |
+
TruncateToTrajectoryLength.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
components: Sequence[LinearTransformConstructor]
|
| 67 |
+
transforms: Sequence[LinearTransform] = dataclasses.field(init=False)
|
| 68 |
+
|
| 69 |
+
def __post_init__(self):
|
| 70 |
+
# Insert TruncateToTrajectoryLength first in all cases. It's okay if it was
|
| 71 |
+
# already inserted... it is idempotent. This ensures that
|
| 72 |
+
# len(self.transforms) = len(self.components) + 1
|
| 73 |
+
# in all cases.
|
| 74 |
+
components = [TruncateToTrajectoryLength] + list(self.components)
|
| 75 |
+
self.transforms = [
|
| 76 |
+
constructor(self.trajectory_spec) for constructor in components
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 80 |
+
for transform in self.transforms:
|
| 81 |
+
errors = transform(errors, targets)
|
| 82 |
+
return errors
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@gin.register
|
| 86 |
+
@dataclasses.dataclass
|
| 87 |
+
class LegacyTimeRescaling(LinearTransform):
|
| 88 |
+
"""Time scaling from WeightedL2CumulativeLoss."""
|
| 89 |
+
|
| 90 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 91 |
+
del targets # unused.
|
| 92 |
+
n = self.trajectory_spec.trajectory_length
|
| 93 |
+
steps_per_save = self.trajectory_spec.steps_per_save
|
| 94 |
+
scale = 1 if n == 1 else 1 / np.sqrt((n - 1) * steps_per_save)
|
| 95 |
+
return tree_map(lambda x: x * scale, errors)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@gin.register
|
| 99 |
+
@dataclasses.dataclass
|
| 100 |
+
class TimeRescaling(LinearTransform):
|
| 101 |
+
"""Time scaling that assumes error grows like a random walk.
|
| 102 |
+
|
| 103 |
+
This rescales errors like
|
| 104 |
+
errors --> errors / σ(T),
|
| 105 |
+
σ(T) := sqrt( sum(variance) / variance(T) )
|
| 106 |
+
where variance(T) is the assumed variance. A random walk has variance ∝ T.
|
| 107 |
+
This function uses similar scaling.
|
| 108 |
+
|
| 109 |
+
See also:
|
| 110 |
+
* Climatology vs. ENS CRPS values indicate skill difficult after 240 hrs
|
| 111 |
+
http://screen/8sVodqThEk6o693
|
| 112 |
+
* Plotting this function for various parameter values
|
| 113 |
+
http://screen/AubXNomsgm7g92o and http://gpaste/6727081386835968
|
| 114 |
+
|
| 115 |
+
Attributes:
|
| 116 |
+
base_squared_error_in_hours: Number of hours before assumed variance starts
|
| 117 |
+
growing (almost) linearly.
|
| 118 |
+
asymptotic_squared_error_in_hours: Number of hours before assumed variance
|
| 119 |
+
slows its growth. Set to None (the default) if variance grows indefinitely
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
base_squared_error_in_hours: float
|
| 123 |
+
asymptotic_squared_error_in_hours: Optional[float] = None
|
| 124 |
+
|
| 125 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 126 |
+
del targets # unused.
|
| 127 |
+
time_sizes = np.unique([x.shape[0] for x in tree_leaves(errors)])
|
| 128 |
+
if time_sizes.size != 1:
|
| 129 |
+
raise ValueError(f'Expected unique time dimension size. {time_sizes=}')
|
| 130 |
+
time_size = time_sizes[0]
|
| 131 |
+
if self.trajectory_spec.trajectory_length != time_size:
|
| 132 |
+
logging.info(
|
| 133 |
+
f'errors has {time_size=} !='
|
| 134 |
+
f' {self.trajectory_spec.trajectory_length=}. This is probably due to'
|
| 135 |
+
' the Loss slicing via the time_step kwarg. Will use {time_size=}'
|
| 136 |
+
' to compute scaling.'
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
steps_per_save = self.trajectory_spec.steps_per_save
|
| 140 |
+
t = np.arange(time_size) * steps_per_save
|
| 141 |
+
if self.asymptotic_squared_error_in_hours is not None:
|
| 142 |
+
# Rescale "time" `t`, so it stops growing when
|
| 143 |
+
# t >> asymptotic_squared_error_in_hours.
|
| 144 |
+
t = t / (1 + t / self.asymptotic_squared_error_in_hours)
|
| 145 |
+
|
| 146 |
+
inv_variance = 1 / (1 + t / self.base_squared_error_in_hours)
|
| 147 |
+
scale = np.sqrt(inv_variance / inv_variance.sum())
|
| 148 |
+
scale = scale.reshape(-1, 1, 1, 1)
|
| 149 |
+
|
| 150 |
+
return tree_map(lambda x: x * scale, errors)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@gin.register
|
| 154 |
+
@dataclasses.dataclass
|
| 155 |
+
class CustomTimeRescaling(LinearTransform):
|
| 156 |
+
"""Custom time scaling that uses pre-specified values."""
|
| 157 |
+
|
| 158 |
+
scaling_weights: Sequence[float]
|
| 159 |
+
|
| 160 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 161 |
+
del targets # unused.
|
| 162 |
+
n = self.trajectory_spec.trajectory_length
|
| 163 |
+
scale = np.asarray(self.scaling_weights)[:n].reshape(-1, 1, 1, 1)
|
| 164 |
+
return tree_map(lambda x: x * scale, errors)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@gin.register
|
| 168 |
+
@dataclasses.dataclass
|
| 169 |
+
class DelayedTimeRescaling(LinearTransform):
|
| 170 |
+
"""Time scaling with smooth delay that transitions into hyperbolic decay."""
|
| 171 |
+
|
| 172 |
+
base_squared_error_in_hours: float
|
| 173 |
+
delay_power: float = 1.0
|
| 174 |
+
decay_power: float = 1.0
|
| 175 |
+
|
| 176 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 177 |
+
del targets # unused.
|
| 178 |
+
n = self.trajectory_spec.trajectory_length
|
| 179 |
+
steps_per_save = self.trajectory_spec.steps_per_save
|
| 180 |
+
t = np.arange(n) * steps_per_save
|
| 181 |
+
|
| 182 |
+
a = 1 / self.base_squared_error_in_hours
|
| 183 |
+
inv_variance = 1 / (
|
| 184 |
+
(1 + (a * t) ** self.delay_power) ** (1/self.decay_power))
|
| 185 |
+
scale = np.sqrt(inv_variance / inv_variance.sum())
|
| 186 |
+
scale = scale.reshape(-1, 1, 1, 1)
|
| 187 |
+
|
| 188 |
+
return tree_map(lambda x: x * scale, errors)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@gin.register
|
| 192 |
+
@dataclasses.dataclass
|
| 193 |
+
class TruncateToTrajectoryLength(LinearTransform):
|
| 194 |
+
"""Truncate errors to self.trajectory_spec.trajectory_length.
|
| 195 |
+
|
| 196 |
+
To ensure loss is computed over the correct trajectory length, this transform
|
| 197 |
+
should be used as the first step in any ComposedTransformForLoss.
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 201 |
+
del targets # unused.
|
| 202 |
+
n = self.trajectory_spec.trajectory_length
|
| 203 |
+
return metrics_util.extract_time_slice(errors, slice(0, n))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@gin.register
|
| 207 |
+
@dataclasses.dataclass
|
| 208 |
+
class TotalWavenumberMasking(LinearTransform):
|
| 209 |
+
"""Transform that masks out wavenumbers greater than `max_wavenumber`."""
|
| 210 |
+
|
| 211 |
+
max_wavenumber: int
|
| 212 |
+
is_encoded: bool = False
|
| 213 |
+
|
| 214 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 215 |
+
del targets # unused.
|
| 216 |
+
if self.is_encoded:
|
| 217 |
+
grid = self.trajectory_spec.coords.horizontal
|
| 218 |
+
else:
|
| 219 |
+
grid = self.trajectory_spec.data_coords.horizontal
|
| 220 |
+
|
| 221 |
+
modal_shape = grid.modal_shape
|
| 222 |
+
mask = np.arange(modal_shape[-1]) < self.max_wavenumber
|
| 223 |
+
mask = mask.astype(float)
|
| 224 |
+
return tree_map(lambda x: x * mask, errors)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@gin.register
|
| 228 |
+
@dataclasses.dataclass
|
| 229 |
+
class ConservativeRegridder(LinearTransform):
|
| 230 |
+
"""Linear transform that regrids."""
|
| 231 |
+
|
| 232 |
+
def __init__(
|
| 233 |
+
self,
|
| 234 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 235 |
+
target_grid: spherical_harmonic.Grid,
|
| 236 |
+
):
|
| 237 |
+
super().__init__(trajectory_spec=trajectory_spec)
|
| 238 |
+
self.regridder = horizontal_interpolation.ConservativeRegridder(
|
| 239 |
+
source_grid=trajectory_spec.coords.horizontal, target_grid=target_grid
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 243 |
+
del targets # Unused
|
| 244 |
+
return tree_map(self.regridder, errors)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
@gin.register
|
| 248 |
+
@dataclasses.dataclass
|
| 249 |
+
class PerVariableRescaling(LinearTransform):
|
| 250 |
+
"""Transform that reweights contribution per variable."""
|
| 251 |
+
weights: Pytree
|
| 252 |
+
scale: float = 1.0
|
| 253 |
+
|
| 254 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 255 |
+
del targets # unused.
|
| 256 |
+
weights = self.weights
|
| 257 |
+
if weights is None:
|
| 258 |
+
weights = tree_map(lambda x: 1.0, errors)
|
| 259 |
+
else:
|
| 260 |
+
weights = pytree_utils.replace_with_matching_or_default(
|
| 261 |
+
errors, weights, default=None,
|
| 262 |
+
check_used_all_replace_keys=True,
|
| 263 |
+
)
|
| 264 |
+
root_weights = tree_map(lambda w: np.sqrt(w * self.scale), weights)
|
| 265 |
+
return tree_map(jnp.multiply, errors, root_weights)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@gin.register
|
| 269 |
+
class ExponentialFilteringByLeadtime(LinearTransform):
|
| 270 |
+
"""Applied leadtime dependent exponential filters to errors."""
|
| 271 |
+
|
| 272 |
+
def __init__(
|
| 273 |
+
self,
|
| 274 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 275 |
+
filter_attenuations: typing.Pytree,
|
| 276 |
+
filter_orders: typing.Pytree,
|
| 277 |
+
is_encoded: bool = False,
|
| 278 |
+
):
|
| 279 |
+
super().__init__(trajectory_spec=trajectory_spec)
|
| 280 |
+
n = trajectory_spec.trajectory_length
|
| 281 |
+
if is_encoded:
|
| 282 |
+
grid = trajectory_spec.coords.horizontal
|
| 283 |
+
else:
|
| 284 |
+
grid = trajectory_spec.data_coords.horizontal
|
| 285 |
+
# expand dims for `level, lon, total wavenumbers` so that filter parameters
|
| 286 |
+
# are applied to different time values.
|
| 287 |
+
to_array_fn = lambda x: np.expand_dims(np.array(x)[:n], axis=(1, 2, 3))
|
| 288 |
+
is_leaf = lambda x: isinstance(x, Sequence)
|
| 289 |
+
attenuations = tree_map(to_array_fn, filter_attenuations, is_leaf=is_leaf)
|
| 290 |
+
orders = tree_map(to_array_fn, filter_orders, is_leaf=is_leaf)
|
| 291 |
+
self.filter_fns = tree_map(
|
| 292 |
+
lambda a, p: filtering.exponential_filter(grid, a, p),
|
| 293 |
+
attenuations,
|
| 294 |
+
orders,
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 298 |
+
del targets # unused.
|
| 299 |
+
filter_fns = pytree_utils.replace_with_matching_or_default(
|
| 300 |
+
errors, self.filter_fns, default=None, check_used_all_replace_keys=True)
|
| 301 |
+
return tree_map(lambda fn, err: fn(err), filter_fns, errors)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
@gin.register
|
| 305 |
+
class LevelRescaling(LinearTransform):
|
| 306 |
+
"""Linear transform that scales values with vertical levels."""
|
| 307 |
+
|
| 308 |
+
def __init__(
|
| 309 |
+
self,
|
| 310 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 311 |
+
scale: Sequence[float],
|
| 312 |
+
keys_to_scale: Sequence[str] = tuple(),
|
| 313 |
+
):
|
| 314 |
+
super().__init__(trajectory_spec)
|
| 315 |
+
self.scale_fn = functools.partial(
|
| 316 |
+
coordinate_systems.scale_levels_for_matching_keys,
|
| 317 |
+
scales=np.asarray(scale),
|
| 318 |
+
keys_to_scale=keys_to_scale,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 322 |
+
del targets # unused.
|
| 323 |
+
return self.scale_fn(errors)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
@gin.register
|
| 327 |
+
class LevelRemoval(LinearTransform):
|
| 328 |
+
"""Linear transform that removes vertical levels."""
|
| 329 |
+
|
| 330 |
+
def __init__(
|
| 331 |
+
self,
|
| 332 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 333 |
+
keep_levels: Sequence[float],
|
| 334 |
+
):
|
| 335 |
+
super().__init__(trajectory_spec)
|
| 336 |
+
n_levels = trajectory_spec.data_coords.vertical.layers
|
| 337 |
+
indices = jnp.array([i for i in range(n_levels) if keep_levels[i]])
|
| 338 |
+
self.take_arr = lambda x: jnp.take(x, indices, axis=metrics_util.LEVEL_AXIS)
|
| 339 |
+
|
| 340 |
+
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
|
| 341 |
+
del targets # unused.
|
| 342 |
+
return tree_map(self.take_arr, errors)
|
model/reference_code/metrics.py
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Metrics and loss functions for NeuralGCM."""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import dataclasses
|
| 19 |
+
import functools
|
| 20 |
+
from typing import Callable, Optional, Sequence
|
| 21 |
+
|
| 22 |
+
from dinosaur import coordinate_systems
|
| 23 |
+
from dinosaur import horizontal_interpolation
|
| 24 |
+
from dinosaur import spherical_harmonic
|
| 25 |
+
from dinosaur import typing
|
| 26 |
+
from dinosaur import vertical_interpolation
|
| 27 |
+
import gin
|
| 28 |
+
import jax
|
| 29 |
+
import jax.numpy as jnp
|
| 30 |
+
import model.reference_code.linear_transforms as linear_transforms
|
| 31 |
+
import model.reference_code.metrics_base as metrics_base
|
| 32 |
+
import model.reference_code.metrics_util as metrics_util
|
| 33 |
+
from model.legacy import model_utils
|
| 34 |
+
import numpy as np
|
| 35 |
+
import model.reference_code.train_utils as train_utils
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
Pytree = typing.Pytree
|
| 39 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 40 |
+
|
| 41 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 42 |
+
tree_map = jax.tree_util.tree_map
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _compute_spectral_norm(
|
| 46 |
+
x: typing.Array, coords: coordinate_systems.CoordinateSystem
|
| 47 |
+
) -> typing.Array:
|
| 48 |
+
"""Computes spectral norm of nodal inputs `x`."""
|
| 49 |
+
x = coordinate_systems.maybe_to_modal(x, coords)
|
| 50 |
+
# axis = -2 corresponds to the longitudinal wavenumber.
|
| 51 |
+
return model_utils.safe_sqrt(
|
| 52 |
+
jnp.sum((x * x.conj()).real, axis=-2, keepdims=True)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@gin.register
|
| 57 |
+
def _spectral_amplitude(
|
| 58 |
+
x: typing.Array, coords: coordinate_systems.CoordinateSystem
|
| 59 |
+
) -> typing.Array:
|
| 60 |
+
"""Computes spectral amplitude ."""
|
| 61 |
+
x = coordinate_systems.maybe_to_modal(x, coords)
|
| 62 |
+
return jnp.abs(x)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@gin.register
|
| 66 |
+
@dataclasses.dataclass
|
| 67 |
+
class TransformedL2Loss(metrics_base.Loss):
|
| 68 |
+
"""L2 loss on linearly transformed errors."""
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 73 |
+
components: Sequence[linear_transforms.LinearTransformConstructor],
|
| 74 |
+
is_nodal: bool = True,
|
| 75 |
+
is_encoded: bool = False,
|
| 76 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 77 |
+
time_step: Optional[int | slice] = None,
|
| 78 |
+
):
|
| 79 |
+
super().__init__(
|
| 80 |
+
trajectory_spec,
|
| 81 |
+
is_nodal=is_nodal,
|
| 82 |
+
is_encoded=is_encoded,
|
| 83 |
+
time_step=time_step,
|
| 84 |
+
)
|
| 85 |
+
self.components = components
|
| 86 |
+
self.getter = getter
|
| 87 |
+
self.transform = linear_transforms.ComposedTransformForLoss(
|
| 88 |
+
trajectory_spec, components
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
def evaluate_per_variable(
|
| 92 |
+
self,
|
| 93 |
+
prediction: TrajectoryRepresentations,
|
| 94 |
+
target: TrajectoryRepresentations,
|
| 95 |
+
) -> Pytree:
|
| 96 |
+
prediction = self.get_representation(prediction)
|
| 97 |
+
target = self.get_representation(target)
|
| 98 |
+
trajectory = self.getter(prediction)
|
| 99 |
+
target = self.getter(target)
|
| 100 |
+
errors = tree_map(jnp.subtract, trajectory, target)
|
| 101 |
+
transformed_errors = self.transform(errors, target)
|
| 102 |
+
squared_transformed_errors = tree_map(jnp.square, transformed_errors)
|
| 103 |
+
return self.mean_per_variable(squared_transformed_errors)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@gin.register
|
| 107 |
+
@dataclasses.dataclass
|
| 108 |
+
class TransformedL2SpectrumLoss(metrics_base.Loss):
|
| 109 |
+
"""L2 loss on linearly transformed errors of spectal norms.
|
| 110 |
+
|
| 111 |
+
Here we define spectrum norm at a given total wavenumber as the length of the
|
| 112 |
+
vector formed by longitude wavenumbers. i.e. for a field `x` with indices
|
| 113 |
+
`{z, m, l}` corresponding to level, longitude wavenumber, total wavenumber
|
| 114 |
+
we have:
|
| 115 |
+
|
| 116 |
+
spectrum_norm(x)_{z, l} = ||x_{z, :, l}||₂
|
| 117 |
+
|
| 118 |
+
The loss is then computed as MSE(spectrum_norm(x), spectrum_norm(y)) where
|
| 119 |
+
`x` and `y` are predicted and target signals in modal representation.
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
def __init__(
|
| 123 |
+
self,
|
| 124 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 125 |
+
components: Sequence[linear_transforms.LinearTransformConstructor],
|
| 126 |
+
is_nodal: bool = True,
|
| 127 |
+
is_encoded: bool = False,
|
| 128 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 129 |
+
time_step: Optional[int | slice] = None,
|
| 130 |
+
):
|
| 131 |
+
super().__init__(
|
| 132 |
+
trajectory_spec,
|
| 133 |
+
is_nodal=is_nodal,
|
| 134 |
+
is_encoded=is_encoded,
|
| 135 |
+
time_step=time_step,
|
| 136 |
+
)
|
| 137 |
+
if self.is_encoded:
|
| 138 |
+
coords = trajectory_spec.coords
|
| 139 |
+
else:
|
| 140 |
+
coords = trajectory_spec.data_coords
|
| 141 |
+
spectrum_fn = lambda x: _compute_spectral_norm(x, coords)
|
| 142 |
+
self.components = components
|
| 143 |
+
self.getter = getter
|
| 144 |
+
self.spectrum_fn = lambda tree: tree_map(spectrum_fn, tree)
|
| 145 |
+
self.transform = linear_transforms.ComposedTransformForLoss(
|
| 146 |
+
trajectory_spec, components
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
def mean_per_variable(self, trajectory: Pytree) -> Pytree:
|
| 150 |
+
return tree_map(jnp.mean, trajectory)
|
| 151 |
+
|
| 152 |
+
def evaluate_per_variable(
|
| 153 |
+
self,
|
| 154 |
+
prediction: TrajectoryRepresentations,
|
| 155 |
+
target: TrajectoryRepresentations,
|
| 156 |
+
) -> Pytree:
|
| 157 |
+
prediction = self.get_representation(prediction)
|
| 158 |
+
target = self.get_representation(target)
|
| 159 |
+
trajectory_spectrum = self.spectrum_fn(self.getter(prediction))
|
| 160 |
+
target_spectrum = self.spectrum_fn(self.getter(target))
|
| 161 |
+
errors = tree_map(jnp.subtract, trajectory_spectrum, target_spectrum)
|
| 162 |
+
transformed_errors = self.transform(errors, target)
|
| 163 |
+
squared_transformed_errors = tree_map(jnp.square, transformed_errors)
|
| 164 |
+
return self.mean_per_variable(squared_transformed_errors)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@gin.register
|
| 168 |
+
@dataclasses.dataclass
|
| 169 |
+
class SumLoss(metrics_base.Loss):
|
| 170 |
+
"""Loss that consists of a sum of separate losses."""
|
| 171 |
+
|
| 172 |
+
def __init__(
|
| 173 |
+
self,
|
| 174 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 175 |
+
terms: Sequence[Callable[..., metrics_base.Loss]],
|
| 176 |
+
labels: Optional[Sequence[str]] = None,
|
| 177 |
+
time_step: Optional[int | slice] = None,
|
| 178 |
+
):
|
| 179 |
+
super().__init__(trajectory_spec)
|
| 180 |
+
self.losses = [term(trajectory_spec, time_step=time_step) for term in terms]
|
| 181 |
+
if labels is not None:
|
| 182 |
+
if len(labels) != len(self.losses):
|
| 183 |
+
raise ValueError(f'Not all losses are labeled: {labels}, {len(terms)=}')
|
| 184 |
+
self.labels = labels
|
| 185 |
+
else:
|
| 186 |
+
self.labels = [''] * len(self.losses)
|
| 187 |
+
|
| 188 |
+
def evaluate_per_variable(
|
| 189 |
+
self,
|
| 190 |
+
prediction: TrajectoryRepresentations,
|
| 191 |
+
target: TrajectoryRepresentations,
|
| 192 |
+
) -> Pytree:
|
| 193 |
+
all_per_variable_losses = [
|
| 194 |
+
loss.evaluate_per_variable(prediction, target) for loss in self.losses
|
| 195 |
+
]
|
| 196 |
+
output = {}
|
| 197 |
+
for per_variable_loss, prefix in zip(all_per_variable_losses, self.labels):
|
| 198 |
+
for k, v in per_variable_loss.items():
|
| 199 |
+
if isinstance(v, dict):
|
| 200 |
+
current_values = output.get(prefix + k, {})
|
| 201 |
+
for ik, iv in v.items():
|
| 202 |
+
current_values[ik] = current_values.get(ik, 0) + iv
|
| 203 |
+
output[prefix + k] = current_values
|
| 204 |
+
else:
|
| 205 |
+
output[prefix + k] = output.get(prefix + k, 0) + v
|
| 206 |
+
return output
|
| 207 |
+
|
| 208 |
+
def evaluate(
|
| 209 |
+
self,
|
| 210 |
+
prediction: TrajectoryRepresentations,
|
| 211 |
+
target: TrajectoryRepresentations,
|
| 212 |
+
) -> Pytree:
|
| 213 |
+
return sum(loss.evaluate(prediction, target) for loss in self.losses)
|
| 214 |
+
|
| 215 |
+
def debug_loss_terms_instance(self) -> metrics_base.EvaluateFunctionWrapper:
|
| 216 |
+
"""Returns class that evaluates relative loss per variable."""
|
| 217 |
+
|
| 218 |
+
def evaluate_fn(
|
| 219 |
+
prediction: TrajectoryRepresentations,
|
| 220 |
+
target: TrajectoryRepresentations,
|
| 221 |
+
) -> Pytree:
|
| 222 |
+
return train_utils.flatten_dict({
|
| 223 |
+
label: loss.debug_loss_terms_instance().evaluate(prediction, target)
|
| 224 |
+
for label, loss in zip(self.labels, self.losses)
|
| 225 |
+
})
|
| 226 |
+
|
| 227 |
+
return metrics_base.EvaluateFunctionWrapper(evaluate_fn)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@gin.register
|
| 231 |
+
def WeightedL2CumulativeLoss( # pylint: disable=invalid-name
|
| 232 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 233 |
+
weights: Pytree = None,
|
| 234 |
+
scale: float = 1.0,
|
| 235 |
+
) -> TransformedL2Loss:
|
| 236 |
+
"""Legacy wrapper for TransformedL2Loss with weighted cumulative error."""
|
| 237 |
+
components = [
|
| 238 |
+
linear_transforms.LegacyTimeRescaling,
|
| 239 |
+
functools.partial(
|
| 240 |
+
linear_transforms.PerVariableRescaling, weights=weights, scale=scale
|
| 241 |
+
),
|
| 242 |
+
]
|
| 243 |
+
return TransformedL2Loss(trajectory_spec, components)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@gin.register
|
| 247 |
+
class RMSE(metrics_base.ScalarMetric):
|
| 248 |
+
"""Root mean squared error."""
|
| 249 |
+
|
| 250 |
+
def __init__(
|
| 251 |
+
self,
|
| 252 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 253 |
+
time_step: int,
|
| 254 |
+
level: Optional[int] = None,
|
| 255 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 256 |
+
is_nodal: bool = True,
|
| 257 |
+
is_encoded: bool = False,
|
| 258 |
+
is_ensemble_data: bool = False,
|
| 259 |
+
):
|
| 260 |
+
super().__init__(trajectory_spec, is_nodal=is_nodal, is_encoded=is_encoded)
|
| 261 |
+
self.time_step = time_step
|
| 262 |
+
self.level = level
|
| 263 |
+
self.getter = getter
|
| 264 |
+
self.is_ensemble_data = is_ensemble_data
|
| 265 |
+
|
| 266 |
+
def _prepare(self, trajectory: TrajectoryRepresentations) -> Pytree:
|
| 267 |
+
"""Prepares target or predictions."""
|
| 268 |
+
trajectory = metrics_util.extract_variable(
|
| 269 |
+
trajectory,
|
| 270 |
+
self.trajectory_spec,
|
| 271 |
+
self.time_step,
|
| 272 |
+
self.level,
|
| 273 |
+
self.getter,
|
| 274 |
+
self.is_nodal,
|
| 275 |
+
self.is_encoded,
|
| 276 |
+
)
|
| 277 |
+
if self.is_ensemble_data:
|
| 278 |
+
# Evaluate RMSE vs. the ensemble mean.
|
| 279 |
+
trajectory = jax.lax.pmean(trajectory, axis_name='ensemble')
|
| 280 |
+
return trajectory
|
| 281 |
+
|
| 282 |
+
def evaluate(
|
| 283 |
+
self,
|
| 284 |
+
prediction: TrajectoryRepresentations,
|
| 285 |
+
target: TrajectoryRepresentations,
|
| 286 |
+
) -> jnp.ndarray:
|
| 287 |
+
"""Evaluates RMSE between prediction and target."""
|
| 288 |
+
prediction = self._prepare(prediction)
|
| 289 |
+
target = self._prepare(target)
|
| 290 |
+
squared_error = tree_map(lambda x, y: (x - y) ** 2, prediction, target)
|
| 291 |
+
mse_per_variable = self.mean_per_variable(squared_error)
|
| 292 |
+
return jnp.sqrt(sum(tree_leaves(mse_per_variable)))
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@gin.register
|
| 296 |
+
class SpatialBiasRMSE(metrics_base.ScalarMetric):
|
| 297 |
+
"""Root mean squared error of spatial bias.
|
| 298 |
+
|
| 299 |
+
This is given by the formula:
|
| 300 |
+
|
| 301 |
+
RMSE(batch_average(prediction - target))
|
| 302 |
+
|
| 303 |
+
where `batch_average()` denotes an average over distinct weather forecasts
|
| 304 |
+
(initialization times or valid times) and ensemble members (if relevant).
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
def __init__(
|
| 308 |
+
self,
|
| 309 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 310 |
+
time_step: int,
|
| 311 |
+
level: Optional[int] = None,
|
| 312 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 313 |
+
is_nodal: bool = True,
|
| 314 |
+
is_encoded: bool = False,
|
| 315 |
+
is_batch_data: bool = True,
|
| 316 |
+
is_ensemble_data: bool = False,
|
| 317 |
+
):
|
| 318 |
+
super().__init__(trajectory_spec, is_nodal=is_nodal, is_encoded=is_encoded)
|
| 319 |
+
self.time_step = time_step
|
| 320 |
+
self.level = level
|
| 321 |
+
self.getter = getter
|
| 322 |
+
self.is_ensemble_data = is_ensemble_data
|
| 323 |
+
self.is_batch_data = is_batch_data
|
| 324 |
+
|
| 325 |
+
def _prepare(self, trajectory: TrajectoryRepresentations) -> Pytree:
|
| 326 |
+
"""Prepares target or predictions."""
|
| 327 |
+
trajectory = metrics_util.extract_variable(
|
| 328 |
+
trajectory,
|
| 329 |
+
self.trajectory_spec,
|
| 330 |
+
time_step=self.time_step,
|
| 331 |
+
level=self.level,
|
| 332 |
+
getter=self.getter,
|
| 333 |
+
is_nodal=self.is_nodal,
|
| 334 |
+
is_encoded=self.is_encoded,
|
| 335 |
+
)
|
| 336 |
+
if self.is_batch_data:
|
| 337 |
+
trajectory = jax.lax.pmean(trajectory, axis_name='batch')
|
| 338 |
+
if self.is_ensemble_data:
|
| 339 |
+
trajectory = jax.lax.pmean(trajectory, axis_name='ensemble')
|
| 340 |
+
return trajectory
|
| 341 |
+
|
| 342 |
+
def evaluate(
|
| 343 |
+
self,
|
| 344 |
+
prediction: TrajectoryRepresentations,
|
| 345 |
+
target: TrajectoryRepresentations,
|
| 346 |
+
) -> jnp.ndarray:
|
| 347 |
+
"""Evaluates RMSE between prediction and target."""
|
| 348 |
+
prediction = self._prepare(prediction)
|
| 349 |
+
target = self._prepare(target)
|
| 350 |
+
squared_error = tree_map(lambda x, y: (x - y) ** 2, prediction, target)
|
| 351 |
+
mse_per_variable = self.mean_per_variable(squared_error)
|
| 352 |
+
return jnp.sqrt(sum(tree_leaves(mse_per_variable)))
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@gin.register
|
| 356 |
+
class BatchMeanSquaredBias(metrics_base.Loss):
|
| 357 |
+
"""Mean squared error for a chosen metric.
|
| 358 |
+
|
| 359 |
+
This is given by the formula:
|
| 360 |
+
|
| 361 |
+
MSE(rollout_average(batch_average(prediction - target)))
|
| 362 |
+
|
| 363 |
+
where `batch_average()` denotes an average over distinct weather forecasts
|
| 364 |
+
(initialization times or valid times) or ensemble members (whichever is
|
| 365 |
+
vmapped first) and 'rollout_average()' denotes an average over all predicted
|
| 366 |
+
times. The MSE is taken over all nodal/modal points.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
def __init__(
|
| 370 |
+
self,
|
| 371 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 372 |
+
components: Sequence[linear_transforms.LinearTransformConstructor] = (),
|
| 373 |
+
observation_fn=_spectral_amplitude,
|
| 374 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 375 |
+
is_nodal: bool = False,
|
| 376 |
+
is_encoded: bool = False,
|
| 377 |
+
time_step: Optional[int | slice] = None,
|
| 378 |
+
):
|
| 379 |
+
super().__init__(
|
| 380 |
+
trajectory_spec,
|
| 381 |
+
is_nodal=is_nodal,
|
| 382 |
+
is_encoded=is_encoded,
|
| 383 |
+
time_step=time_step,
|
| 384 |
+
)
|
| 385 |
+
if self.is_encoded:
|
| 386 |
+
coords = trajectory_spec.coords
|
| 387 |
+
else:
|
| 388 |
+
coords = trajectory_spec.data_coords
|
| 389 |
+
metric_fn = lambda x: observation_fn(x, coords)
|
| 390 |
+
self.components = components
|
| 391 |
+
self.getter = getter
|
| 392 |
+
self.metric_fn = lambda tree: tree_map(metric_fn, tree)
|
| 393 |
+
self.transform = linear_transforms.ComposedTransformForLoss(
|
| 394 |
+
trajectory_spec, components
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
def evaluate_per_variable(
|
| 398 |
+
self,
|
| 399 |
+
prediction: TrajectoryRepresentations,
|
| 400 |
+
target: TrajectoryRepresentations,
|
| 401 |
+
) -> Pytree:
|
| 402 |
+
"""Evaluates the squere bias of a chosen metric between prediction and target.
|
| 403 |
+
|
| 404 |
+
Note: this method is only valid when vmapped.
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
prediction: a TrajectoryRepresentations of prediction
|
| 408 |
+
target: a TrajectoryRepresentations of ground truth
|
| 409 |
+
|
| 410 |
+
Returns:
|
| 411 |
+
Pytree of MSE
|
| 412 |
+
"""
|
| 413 |
+
prediction = self.get_representation(prediction)
|
| 414 |
+
target = self.get_representation(target)
|
| 415 |
+
# because this function applies average over time axis, we apply
|
| 416 |
+
# `TruncateToTrajectoryLength` prior to computing
|
| 417 |
+
truncate_transform = self.transform.transforms[0]
|
| 418 |
+
assert isinstance(
|
| 419 |
+
truncate_transform, linear_transforms.TruncateToTrajectoryLength
|
| 420 |
+
)
|
| 421 |
+
getter_fn = lambda x: self.getter(truncate_transform(x, None))
|
| 422 |
+
trajectory_calc = self.metric_fn(getter_fn(prediction))
|
| 423 |
+
target_calc = self.metric_fn(getter_fn(target))
|
| 424 |
+
# Batch mean over "ensemble" and "batch" dimensions
|
| 425 |
+
trajectory_calc = tree_map(metrics_util.pmean_all_axes, trajectory_calc)
|
| 426 |
+
target_calc = tree_map(metrics_util.pmean_all_axes, target_calc)
|
| 427 |
+
# Time mean:
|
| 428 |
+
trajectory_calc = tree_map(
|
| 429 |
+
lambda x,: jnp.mean(x, axis=0, keepdims=True), trajectory_calc
|
| 430 |
+
)
|
| 431 |
+
target_calc = tree_map(
|
| 432 |
+
lambda x,: jnp.mean(x, axis=0, keepdims=True), target_calc
|
| 433 |
+
)
|
| 434 |
+
errors = tree_map(jnp.subtract, trajectory_calc, target_calc)
|
| 435 |
+
transformed_errors = self.transform(errors, target)
|
| 436 |
+
squared_transformed_errors = tree_map(jnp.square, transformed_errors)
|
| 437 |
+
mse_per_variable = tree_map(jnp.mean, squared_transformed_errors)
|
| 438 |
+
return mse_per_variable
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
@gin.register
|
| 442 |
+
class MAE(metrics_base.ScalarMetric):
|
| 443 |
+
"""Mean absolute error."""
|
| 444 |
+
|
| 445 |
+
def __init__(
|
| 446 |
+
self,
|
| 447 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 448 |
+
time_step: int,
|
| 449 |
+
level: Optional[int] = None,
|
| 450 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 451 |
+
is_nodal: bool = True,
|
| 452 |
+
is_encoded: bool = False,
|
| 453 |
+
):
|
| 454 |
+
super().__init__(trajectory_spec, is_nodal=is_nodal, is_encoded=is_encoded)
|
| 455 |
+
self.time_step = time_step
|
| 456 |
+
self.level = level
|
| 457 |
+
self.getter = getter
|
| 458 |
+
|
| 459 |
+
def _prepare(self, trajectory: TrajectoryRepresentations) -> Pytree:
|
| 460 |
+
return metrics_util.extract_variable(
|
| 461 |
+
trajectory,
|
| 462 |
+
self.trajectory_spec,
|
| 463 |
+
self.time_step,
|
| 464 |
+
self.level,
|
| 465 |
+
self.getter,
|
| 466 |
+
self.is_nodal,
|
| 467 |
+
self.is_encoded,
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
def evaluate(
|
| 471 |
+
self,
|
| 472 |
+
prediction: TrajectoryRepresentations,
|
| 473 |
+
target: TrajectoryRepresentations,
|
| 474 |
+
) -> jnp.ndarray:
|
| 475 |
+
prediction = self._prepare(prediction)
|
| 476 |
+
target = self._prepare(target)
|
| 477 |
+
abs_error = tree_map(lambda x, y: abs(x - y), prediction, target)
|
| 478 |
+
mse_per_variable = self.mean_per_variable(abs_error)
|
| 479 |
+
flat_mse = tree_leaves(mse_per_variable)
|
| 480 |
+
return sum(flat_mse) / len(flat_mse)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
@jax.jit
|
| 484 |
+
def weighted_quantile(
|
| 485 |
+
data: jax.Array, quantile: jax.Array, weights: jax.Array
|
| 486 |
+
) -> jax.Array:
|
| 487 |
+
"""Calculate a weighted quantile."""
|
| 488 |
+
if data.shape != weights.shape:
|
| 489 |
+
raise ValueError(f'incompatible shapes: {data.shape=} != {weights.shape=}')
|
| 490 |
+
data = data.ravel()
|
| 491 |
+
weights = weights.ravel() / weights.sum()
|
| 492 |
+
indices = jnp.argsort(data)
|
| 493 |
+
cum_weights = weights[indices].cumsum()
|
| 494 |
+
return jnp.interp(quantile, cum_weights, data[indices])
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
@dataclasses.dataclass
|
| 498 |
+
class AbsErrorQuantile(metrics_base.ScalarMetric):
|
| 499 |
+
"""Quantile of absolute error."""
|
| 500 |
+
|
| 501 |
+
def __init__(
|
| 502 |
+
self,
|
| 503 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 504 |
+
quantile: float,
|
| 505 |
+
time_step: int,
|
| 506 |
+
level: Optional[int] = None,
|
| 507 |
+
getter: Callable[[Pytree], Pytree] = metrics_util.filter_sim_time,
|
| 508 |
+
is_nodal: bool = True,
|
| 509 |
+
is_encoded: bool = False,
|
| 510 |
+
is_ensemble_data: bool = False,
|
| 511 |
+
):
|
| 512 |
+
super().__init__(trajectory_spec, is_nodal=is_nodal, is_encoded=is_encoded)
|
| 513 |
+
self.quantile = quantile
|
| 514 |
+
self.time_step = time_step
|
| 515 |
+
self.level = level
|
| 516 |
+
self.getter = getter
|
| 517 |
+
self.is_ensemble_data = is_ensemble_data
|
| 518 |
+
|
| 519 |
+
def _prepare(self, trajectory: TrajectoryRepresentations) -> Pytree:
|
| 520 |
+
return metrics_util.extract_variable(
|
| 521 |
+
trajectory,
|
| 522 |
+
self.trajectory_spec,
|
| 523 |
+
self.time_step,
|
| 524 |
+
self.level,
|
| 525 |
+
self.getter,
|
| 526 |
+
self.is_nodal,
|
| 527 |
+
self.is_encoded,
|
| 528 |
+
)
|
| 529 |
+
|
| 530 |
+
def _get_weights(self) -> np.ndarray:
|
| 531 |
+
if self.is_encoded:
|
| 532 |
+
coords = self.trajectory_spec.coords
|
| 533 |
+
else:
|
| 534 |
+
coords = self.trajectory_spec.data_coords
|
| 535 |
+
if self.is_nodal:
|
| 536 |
+
weights = coords.horizontal.quadrature_weights
|
| 537 |
+
else:
|
| 538 |
+
weights = coords.horizontal.mask
|
| 539 |
+
return weights
|
| 540 |
+
|
| 541 |
+
def evaluate(
|
| 542 |
+
self,
|
| 543 |
+
prediction: TrajectoryRepresentations,
|
| 544 |
+
target: TrajectoryRepresentations,
|
| 545 |
+
) -> jnp.ndarray:
|
| 546 |
+
prediction = self._prepare(prediction)
|
| 547 |
+
target = self._prepare(target)
|
| 548 |
+
abs_error = tree_map(lambda x, y: abs(x - y), prediction, target)
|
| 549 |
+
weights = jnp.broadcast_to(self._get_weights(), target.shape)
|
| 550 |
+
result = tree_map(
|
| 551 |
+
lambda e: weighted_quantile(e, self.quantile, weights), abs_error
|
| 552 |
+
)
|
| 553 |
+
if self.is_ensemble_data:
|
| 554 |
+
# metrics must be consistent across the ensmble dimension.
|
| 555 |
+
result = jax.lax.pmean(result, axis_name='ensemble')
|
| 556 |
+
return result
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def weatherbench2_rmse_metrics(
|
| 560 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 561 |
+
time_steps: Sequence[int],
|
| 562 |
+
is_ensemble_data: bool = False,
|
| 563 |
+
extra_metric_grids: Optional[dict[str, spherical_harmonic.Grid]] = None,
|
| 564 |
+
) -> dict[str, metrics_base.Metric]:
|
| 565 |
+
"""RMSE based metrics for WeatherBench2."""
|
| 566 |
+
metric_grids = {} if extra_metric_grids is None else extra_metric_grids.copy()
|
| 567 |
+
trajectory_grid = trajectory_spec.coords.horizontal
|
| 568 |
+
if trajectory_grid not in metric_grids.values():
|
| 569 |
+
metric_grids['Traj'] = trajectory_grid
|
| 570 |
+
|
| 571 |
+
def get_and_regrid(tree, regrid_fn, getter):
|
| 572 |
+
return tree_map(regrid_fn, getter(tree))
|
| 573 |
+
|
| 574 |
+
metrics = {}
|
| 575 |
+
for name, grid in metric_grids.items():
|
| 576 |
+
if grid == trajectory_grid:
|
| 577 |
+
regrid = lambda tree: tree
|
| 578 |
+
rmse_traj_spec = trajectory_spec
|
| 579 |
+
else:
|
| 580 |
+
regrid = horizontal_interpolation.ConservativeRegridder(
|
| 581 |
+
source_grid=trajectory_spec.coords.horizontal, target_grid=grid
|
| 582 |
+
)
|
| 583 |
+
rmse_traj_spec = dataclasses.replace(
|
| 584 |
+
trajectory_spec,
|
| 585 |
+
# Only data_coords needs to be replaced since RMSE.is_encoded=False.
|
| 586 |
+
data_coords=dataclasses.replace(
|
| 587 |
+
trajectory_spec.data_coords,
|
| 588 |
+
horizontal=grid,
|
| 589 |
+
),
|
| 590 |
+
)
|
| 591 |
+
for time_step in time_steps:
|
| 592 |
+
for var, level, getter in [
|
| 593 |
+
('T', 850, lambda x: x['t']),
|
| 594 |
+
('Z', 500, lambda x: x['z']),
|
| 595 |
+
('UV', 700, lambda x: (x['u'], x['v'])),
|
| 596 |
+
('Q', 700, lambda x: 1000 * x['tracers']['specific_humidity']),
|
| 597 |
+
]:
|
| 598 |
+
t = time_step * trajectory_spec.steps_per_save
|
| 599 |
+
key = f'RMSE[{name}]_{var}{level}_{t:03d}_hours'
|
| 600 |
+
metrics[key] = RMSE(
|
| 601 |
+
rmse_traj_spec,
|
| 602 |
+
is_encoded=False, # To make this (default) clear.
|
| 603 |
+
time_step=time_step,
|
| 604 |
+
level=level,
|
| 605 |
+
getter=functools.partial(
|
| 606 |
+
get_and_regrid, regrid_fn=regrid, getter=getter
|
| 607 |
+
),
|
| 608 |
+
is_ensemble_data=is_ensemble_data,
|
| 609 |
+
)
|
| 610 |
+
return metrics
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def default_metrics(
|
| 614 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 615 |
+
eval_time_steps: Sequence[int],
|
| 616 |
+
train_loss: metrics_base.Loss,
|
| 617 |
+
is_batch_data: bool = True,
|
| 618 |
+
is_ensemble_data: bool = False,
|
| 619 |
+
) -> dict[str, metrics_base.Evaluator]:
|
| 620 |
+
"""Default evaluation metrics for Whirl models."""
|
| 621 |
+
metrics_dict = {
|
| 622 |
+
'training_loss': train_loss,
|
| 623 |
+
'debug': train_loss.debug_loss_terms_instance(),
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
if isinstance(
|
| 627 |
+
trajectory_spec.data_coords.vertical,
|
| 628 |
+
vertical_interpolation.PressureCoordinates,
|
| 629 |
+
):
|
| 630 |
+
tl31_grid = dataclasses.replace(
|
| 631 |
+
spherical_harmonic.Grid.TL31(),
|
| 632 |
+
spherical_harmonics_impl=trajectory_spec.data_coords.horizontal.spherical_harmonics_impl,
|
| 633 |
+
)
|
| 634 |
+
metrics_dict.update(
|
| 635 |
+
weatherbench2_rmse_metrics(
|
| 636 |
+
trajectory_spec,
|
| 637 |
+
eval_time_steps,
|
| 638 |
+
is_ensemble_data=is_ensemble_data,
|
| 639 |
+
extra_metric_grids={'TL31': tl31_grid},
|
| 640 |
+
)
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
for time_step in eval_time_steps:
|
| 644 |
+
t = time_step * trajectory_spec.steps_per_save
|
| 645 |
+
|
| 646 |
+
for var, getter in [
|
| 647 |
+
('T', lambda x: x['t']),
|
| 648 |
+
('Z', lambda x: x['z']),
|
| 649 |
+
('UV', lambda x: (x['u'], x['v'])),
|
| 650 |
+
('Q', lambda x: 1000 * x['tracers']['specific_humidity']),
|
| 651 |
+
]:
|
| 652 |
+
key = f'rmse_{var}_all_levels_{t:03d}_hours'
|
| 653 |
+
metrics_dict[key] = RMSE(
|
| 654 |
+
trajectory_spec,
|
| 655 |
+
time_step=time_step,
|
| 656 |
+
level=None,
|
| 657 |
+
getter=getter,
|
| 658 |
+
is_ensemble_data=is_ensemble_data,
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
for var, level, getter in [
|
| 662 |
+
('T', 850, lambda x: x['t']),
|
| 663 |
+
('Z', 500, lambda x: x['z']),
|
| 664 |
+
('U', 700, lambda x: x['u']),
|
| 665 |
+
('V', 700, lambda x: x['v']),
|
| 666 |
+
('Q', 700, lambda x: 1000 * x['tracers']['specific_humidity']),
|
| 667 |
+
]:
|
| 668 |
+
key = f'spatial_bias_rmse_{var}{level}_{t:03d}_hours'
|
| 669 |
+
metrics_dict[key] = SpatialBiasRMSE(
|
| 670 |
+
trajectory_spec,
|
| 671 |
+
time_step=time_step,
|
| 672 |
+
level=level,
|
| 673 |
+
getter=getter,
|
| 674 |
+
is_batch_data=is_batch_data,
|
| 675 |
+
is_ensemble_data=is_ensemble_data,
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
for var, level, getter in [
|
| 679 |
+
('T', 850, lambda x: x['t']),
|
| 680 |
+
('Z', 500, lambda x: x['z']),
|
| 681 |
+
('U', 700, lambda x: x['u']),
|
| 682 |
+
('V', 700, lambda x: x['v']),
|
| 683 |
+
('Q', 700, lambda x: 1000 * x['tracers']['specific_humidity']),
|
| 684 |
+
]:
|
| 685 |
+
for q in [0.99]:
|
| 686 |
+
key = f'abs_error_q{q}_{var}{level}_{t:03d}_hours'
|
| 687 |
+
metrics_dict[key] = AbsErrorQuantile(
|
| 688 |
+
trajectory_spec,
|
| 689 |
+
quantile=q,
|
| 690 |
+
time_step=time_step,
|
| 691 |
+
level=level,
|
| 692 |
+
getter=getter,
|
| 693 |
+
is_ensemble_data=is_ensemble_data,
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
return metrics_dict
|
model/reference_code/metrics_base.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Base classes for Metrics."""
|
| 15 |
+
import dataclasses
|
| 16 |
+
from typing import Callable
|
| 17 |
+
from dinosaur import typing
|
| 18 |
+
import jax
|
| 19 |
+
import jax.numpy as jnp
|
| 20 |
+
import model.reference_code.metrics_util as metrics_util
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
Pytree = typing.Pytree
|
| 24 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 25 |
+
|
| 26 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 27 |
+
tree_map = jax.tree_util.tree_map
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclasses.dataclass
|
| 31 |
+
class Evaluator:
|
| 32 |
+
"""Class that evaluates on (prediction, trajectory) returning Pytree."""
|
| 33 |
+
|
| 34 |
+
def evaluate(
|
| 35 |
+
self,
|
| 36 |
+
prediction: TrajectoryRepresentations,
|
| 37 |
+
target: TrajectoryRepresentations,
|
| 38 |
+
) -> Pytree:
|
| 39 |
+
"""Evaluates giving values of interest."""
|
| 40 |
+
raise NotImplementedError()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclasses.dataclass
|
| 44 |
+
class EvaluateFunctionWrapper(Evaluator):
|
| 45 |
+
"""Wraps `evaluate_fn` function to be used as an Evaluator."""
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
evaluate_fn: Callable[
|
| 50 |
+
[TrajectoryRepresentations, TrajectoryRepresentations], Pytree
|
| 51 |
+
],
|
| 52 |
+
):
|
| 53 |
+
self._evaluate_fn = evaluate_fn
|
| 54 |
+
|
| 55 |
+
def evaluate(
|
| 56 |
+
self,
|
| 57 |
+
prediction: TrajectoryRepresentations,
|
| 58 |
+
target: TrajectoryRepresentations,
|
| 59 |
+
) -> Pytree:
|
| 60 |
+
return self._evaluate_fn(prediction, target)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class MetricRuntimeError(Exception):
|
| 64 |
+
"""Generic error for Metrics to raise in place of generic RuntimeError."""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclasses.dataclass
|
| 68 |
+
class Metric(Evaluator):
|
| 69 |
+
"""An Evaluator that derives information from a TrajectorySpec."""
|
| 70 |
+
|
| 71 |
+
trajectory_spec: metrics_util.TrajectorySpec
|
| 72 |
+
is_nodal: bool = dataclasses.field(default=True, kw_only=True)
|
| 73 |
+
is_encoded: bool = dataclasses.field(default=False, kw_only=True)
|
| 74 |
+
|
| 75 |
+
def get_representation(self, x: TrajectoryRepresentations) -> Pytree:
|
| 76 |
+
x_rep = x.get_representation(
|
| 77 |
+
is_nodal=self.is_nodal, is_encoded=self.is_encoded
|
| 78 |
+
)
|
| 79 |
+
if x_rep is None:
|
| 80 |
+
raise MetricRuntimeError(
|
| 81 |
+
'Desired representation of `x` was None. '
|
| 82 |
+
f'{self.is_nodal=}, {self.is_encoded=}'
|
| 83 |
+
)
|
| 84 |
+
return x_rep
|
| 85 |
+
|
| 86 |
+
def surface_mean(self, trajectory: Pytree) -> Pytree:
|
| 87 |
+
if self.is_encoded:
|
| 88 |
+
coords = self.trajectory_spec.coords
|
| 89 |
+
else:
|
| 90 |
+
coords = self.trajectory_spec.data_coords
|
| 91 |
+
if self.is_nodal:
|
| 92 |
+
# Mean over lat/lon. Converts shapes
|
| 93 |
+
# (n_time, n_level, n_lon, n_lat) --> (n_time, n_level)
|
| 94 |
+
fn = lambda x: metrics_util.nodal_surface_mean(x, coords)
|
| 95 |
+
else:
|
| 96 |
+
fn = lambda x: metrics_util.modal_surface_mean(x, coords)
|
| 97 |
+
return tree_map(fn, trajectory)
|
| 98 |
+
|
| 99 |
+
def mean_per_variable(self, trajectory: Pytree) -> Pytree:
|
| 100 |
+
# In practice this is used to reduce shape (n_time, n_level) --> ()
|
| 101 |
+
return tree_map(jnp.mean, self.surface_mean(trajectory))
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class ScalarMetric(Metric):
|
| 105 |
+
"""Metric that compute scalar quantities."""
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
@dataclasses.dataclass
|
| 109 |
+
class Loss(ScalarMetric):
|
| 110 |
+
"""Metric that can be used as a loss."""
|
| 111 |
+
|
| 112 |
+
trajectory_spec: metrics_util.TrajectorySpec
|
| 113 |
+
is_nodal: bool = dataclasses.field(default=True, kw_only=True)
|
| 114 |
+
is_encoded: bool = dataclasses.field(default=False, kw_only=True)
|
| 115 |
+
time_step: int | slice | None = dataclasses.field(default=None, kw_only=True)
|
| 116 |
+
|
| 117 |
+
def evaluate_per_variable(
|
| 118 |
+
self,
|
| 119 |
+
prediction: TrajectoryRepresentations,
|
| 120 |
+
target: TrajectoryRepresentations,
|
| 121 |
+
) -> Pytree:
|
| 122 |
+
raise NotImplementedError()
|
| 123 |
+
|
| 124 |
+
def evaluate(
|
| 125 |
+
self,
|
| 126 |
+
prediction: TrajectoryRepresentations,
|
| 127 |
+
target: TrajectoryRepresentations,
|
| 128 |
+
) -> jnp.ndarray:
|
| 129 |
+
error_per_variable = self.evaluate_per_variable(prediction, target)
|
| 130 |
+
return sum(tree_leaves(error_per_variable))
|
| 131 |
+
|
| 132 |
+
def debug_loss_terms_instance(self) -> EvaluateFunctionWrapper:
|
| 133 |
+
"""Returns class that evaluates relative loss per variable."""
|
| 134 |
+
|
| 135 |
+
def evaluate_fn(
|
| 136 |
+
prediction: TrajectoryRepresentations,
|
| 137 |
+
target: TrajectoryRepresentations,
|
| 138 |
+
) -> Pytree:
|
| 139 |
+
# self.loss.evaluate takes ensemble mean (to evaluate on ensemble mean) if
|
| 140 |
+
# needed.
|
| 141 |
+
loss_per_variable = self.evaluate_per_variable(prediction, target)
|
| 142 |
+
# here we reduce terms by summation to expose relative contributions,
|
| 143 |
+
# even though the actual total_loss might be different.
|
| 144 |
+
sum_of_all_terms = sum(tree_leaves(loss_per_variable))
|
| 145 |
+
relative_loss = tree_map(
|
| 146 |
+
lambda x: x / sum_of_all_terms, loss_per_variable
|
| 147 |
+
)
|
| 148 |
+
return {'relative_loss': relative_loss}
|
| 149 |
+
|
| 150 |
+
return EvaluateFunctionWrapper(evaluate_fn)
|
model/reference_code/metrics_util.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Shared utilities and classes for metrics and related modules."""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
import dataclasses
|
| 17 |
+
from types import MappingProxyType
|
| 18 |
+
from typing import Callable, Optional, Sequence
|
| 19 |
+
|
| 20 |
+
from dinosaur import coordinate_systems
|
| 21 |
+
from dinosaur import horizontal_interpolation
|
| 22 |
+
from dinosaur import pytree_utils
|
| 23 |
+
from dinosaur import spherical_harmonic
|
| 24 |
+
from dinosaur import typing
|
| 25 |
+
import gin
|
| 26 |
+
import jax
|
| 27 |
+
import jax.extend as jex
|
| 28 |
+
from jax.interpreters import ad
|
| 29 |
+
from jax.interpreters import batching
|
| 30 |
+
from jax.interpreters import mlir
|
| 31 |
+
import jax.numpy as jnp
|
| 32 |
+
import numpy as np
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
tree_map = jax.tree_util.tree_map
|
| 36 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 37 |
+
Pytree = typing.Pytree
|
| 38 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 39 |
+
|
| 40 |
+
# Number of state variables in the model. t/z/u/v/specific_humidity.
|
| 41 |
+
N_VARS = 5
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Axis names.
|
| 45 |
+
TIME = 'time'
|
| 46 |
+
LEVEL = 'level'
|
| 47 |
+
LONGITUDINAL_WAVENUMBER = 'longitudinal_wavenumber'
|
| 48 |
+
TOTAL_WAVENUMBER = 'total_wavenumber'
|
| 49 |
+
LONGITUDINAL = 'longitudinal'
|
| 50 |
+
LATITUDINAL = 'latitudinal'
|
| 51 |
+
|
| 52 |
+
# SPATIAL_AXES is negatively indexed because it is used in a place where there
|
| 53 |
+
# are variable number of leading axis.
|
| 54 |
+
SPATIAL_AXES = (-2, -1)
|
| 55 |
+
TIME_AXIS = 0
|
| 56 |
+
LEVEL_AXIS = 1
|
| 57 |
+
ALL_AXES = (TIME_AXIS, LEVEL_AXIS) + SPATIAL_AXES
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
MODAL_AXIS_INDICES = MappingProxyType({
|
| 61 |
+
TIME: TIME_AXIS,
|
| 62 |
+
LEVEL: LEVEL_AXIS,
|
| 63 |
+
LONGITUDINAL_WAVENUMBER: 2,
|
| 64 |
+
TOTAL_WAVENUMBER: 3,
|
| 65 |
+
})
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
NODAL_AXIS_INDICES = MappingProxyType({
|
| 69 |
+
TIME: TIME_AXIS,
|
| 70 |
+
LEVEL: LEVEL_AXIS,
|
| 71 |
+
LONGITUDINAL: 2,
|
| 72 |
+
LATITUDINAL: 3,
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ShapeError(Exception):
|
| 77 |
+
"""Raised when an unexpected shape is encountered."""
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@dataclasses.dataclass
|
| 81 |
+
class TrajectorySpec:
|
| 82 |
+
"""Specification of a saved model trajectory."""
|
| 83 |
+
|
| 84 |
+
trajectory_length: int # i.e., max "outer steps"
|
| 85 |
+
max_trajectory_length: int # Maximum length for any stage of an Experiment.
|
| 86 |
+
steps_per_save: int # Number of (1 hr) inner steps between each outer step.
|
| 87 |
+
coords: coordinate_systems.CoordinateSystem # i.e., model coords
|
| 88 |
+
data_coords: coordinate_systems.CoordinateSystem # i.e., data coords
|
| 89 |
+
|
| 90 |
+
def __post_init__(self):
|
| 91 |
+
if self.trajectory_length > self.max_trajectory_length:
|
| 92 |
+
raise ValueError(
|
| 93 |
+
f'{self.trajectory_length=} > {self.max_trajectory_length=}.'
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclasses.dataclass
|
| 98 |
+
class TrajectoryShape:
|
| 99 |
+
"""Specifies shape of trajectory after LinearTransforms are applied."""
|
| 100 |
+
|
| 101 |
+
n_times: int
|
| 102 |
+
n_levels: int
|
| 103 |
+
n_longitudinal_wavenumbers: int
|
| 104 |
+
n_total_wavenumbers: int
|
| 105 |
+
n_longitude_nodes: int
|
| 106 |
+
n_latitude_nodes: int
|
| 107 |
+
|
| 108 |
+
def assert_compliant(self, trajectory: typing.Pytree, is_nodal: bool) -> None:
|
| 109 |
+
"""Asserts `trajectory` is compliant with this `TrajectoryShape`.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
trajectory: A trajectory, after LinearTransforms have been applied.
|
| 113 |
+
is_nodal: Whether the trajectory is presumed nodal (vs. modal).
|
| 114 |
+
|
| 115 |
+
Raises:
|
| 116 |
+
ShapeError: If the shape is not compliant.
|
| 117 |
+
"""
|
| 118 |
+
if is_nodal:
|
| 119 |
+
expected_shape = (
|
| 120 |
+
self.n_times,
|
| 121 |
+
self.n_levels,
|
| 122 |
+
self.n_longitude_nodes,
|
| 123 |
+
self.n_latitude_nodes,
|
| 124 |
+
)
|
| 125 |
+
else:
|
| 126 |
+
expected_shape = (
|
| 127 |
+
self.n_times,
|
| 128 |
+
self.n_levels,
|
| 129 |
+
self.n_longitudinal_wavenumbers,
|
| 130 |
+
self.n_total_wavenumbers,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
is_compliant = tree_map(lambda x: np.shape(x) == expected_shape, trajectory)
|
| 134 |
+
if not all(tree_leaves(is_compliant)):
|
| 135 |
+
shapes = tree_map(np.shape, trajectory)
|
| 136 |
+
raise ShapeError(
|
| 137 |
+
f'Some `trajectory` shapes were non-compliant ({is_nodal=}). '
|
| 138 |
+
f'{expected_shape=}. Found {shapes=}. '
|
| 139 |
+
f'This TrajectoryShape is {self}.'
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def nodal_surface_mean(
|
| 144 |
+
x: typing.Array, coords: coordinate_systems.CoordinateSystem
|
| 145 |
+
) -> typing.Array:
|
| 146 |
+
"""Integrates x over the surface of a sphere, normalized by surface area."""
|
| 147 |
+
if x.shape[-2:] != coords.horizontal.nodal_shape[-2:]:
|
| 148 |
+
raise ValueError(f'Input to nodal_surface_mean: {x.shape=}, while expected '
|
| 149 |
+
f'spatial shape is {coords.horizontal.nodal_shape=}.')
|
| 150 |
+
surface_area = 4 * jnp.pi * coords.horizontal.radius**2
|
| 151 |
+
# Changes shape (n_t, n_z, n_lon, n_lat) --> (n_t, n_z)
|
| 152 |
+
return coords.horizontal.integrate(x) / surface_area
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def modal_surface_mean(
|
| 156 |
+
x: typing.Array, coords: coordinate_systems.CoordinateSystem
|
| 157 |
+
) -> typing.Array:
|
| 158 |
+
"""Integrates Σxₖφₖ² over a sphere, normalized by surface area."""
|
| 159 |
+
if x.shape[-2:] != coords.horizontal.modal_shape[-2:]:
|
| 160 |
+
raise ValueError(f'Input to modal_surface_mean: {x.shape=}, while expected '
|
| 161 |
+
f'modal shape is {coords.horizontal.modal_shape=}.')
|
| 162 |
+
# This is equivalent to computing ||f||² / SurfaceArea, where
|
| 163 |
+
# f = Σₖsqrt(x)ₖφₖ
|
| 164 |
+
surface_area = 4 * jnp.pi * coords.horizontal.radius**2
|
| 165 |
+
|
| 166 |
+
# Changes shape (n_t, n_z, m, l) --> (n_t, n_z)
|
| 167 |
+
return jnp.sum(x, axis=SPATIAL_AXES) / surface_area
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def extract_time_slice(trajectory: Pytree, time_slice: slice) -> Pytree:
|
| 171 |
+
return pytree_utils.slice_along_axis(trajectory, TIME_AXIS, time_slice)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def extract_time_step(trajectory: Pytree, time_step: int) -> Pytree:
|
| 175 |
+
return extract_time_slice(trajectory, slice(time_step, time_step + 1))
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def extract_vertical_slice(
|
| 179 |
+
trajectory: Pytree,
|
| 180 |
+
coords: coordinate_systems.CoordinateSystem,
|
| 181 |
+
level: int,
|
| 182 |
+
) -> Pytree:
|
| 183 |
+
i = coords.vertical.centers.tolist().index(level)
|
| 184 |
+
index = slice(i, i + 1)
|
| 185 |
+
trajectory = pytree_utils.slice_along_axis(trajectory, LEVEL_AXIS, index)
|
| 186 |
+
return trajectory
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def filter_sim_time(trajectory: Pytree) -> Pytree:
|
| 190 |
+
if isinstance(trajectory, dict):
|
| 191 |
+
trajectory = dict(trajectory)
|
| 192 |
+
trajectory.pop('sim_time', None)
|
| 193 |
+
return trajectory
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def filter_sim_time_and_diagnostics(trajectory: Pytree) -> Pytree:
|
| 197 |
+
if isinstance(trajectory, dict):
|
| 198 |
+
trajectory = dict(trajectory)
|
| 199 |
+
trajectory.pop('sim_time', None)
|
| 200 |
+
trajectory.pop('diagnostics', None)
|
| 201 |
+
return trajectory
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def extract_variable(
|
| 205 |
+
trajectory: TrajectoryRepresentations,
|
| 206 |
+
trajectory_spec: TrajectorySpec,
|
| 207 |
+
time_step: int | slice | None = None,
|
| 208 |
+
level: int | None = None,
|
| 209 |
+
getter: Callable[[Pytree], Pytree] = filter_sim_time,
|
| 210 |
+
is_nodal: bool = True,
|
| 211 |
+
is_encoded: bool = False,
|
| 212 |
+
) -> Pytree:
|
| 213 |
+
"""Extract a variable from a trajectory."""
|
| 214 |
+
if is_encoded:
|
| 215 |
+
coords = trajectory_spec.coords
|
| 216 |
+
else:
|
| 217 |
+
coords = trajectory_spec.data_coords
|
| 218 |
+
trajectory = trajectory.get_representation(
|
| 219 |
+
is_nodal=is_nodal, is_encoded=is_encoded
|
| 220 |
+
)
|
| 221 |
+
trajectory = getter(trajectory)
|
| 222 |
+
if time_step is not None:
|
| 223 |
+
if isinstance(time_step, slice):
|
| 224 |
+
trajectory = extract_time_slice(trajectory, time_step)
|
| 225 |
+
else:
|
| 226 |
+
trajectory = extract_time_step(trajectory, time_step)
|
| 227 |
+
if level is not None:
|
| 228 |
+
trajectory = extract_vertical_slice(trajectory, coords, level)
|
| 229 |
+
return trajectory
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def replace_with_linear_trucation(
|
| 233 |
+
trajectory_spec: TrajectorySpec,
|
| 234 |
+
) -> TrajectorySpec:
|
| 235 |
+
"""Replaces TrajectorySpec with a TL* version of it."""
|
| 236 |
+
grid = trajectory_spec.data_coords.horizontal
|
| 237 |
+
max_wavenumber = grid.longitude_wavenumbers - 1
|
| 238 |
+
assert max_wavenumber + 2 == grid.total_wavenumbers
|
| 239 |
+
gaussian_nodes = grid.longitude_nodes // 4
|
| 240 |
+
assert gaussian_nodes == grid.latitude_nodes // 2
|
| 241 |
+
|
| 242 |
+
# pytype: disable=attribute-error
|
| 243 |
+
new_horizontal = spherical_harmonic.Grid.construct(
|
| 244 |
+
max_wavenumber=2 * gaussian_nodes - 1, # Larger in TL version
|
| 245 |
+
gaussian_nodes=gaussian_nodes, # Same in T and TL versions
|
| 246 |
+
latitude_spacing=grid.latitude_spacing,
|
| 247 |
+
radius=grid.radius,
|
| 248 |
+
)
|
| 249 |
+
# pytype: enable=attribute-error
|
| 250 |
+
|
| 251 |
+
return dataclasses.replace(
|
| 252 |
+
trajectory_spec,
|
| 253 |
+
data_coords=dataclasses.replace(
|
| 254 |
+
trajectory_spec.data_coords,
|
| 255 |
+
horizontal=new_horizontal,
|
| 256 |
+
),
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def trajectory_4d_shape(
|
| 261 |
+
trajectory_spec: TrajectorySpec,
|
| 262 |
+
keep_levels: Optional[Sequence[float]] = None,
|
| 263 |
+
) -> TrajectoryShape:
|
| 264 |
+
"""Returns the shape of the trajectory leaf values in data representation."""
|
| 265 |
+
if keep_levels is None:
|
| 266 |
+
n_levels = trajectory_spec.data_coords.vertical.layers
|
| 267 |
+
else:
|
| 268 |
+
n_levels = sum(bool(i) for i in keep_levels)
|
| 269 |
+
if n_levels > trajectory_spec.data_coords.vertical.layers:
|
| 270 |
+
raise ValueError(
|
| 271 |
+
f'{n_levels=} implied by `keep_levels` was greater than '
|
| 272 |
+
f'{trajectory_spec.data_coords.vertical.layers=}'
|
| 273 |
+
)
|
| 274 |
+
grid = trajectory_spec.data_coords.horizontal
|
| 275 |
+
n_m, n_l = grid.modal_shape
|
| 276 |
+
return TrajectoryShape(
|
| 277 |
+
n_times=trajectory_spec.trajectory_length,
|
| 278 |
+
n_levels=n_levels,
|
| 279 |
+
n_longitudinal_wavenumbers=n_m,
|
| 280 |
+
n_total_wavenumbers=n_l,
|
| 281 |
+
n_longitude_nodes=grid.longitude_nodes,
|
| 282 |
+
n_latitude_nodes=grid.latitude_nodes,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def pmean_all_axes(x: jax.Array) -> jax.Array:
|
| 287 |
+
"""Average over all vmapped axes."""
|
| 288 |
+
return _pmean_all_axes_p.bind(x)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def _pmean_all_axes_impl(x):
|
| 292 |
+
return x
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def _pmean_all_axes_batch(args, batch_axes):
|
| 296 |
+
(x,) = args
|
| 297 |
+
(batch_axis,) = batch_axes
|
| 298 |
+
y = jnp.broadcast_to(x.mean(axis=batch_axes, keepdims=True), x.shape)
|
| 299 |
+
return _pmean_all_axes_p.bind(y), batch_axis
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
_pmean_all_axes_p = jex.core.Primitive('pmean_all_axes')
|
| 303 |
+
_pmean_all_axes_p.def_impl(_pmean_all_axes_impl)
|
| 304 |
+
_pmean_all_axes_p.def_abstract_eval(_pmean_all_axes_impl)
|
| 305 |
+
batching.primitive_batchers[_pmean_all_axes_p] = _pmean_all_axes_batch
|
| 306 |
+
ad.deflinear(_pmean_all_axes_p, lambda cotangent: [pmean_all_axes(cotangent)])
|
| 307 |
+
mlir.register_lowering(
|
| 308 |
+
_pmean_all_axes_p,
|
| 309 |
+
mlir.lower_fun(_pmean_all_axes_impl, multiple_results=False),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@dataclasses.dataclass
|
| 314 |
+
class AggregationTransform:
|
| 315 |
+
"""A transformation that aggregates spatial or temporal groups in inputs.
|
| 316 |
+
|
| 317 |
+
These transformations are useful for (1) coarsening of error observations and
|
| 318 |
+
(2) aggregation of error norms to compute L2^2 distance between two vectors.
|
| 319 |
+
The former case does not strictly impose any restrictions on the coarsening
|
| 320 |
+
transformation, although in most cases we would expect it to be a form of a
|
| 321 |
+
linear, non-invertible transformation. The latter requires that the result of
|
| 322 |
+
aggregation of non-negative values is non-negative.
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
trajectory_spec: TrajectorySpec
|
| 326 |
+
out_trajectory_spec: TrajectorySpec
|
| 327 |
+
is_nodal: bool
|
| 328 |
+
is_encoded: bool
|
| 329 |
+
|
| 330 |
+
def __call__(self, inputs: Pytree) -> Pytree:
|
| 331 |
+
raise NotImplementedError
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
AggregationTransformConstructor = Callable[..., AggregationTransform]
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
@gin.register
|
| 338 |
+
class AggregateIdentity(AggregationTransform):
|
| 339 |
+
|
| 340 |
+
def __init__(
|
| 341 |
+
self,
|
| 342 |
+
trajectory_spec: TrajectorySpec,
|
| 343 |
+
is_nodal: bool,
|
| 344 |
+
is_encoded: bool,
|
| 345 |
+
):
|
| 346 |
+
super().__init__(trajectory_spec, trajectory_spec, is_nodal, is_encoded)
|
| 347 |
+
|
| 348 |
+
def __call__(self, inputs: Pytree) -> Pytree:
|
| 349 |
+
return inputs
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@gin.register
|
| 353 |
+
class SumVariables(AggregationTransform):
|
| 354 |
+
"""Transform that adds sums all variables aka pytree leaves of inputs."""
|
| 355 |
+
|
| 356 |
+
def __init__(
|
| 357 |
+
self,
|
| 358 |
+
trajectory_spec: TrajectorySpec,
|
| 359 |
+
is_nodal: bool,
|
| 360 |
+
is_encoded: bool,
|
| 361 |
+
):
|
| 362 |
+
super().__init__(trajectory_spec, trajectory_spec, is_nodal, is_encoded)
|
| 363 |
+
|
| 364 |
+
def __call__(self, inputs: Pytree) -> Pytree:
|
| 365 |
+
return sum(jax.tree_util.tree_leaves(inputs))
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@gin.register
|
| 369 |
+
class RegriddingAggregation(AggregationTransform):
|
| 370 |
+
"""Transform that aggregates horizontal cells via regridding.
|
| 371 |
+
|
| 372 |
+
To perform aggregation over a few nearby lon/lat cells this transform performs
|
| 373 |
+
regridding to a coarser `target_grid`. By default, the aggregated value
|
| 374 |
+
contains a regridded (i.e. mean) value of the inputs. Setting `scale_by_area`
|
| 375 |
+
to `True` multiplies outputs by an area which is close to area-weighted
|
| 376 |
+
aggregation.
|
| 377 |
+
"""
|
| 378 |
+
|
| 379 |
+
def __init__(
|
| 380 |
+
self,
|
| 381 |
+
trajectory_spec: TrajectorySpec,
|
| 382 |
+
is_nodal: bool,
|
| 383 |
+
is_encoded: bool,
|
| 384 |
+
target_grid: coordinate_systems.CoordinateSystem,
|
| 385 |
+
scale_by_area: bool = False,
|
| 386 |
+
):
|
| 387 |
+
if not is_nodal:
|
| 388 |
+
raise ValueError('AggregateHorizontal is only supported on nodal data')
|
| 389 |
+
if is_encoded:
|
| 390 |
+
source_coords = trajectory_spec.coords
|
| 391 |
+
coords = dataclasses.replace(source_coords, horizontal=target_grid) # pytype: disable=wrong-arg-types # dataclasses-replace-types
|
| 392 |
+
out_trajectory_spec = dataclasses.replace(trajectory_spec, coords=coords)
|
| 393 |
+
else:
|
| 394 |
+
source_coords = trajectory_spec.data_coords
|
| 395 |
+
coords = dataclasses.replace(source_coords, horizontal=target_grid) # pytype: disable=wrong-arg-types # dataclasses-replace-types
|
| 396 |
+
out_trajectory_spec = dataclasses.replace(
|
| 397 |
+
trajectory_spec, data_coords=coords)
|
| 398 |
+
super().__init__(trajectory_spec, out_trajectory_spec, is_nodal, is_encoded)
|
| 399 |
+
self.regrid_fn = horizontal_interpolation.ConservativeRegridder(
|
| 400 |
+
source_coords.horizontal, coords.horizontal)
|
| 401 |
+
# conservative regridding computes weighted averages rather than aggregation
|
| 402 |
+
# so we reweight the results by area.
|
| 403 |
+
lower_lon_boundaries = horizontal_interpolation._periodic_lower_bounds(
|
| 404 |
+
coords.horizontal.longitudes, 2 * np.pi)
|
| 405 |
+
upper_lon_boundaries = horizontal_interpolation._periodic_upper_bounds(
|
| 406 |
+
coords.horizontal.longitudes, 2 * np.pi)
|
| 407 |
+
lat_boundaries = horizontal_interpolation._latitude_cell_bounds(
|
| 408 |
+
coords.horizontal.latitudes)
|
| 409 |
+
lon_weights = upper_lon_boundaries - lower_lon_boundaries
|
| 410 |
+
lat_weights = jnp.sin(lat_boundaries[1:]) - jnp.sin(lat_boundaries[:-1])
|
| 411 |
+
self.weights = lat_weights[np.newaxis, :] * lon_weights[:, np.newaxis]
|
| 412 |
+
self.scale_by_area = scale_by_area
|
| 413 |
+
|
| 414 |
+
def __call__(self, inputs: Pytree) -> Pytree:
|
| 415 |
+
if self.scale_by_area:
|
| 416 |
+
return tree_map(lambda x: self.regrid_fn(x) * self.weights, inputs)
|
| 417 |
+
else:
|
| 418 |
+
return tree_map(self.regrid_fn, inputs)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
@gin.register
|
| 422 |
+
class TimeWindowSum(AggregationTransform):
|
| 423 |
+
"""Transform that sums temporal blocks of `time_window_size`."""
|
| 424 |
+
|
| 425 |
+
def __init__(
|
| 426 |
+
self,
|
| 427 |
+
trajectory_spec: TrajectorySpec,
|
| 428 |
+
is_nodal: bool,
|
| 429 |
+
is_encoded: bool,
|
| 430 |
+
time_window_size: int,
|
| 431 |
+
):
|
| 432 |
+
trajectory_length = trajectory_spec.trajectory_length
|
| 433 |
+
if trajectory_length % time_window_size != 0:
|
| 434 |
+
raise ValueError(f'Cannot aggregate {trajectory_length=} '
|
| 435 |
+
f'into {time_window_size=} sections.')
|
| 436 |
+
new_length = trajectory_spec.trajectory_length // time_window_size
|
| 437 |
+
out_trajectory_spec = dataclasses.replace(
|
| 438 |
+
trajectory_spec,
|
| 439 |
+
trajectory_length=new_length,
|
| 440 |
+
steps_per_save=trajectory_spec.steps_per_save * time_window_size)
|
| 441 |
+
super().__init__(trajectory_spec, out_trajectory_spec, is_nodal, is_encoded)
|
| 442 |
+
eye = np.eye(trajectory_length)
|
| 443 |
+
# columns of the weight matrix have 1s in rows that are in the same window.
|
| 444 |
+
# see http://screen/8CaZoBVNPtjpwNu for a hint.
|
| 445 |
+
self.time_axis_weights = sum(
|
| 446 |
+
[np.roll(eye, i, 0) for i in range(time_window_size)]
|
| 447 |
+
)[:, ::time_window_size]
|
| 448 |
+
|
| 449 |
+
def __call__(self, inputs: Pytree) -> Pytree:
|
| 450 |
+
def _aggregate_time(x: jax.Array):
|
| 451 |
+
return jnp.einsum(
|
| 452 |
+
'tk,...thml->...khml', self.time_axis_weights, x, precision='float32')
|
| 453 |
+
return tree_map(_aggregate_time, inputs)
|
model/reference_code/paper_configs/deterministic_0_7_deg.gin
ADDED
|
@@ -0,0 +1,2368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Macros:
|
| 2 |
+
# ==============================================================================
|
| 3 |
+
ACTIVATION = @gelu
|
| 4 |
+
BASE_SHAPE_MULTIPLE = None
|
| 5 |
+
CORRECTOR_MODULE = @CustomCoordsCorrector
|
| 6 |
+
CORRECTOR_SCALE = 0.01
|
| 7 |
+
DATA_FILTER_ATTENUATION = 0.0
|
| 8 |
+
DYCORE_FILTER_ORDER = 3
|
| 9 |
+
DYCORE_GRID = @GridWithWavenumbers()
|
| 10 |
+
DYCORE_INTEGRATOR = @imex_rk_sil3
|
| 11 |
+
DYCORE_TAU = '120 minutes'
|
| 12 |
+
GLOBAL_OUT_SCALE = 0.01
|
| 13 |
+
LATENT_SIZE = 384
|
| 14 |
+
LAYER_SIZE = 384
|
| 15 |
+
N_CNN_FEATURES = 32
|
| 16 |
+
N_INNER_DYCORE_STEPS = 8
|
| 17 |
+
N_SIGMA_LAYERS = 32
|
| 18 |
+
N_TO_CLIP = 1
|
| 19 |
+
NUM_BLOCKS = 5
|
| 20 |
+
NUM_SUBSTEPS = 2
|
| 21 |
+
PARAMETERIZATION_FILTER = @ml/SequentialStepFilter
|
| 22 |
+
POSITIONAL_LATENT_SIZE = 32
|
| 23 |
+
REVERSE_EINSUM_ARG_ORDER = None
|
| 24 |
+
STABILITY_TAU = '4 minutes'
|
| 25 |
+
SURFACE_MODEL_LATENT_SIZE = 8
|
| 26 |
+
SURFACE_MODEL_LAYER_SIZE = 8
|
| 27 |
+
SURFACE_MODEL_OUTPUT_SIZE = 8
|
| 28 |
+
|
| 29 |
+
# Parameters for decode/ColumnTower:
|
| 30 |
+
# ==============================================================================
|
| 31 |
+
decode/ColumnTower.checkpoint_tower = False
|
| 32 |
+
decode/ColumnTower.column_net_factory = @decode/MlpUniform
|
| 33 |
+
decode/ColumnTower.name = 'decode_tower'
|
| 34 |
+
|
| 35 |
+
# Parameters for encode/ColumnTower:
|
| 36 |
+
# ==============================================================================
|
| 37 |
+
encode/ColumnTower.checkpoint_tower = False
|
| 38 |
+
encode/ColumnTower.column_net_factory = @encode/MlpUniform
|
| 39 |
+
encode/ColumnTower.name = 'encode_tower'
|
| 40 |
+
|
| 41 |
+
# Parameters for process/ColumnTower:
|
| 42 |
+
# ==============================================================================
|
| 43 |
+
process/ColumnTower.checkpoint_tower = False
|
| 44 |
+
process/ColumnTower.column_net_factory = @process/MlpUniform
|
| 45 |
+
process/ColumnTower.name = 'process_tower'
|
| 46 |
+
|
| 47 |
+
# Parameters for surface_model_decode/ColumnTower:
|
| 48 |
+
# ==============================================================================
|
| 49 |
+
surface_model_decode/ColumnTower.checkpoint_tower = False
|
| 50 |
+
surface_model_decode/ColumnTower.column_net_factory = \
|
| 51 |
+
@surface_model_decode/MlpUniform
|
| 52 |
+
surface_model_decode/ColumnTower.name = 'surface_model_decode_tower'
|
| 53 |
+
|
| 54 |
+
# Parameters for surface_model_encode/ColumnTower:
|
| 55 |
+
# ==============================================================================
|
| 56 |
+
surface_model_encode/ColumnTower.checkpoint_tower = False
|
| 57 |
+
surface_model_encode/ColumnTower.column_net_factory = \
|
| 58 |
+
@surface_model_encode/MlpUniform
|
| 59 |
+
surface_model_encode/ColumnTower.name = 'surface_model_encode_tower'
|
| 60 |
+
|
| 61 |
+
# Parameters for surface_model_process/ColumnTower:
|
| 62 |
+
# ==============================================================================
|
| 63 |
+
surface_model_process/ColumnTower.checkpoint_tower = False
|
| 64 |
+
surface_model_process/ColumnTower.column_net_factory = \
|
| 65 |
+
@surface_model_process/MlpUniform
|
| 66 |
+
surface_model_process/ColumnTower.name = 'surface_model_process_tower'
|
| 67 |
+
|
| 68 |
+
# Parameters for advance/CombinedFeatures:
|
| 69 |
+
# ==============================================================================
|
| 70 |
+
advance/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 71 |
+
advance/CombinedFeatures.feature_modules = \
|
| 72 |
+
(@EmbeddingSurfaceFeatures,
|
| 73 |
+
@EmbeddingVolumeFeatures,
|
| 74 |
+
@PressureFeatures,
|
| 75 |
+
@RadiationFeatures,
|
| 76 |
+
@LatitudeFeatures,
|
| 77 |
+
@advance/VelocityAndPrognostics,
|
| 78 |
+
@MemoryVelocityAndValues,
|
| 79 |
+
@with_grads/FloatDataFeatures,
|
| 80 |
+
@without_grads/FloatDataFeatures,
|
| 81 |
+
@LearnedPositionalFeatures)
|
| 82 |
+
advance/CombinedFeatures.features_to_exclude = ()
|
| 83 |
+
advance/CombinedFeatures.features_transform_module = @advance/SequentialTransform
|
| 84 |
+
advance/CombinedFeatures.name = None
|
| 85 |
+
|
| 86 |
+
# Parameters for decoder_model/CombinedFeatures:
|
| 87 |
+
# ==============================================================================
|
| 88 |
+
decoder_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 89 |
+
decoder_model/CombinedFeatures.feature_modules = \
|
| 90 |
+
(@RadiationFeatures,
|
| 91 |
+
@LatitudeFeatures,
|
| 92 |
+
@model/VelocityAndPrognostics,
|
| 93 |
+
@with_grads/FloatDataFeatures,
|
| 94 |
+
@without_grads/FloatDataFeatures,
|
| 95 |
+
@LearnedPositionalFeatures)
|
| 96 |
+
decoder_model/CombinedFeatures.features_to_exclude = ()
|
| 97 |
+
decoder_model/CombinedFeatures.features_transform_module = \
|
| 98 |
+
@decoder_model/SequentialTransform
|
| 99 |
+
decoder_model/CombinedFeatures.name = None
|
| 100 |
+
|
| 101 |
+
# Parameters for embedding_model/CombinedFeatures:
|
| 102 |
+
# ==============================================================================
|
| 103 |
+
embedding_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 104 |
+
embedding_model/CombinedFeatures.feature_modules = \
|
| 105 |
+
(@embedding_model/VelocityAndPrognostics, @PressureFeatures)
|
| 106 |
+
embedding_model/CombinedFeatures.features_to_exclude = ()
|
| 107 |
+
embedding_model/CombinedFeatures.features_transform_module = \
|
| 108 |
+
@embedding_model/ShiftAndNormalize
|
| 109 |
+
embedding_model/CombinedFeatures.name = None
|
| 110 |
+
|
| 111 |
+
# Parameters for encoder_data/CombinedFeatures:
|
| 112 |
+
# ==============================================================================
|
| 113 |
+
encoder_data/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 114 |
+
encoder_data/CombinedFeatures.feature_modules = \
|
| 115 |
+
(@RadiationFeatures,
|
| 116 |
+
@LatitudeFeatures,
|
| 117 |
+
@encoder_data/VelocityAndPrognostics,
|
| 118 |
+
@with_grads/FloatDataFeatures,
|
| 119 |
+
@without_grads/FloatDataFeatures,
|
| 120 |
+
@LearnedPositionalFeatures)
|
| 121 |
+
encoder_data/CombinedFeatures.features_to_exclude = ()
|
| 122 |
+
encoder_data/CombinedFeatures.features_transform_module = \
|
| 123 |
+
@encoder_data/SequentialTransform
|
| 124 |
+
encoder_data/CombinedFeatures.name = None
|
| 125 |
+
|
| 126 |
+
# Parameters for sea_model/CombinedFeatures:
|
| 127 |
+
# ==============================================================================
|
| 128 |
+
sea_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 129 |
+
sea_model/CombinedFeatures.feature_modules = (@sea_model/ForcingFeatures,)
|
| 130 |
+
sea_model/CombinedFeatures.features_to_exclude = ()
|
| 131 |
+
sea_model/CombinedFeatures.features_transform_module = @sea_model/ShiftAndNormalize
|
| 132 |
+
sea_model/CombinedFeatures.name = None
|
| 133 |
+
|
| 134 |
+
# Parameters for coordinate_system_from_dataset:
|
| 135 |
+
# ==============================================================================
|
| 136 |
+
coordinate_system_from_dataset.spherical_harmonics_impl = \
|
| 137 |
+
@RealSphericalHarmonicsWithZeroImag
|
| 138 |
+
coordinate_system_from_dataset.truncation = 'LINEAR'
|
| 139 |
+
|
| 140 |
+
# Parameters for CoordinateSystem:
|
| 141 |
+
# ==============================================================================
|
| 142 |
+
CoordinateSystem.horizontal = @GridTL255()
|
| 143 |
+
CoordinateSystem.vertical = @SigmaCoordinatesEquidistant()
|
| 144 |
+
|
| 145 |
+
# Parameters for custom_corrds/CoordinateSystem:
|
| 146 |
+
# ==============================================================================
|
| 147 |
+
custom_corrds/CoordinateSystem.horizontal = %DYCORE_GRID
|
| 148 |
+
custom_corrds/CoordinateSystem.vertical = @SigmaCoordinatesEquidistant()
|
| 149 |
+
|
| 150 |
+
# Parameters for CustomCoordsCorrector:
|
| 151 |
+
# ==============================================================================
|
| 152 |
+
CustomCoordsCorrector.corrector_module = @DycoreWithPhysicsCorrector
|
| 153 |
+
CustomCoordsCorrector.custom_coords = @custom_corrds/CoordinateSystem()
|
| 154 |
+
CustomCoordsCorrector.name = None
|
| 155 |
+
|
| 156 |
+
# Parameters for data_to_xarray_with_renaming:
|
| 157 |
+
# ==============================================================================
|
| 158 |
+
data_to_xarray_with_renaming.additional_coords = None
|
| 159 |
+
data_to_xarray_with_renaming.attrs = None
|
| 160 |
+
data_to_xarray_with_renaming.renaming_dict = \
|
| 161 |
+
{'geopotential': 'z',
|
| 162 |
+
'latitude': 'lat',
|
| 163 |
+
'longitude': 'lon',
|
| 164 |
+
'temperature': 't',
|
| 165 |
+
'u_component_of_wind': 'u',
|
| 166 |
+
'v_component_of_wind': 'v'}
|
| 167 |
+
data_to_xarray_with_renaming.sample_ids = None
|
| 168 |
+
data_to_xarray_with_renaming.to_xarray_fn = @primitive_eq_to_xarray
|
| 169 |
+
|
| 170 |
+
# Parameters for divergence/DataExponentialFilter:
|
| 171 |
+
# ==============================================================================
|
| 172 |
+
divergence/DataExponentialFilter.attenuation = 14.0
|
| 173 |
+
divergence/DataExponentialFilter.cutoff = 0.62
|
| 174 |
+
divergence/DataExponentialFilter.name = None
|
| 175 |
+
divergence/DataExponentialFilter.order = 2
|
| 176 |
+
|
| 177 |
+
# Parameters for lsp/DataExponentialFilter:
|
| 178 |
+
# ==============================================================================
|
| 179 |
+
lsp/DataExponentialFilter.attenuation = 8
|
| 180 |
+
lsp/DataExponentialFilter.cutoff = 0.82
|
| 181 |
+
lsp/DataExponentialFilter.name = None
|
| 182 |
+
lsp/DataExponentialFilter.order = 1
|
| 183 |
+
|
| 184 |
+
# Parameters for orography/DataExponentialFilter:
|
| 185 |
+
# ==============================================================================
|
| 186 |
+
orography/DataExponentialFilter.attenuation = %DATA_FILTER_ATTENUATION
|
| 187 |
+
orography/DataExponentialFilter.cutoff = 0
|
| 188 |
+
orography/DataExponentialFilter.name = None
|
| 189 |
+
orography/DataExponentialFilter.order = 1
|
| 190 |
+
|
| 191 |
+
# Parameters for q/DataExponentialFilter:
|
| 192 |
+
# ==============================================================================
|
| 193 |
+
q/DataExponentialFilter.attenuation = 17
|
| 194 |
+
q/DataExponentialFilter.cutoff = 0.62
|
| 195 |
+
q/DataExponentialFilter.name = None
|
| 196 |
+
q/DataExponentialFilter.order = 2
|
| 197 |
+
|
| 198 |
+
# Parameters for temperature_variation/DataExponentialFilter:
|
| 199 |
+
# ==============================================================================
|
| 200 |
+
temperature_variation/DataExponentialFilter.attenuation = 13.0
|
| 201 |
+
temperature_variation/DataExponentialFilter.cutoff = 0.45
|
| 202 |
+
temperature_variation/DataExponentialFilter.name = None
|
| 203 |
+
temperature_variation/DataExponentialFilter.order = 3
|
| 204 |
+
|
| 205 |
+
# Parameters for vorticity/DataExponentialFilter:
|
| 206 |
+
# ==============================================================================
|
| 207 |
+
vorticity/DataExponentialFilter.attenuation = 14
|
| 208 |
+
vorticity/DataExponentialFilter.cutoff = 0.62
|
| 209 |
+
vorticity/DataExponentialFilter.name = None
|
| 210 |
+
vorticity/DataExponentialFilter.order = 2
|
| 211 |
+
|
| 212 |
+
# Parameters for DataNoFilter:
|
| 213 |
+
# ==============================================================================
|
| 214 |
+
DataNoFilter.name = None
|
| 215 |
+
|
| 216 |
+
# Parameters for DimensionalLearnedPrimitiveToWeatherbenchDecoder:
|
| 217 |
+
# ==============================================================================
|
| 218 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.correction_transform_module = \
|
| 219 |
+
@decoder/SequentialTransform
|
| 220 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.inputs_to_units_mapping = \
|
| 221 |
+
{'sim_time': 'dimensionless',
|
| 222 |
+
't': 'kelvin',
|
| 223 |
+
'tracers': {'specific_cloud_ice_water_content': 'dimensionless',
|
| 224 |
+
'specific_cloud_liquid_water_content': 'dimensionless',
|
| 225 |
+
'specific_humidity': 'dimensionless'},
|
| 226 |
+
'u': 'meter / second',
|
| 227 |
+
'v': 'meter / second',
|
| 228 |
+
'z': 'm**2 s**-2'}
|
| 229 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.modal_to_nodal_data_features_module = \
|
| 230 |
+
@NullFeatures
|
| 231 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.modal_to_nodal_model_features_module = \
|
| 232 |
+
@decoder_model/CombinedFeatures
|
| 233 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.name = None
|
| 234 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.nodal_mapping_module = \
|
| 235 |
+
@NodalMapping
|
| 236 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.orography_module = \
|
| 237 |
+
@LearnedOrography
|
| 238 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.prediction_mask = \
|
| 239 |
+
{'sim_time': False,
|
| 240 |
+
't': True,
|
| 241 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 242 |
+
'specific_cloud_liquid_water_content': True,
|
| 243 |
+
'specific_humidity': True},
|
| 244 |
+
'u': True,
|
| 245 |
+
'v': True,
|
| 246 |
+
'z': True}
|
| 247 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.time_axis = 0
|
| 248 |
+
|
| 249 |
+
# Parameters for DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder:
|
| 250 |
+
# ==============================================================================
|
| 251 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.correction_transform_module = \
|
| 252 |
+
@encode/SequentialTransform
|
| 253 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.inputs_to_units_mapping = \
|
| 254 |
+
{'sim_time': 'dimensionless',
|
| 255 |
+
't': 'kelvin',
|
| 256 |
+
'tracers': {'specific_cloud_ice_water_content': 'dimensionless',
|
| 257 |
+
'specific_cloud_liquid_water_content': 'dimensionless',
|
| 258 |
+
'specific_humidity': 'dimensionless'},
|
| 259 |
+
'u': 'meter / second',
|
| 260 |
+
'v': 'meter / second',
|
| 261 |
+
'z': 'm**2 s**-2'}
|
| 262 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.modal_to_nodal_data_features_module = \
|
| 263 |
+
@encoder_data/CombinedFeatures
|
| 264 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.modal_to_nodal_model_features_module = \
|
| 265 |
+
@NullFeatures
|
| 266 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.name = None
|
| 267 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.nodal_mapping_module = \
|
| 268 |
+
@NodalMapping
|
| 269 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.orography_module = \
|
| 270 |
+
@LearnedOrography
|
| 271 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.prediction_mask = \
|
| 272 |
+
{'divergence': True,
|
| 273 |
+
'log_surface_pressure': True,
|
| 274 |
+
'sim_time': False,
|
| 275 |
+
'temperature_variation': True,
|
| 276 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 277 |
+
'specific_cloud_liquid_water_content': True,
|
| 278 |
+
'specific_humidity': True},
|
| 279 |
+
'vorticity': True}
|
| 280 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.time_axis = 0
|
| 281 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.transform_module = \
|
| 282 |
+
@EncoderCombinedTransform
|
| 283 |
+
|
| 284 |
+
# Parameters for DivCurlNeuralParameterization:
|
| 285 |
+
# ==============================================================================
|
| 286 |
+
DivCurlNeuralParameterization.filter_module = %PARAMETERIZATION_FILTER
|
| 287 |
+
DivCurlNeuralParameterization.modal_to_nodal_features_module = \
|
| 288 |
+
@advance/CombinedFeatures
|
| 289 |
+
DivCurlNeuralParameterization.name = None
|
| 290 |
+
DivCurlNeuralParameterization.nodal_mapping_module = @NodalMapping
|
| 291 |
+
DivCurlNeuralParameterization.prediction_mask = \
|
| 292 |
+
{'divergence': True,
|
| 293 |
+
'log_surface_pressure': False,
|
| 294 |
+
'sim_time': False,
|
| 295 |
+
'temperature_variation': True,
|
| 296 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 297 |
+
'specific_cloud_liquid_water_content': True,
|
| 298 |
+
'specific_humidity': True},
|
| 299 |
+
'vorticity': True}
|
| 300 |
+
DivCurlNeuralParameterization.tendency_transform_module = \
|
| 301 |
+
@div_curl_tendency_outputs/SequentialTransform
|
| 302 |
+
|
| 303 |
+
# Parameters for DycoreWithPhysicsCorrector:
|
| 304 |
+
# ==============================================================================
|
| 305 |
+
DycoreWithPhysicsCorrector.checkpoint_explicit_terms = True
|
| 306 |
+
DycoreWithPhysicsCorrector.dycore_equation_module = \
|
| 307 |
+
@MoistPrimitiveEquationsWithCloudMoisture
|
| 308 |
+
DycoreWithPhysicsCorrector.dycore_substeps = %N_INNER_DYCORE_STEPS
|
| 309 |
+
DycoreWithPhysicsCorrector.filter_module = @dycore/SequentialStepFilter
|
| 310 |
+
DycoreWithPhysicsCorrector.name = None
|
| 311 |
+
DycoreWithPhysicsCorrector.time_integrator = %DYCORE_INTEGRATOR
|
| 312 |
+
|
| 313 |
+
# Parameters for DynamicDataForcing:
|
| 314 |
+
# ==============================================================================
|
| 315 |
+
DynamicDataForcing.check_sim_time_errors = False
|
| 316 |
+
DynamicDataForcing.data_time_step = '6 hours'
|
| 317 |
+
DynamicDataForcing.dt_tolerance = '1 year'
|
| 318 |
+
DynamicDataForcing.inputs_to_units_mapping = \
|
| 319 |
+
{'sea_ice_cover': 'dimensionless',
|
| 320 |
+
'sea_surface_temperature': 'kelvin',
|
| 321 |
+
'sim_time': 'dimensionless'}
|
| 322 |
+
DynamicDataForcing.name = None
|
| 323 |
+
DynamicDataForcing.time_axis = 0
|
| 324 |
+
|
| 325 |
+
# Parameters for advance/EmbeddingSurfaceFeatures:
|
| 326 |
+
# ==============================================================================
|
| 327 |
+
advance/EmbeddingSurfaceFeatures.embedding_module = @NodalLandSeaIceEmbedding
|
| 328 |
+
advance/EmbeddingSurfaceFeatures.feature_name = 'surface_embedding'
|
| 329 |
+
advance/EmbeddingSurfaceFeatures.name = None
|
| 330 |
+
advance/EmbeddingSurfaceFeatures.output_size = %SURFACE_MODEL_OUTPUT_SIZE
|
| 331 |
+
|
| 332 |
+
# Parameters for advance/EmbeddingVolumeFeatures:
|
| 333 |
+
# ==============================================================================
|
| 334 |
+
advance/EmbeddingVolumeFeatures.embedding_module = @ModalToNodalEmbedding
|
| 335 |
+
advance/EmbeddingVolumeFeatures.feature_name = 'CNN1D'
|
| 336 |
+
advance/EmbeddingVolumeFeatures.name = None
|
| 337 |
+
advance/EmbeddingVolumeFeatures.output_size = %N_CNN_FEATURES
|
| 338 |
+
|
| 339 |
+
# Parameters for EncoderCombinedTransform:
|
| 340 |
+
# ==============================================================================
|
| 341 |
+
EncoderCombinedTransform.name = None
|
| 342 |
+
EncoderCombinedTransform.transforms = \
|
| 343 |
+
(@InputClipTransform, @EncoderFilterTransform)
|
| 344 |
+
|
| 345 |
+
# Parameters for EncoderFilterTransform:
|
| 346 |
+
# ==============================================================================
|
| 347 |
+
EncoderFilterTransform.filter_modules = (@PerVariableDataFilter,)
|
| 348 |
+
EncoderFilterTransform.name = None
|
| 349 |
+
|
| 350 |
+
# Parameters for EpdTower:
|
| 351 |
+
# ==============================================================================
|
| 352 |
+
EpdTower.decode_tower_factory = @decode/ColumnTower
|
| 353 |
+
EpdTower.encode_tower_factory = @encode/ColumnTower
|
| 354 |
+
EpdTower.final_activation = None
|
| 355 |
+
EpdTower.latent_size = %LATENT_SIZE
|
| 356 |
+
EpdTower.name = None
|
| 357 |
+
EpdTower.num_process_blocks = %NUM_BLOCKS
|
| 358 |
+
EpdTower.post_encode_activation = None
|
| 359 |
+
EpdTower.pre_decode_activation = None
|
| 360 |
+
EpdTower.process_tower_factory = @process/ColumnTower
|
| 361 |
+
|
| 362 |
+
# Parameters for surface_model/EpdTower:
|
| 363 |
+
# ==============================================================================
|
| 364 |
+
surface_model/EpdTower.decode_tower_factory = @surface_model_decode/ColumnTower
|
| 365 |
+
surface_model/EpdTower.encode_tower_factory = @surface_model_encode/ColumnTower
|
| 366 |
+
surface_model/EpdTower.final_activation = None
|
| 367 |
+
surface_model/EpdTower.latent_size = %SURFACE_MODEL_LATENT_SIZE
|
| 368 |
+
surface_model/EpdTower.name = None
|
| 369 |
+
surface_model/EpdTower.num_process_blocks = 1
|
| 370 |
+
surface_model/EpdTower.post_encode_activation = None
|
| 371 |
+
surface_model/EpdTower.pre_decode_activation = None
|
| 372 |
+
surface_model/EpdTower.process_tower_factory = @surface_model_process/ColumnTower
|
| 373 |
+
|
| 374 |
+
# Parameters for dycore/ExponentialFilter:
|
| 375 |
+
# ==============================================================================
|
| 376 |
+
dycore/ExponentialFilter.cutoff = 0
|
| 377 |
+
dycore/ExponentialFilter.name = None
|
| 378 |
+
dycore/ExponentialFilter.order = %DYCORE_FILTER_ORDER
|
| 379 |
+
dycore/ExponentialFilter.tau = %DYCORE_TAU
|
| 380 |
+
|
| 381 |
+
# Parameters for stability/ExponentialFilter:
|
| 382 |
+
# ==============================================================================
|
| 383 |
+
stability/ExponentialFilter.cutoff = 0.4
|
| 384 |
+
stability/ExponentialFilter.name = None
|
| 385 |
+
stability/ExponentialFilter.order = 6
|
| 386 |
+
stability/ExponentialFilter.tau = %STABILITY_TAU
|
| 387 |
+
|
| 388 |
+
# Parameters for FilteredCustomOrography:
|
| 389 |
+
# ==============================================================================
|
| 390 |
+
FilteredCustomOrography.filter_modules = (@orography/DataExponentialFilter,)
|
| 391 |
+
FilteredCustomOrography.name = None
|
| 392 |
+
FilteredCustomOrography.orography_data_path = None
|
| 393 |
+
FilteredCustomOrography.renaming_dict = {'latitude': 'lat', 'longitude': 'lon'}
|
| 394 |
+
|
| 395 |
+
# Parameters for with_grads/FloatDataFeatures:
|
| 396 |
+
# ==============================================================================
|
| 397 |
+
with_grads/FloatDataFeatures.compute_gradients_module = @ToModalDiffOperators
|
| 398 |
+
with_grads/FloatDataFeatures.covariate_data_path = None
|
| 399 |
+
with_grads/FloatDataFeatures.covariate_keys = ('geopotential_at_surface',)
|
| 400 |
+
with_grads/FloatDataFeatures.name = None
|
| 401 |
+
with_grads/FloatDataFeatures.renaming_dict = \
|
| 402 |
+
{'latitude': 'lat', 'longitude': 'lon'}
|
| 403 |
+
|
| 404 |
+
# Parameters for without_grads/FloatDataFeatures:
|
| 405 |
+
# ==============================================================================
|
| 406 |
+
without_grads/FloatDataFeatures.covariate_data_path = None
|
| 407 |
+
without_grads/FloatDataFeatures.covariate_keys = ('land_sea_mask',)
|
| 408 |
+
without_grads/FloatDataFeatures.name = None
|
| 409 |
+
without_grads/FloatDataFeatures.renaming_dict = \
|
| 410 |
+
{'latitude': 'lat', 'longitude': 'lon'}
|
| 411 |
+
|
| 412 |
+
# Parameters for sea_model/ForcingFeatures:
|
| 413 |
+
# ==============================================================================
|
| 414 |
+
sea_model/ForcingFeatures.forcing_to_include = ('sea_surface_temperature',)
|
| 415 |
+
sea_model/ForcingFeatures.name = None
|
| 416 |
+
|
| 417 |
+
# Parameters for gelu:
|
| 418 |
+
# ==============================================================================
|
| 419 |
+
gelu.approximate = True
|
| 420 |
+
|
| 421 |
+
# Parameters for GET_ATMOSPHERIC_SCALE:
|
| 422 |
+
# ==============================================================================
|
| 423 |
+
# None.
|
| 424 |
+
|
| 425 |
+
# Parameters for get_model_specs:
|
| 426 |
+
# ==============================================================================
|
| 427 |
+
get_model_specs.custom_coords = @CoordinateSystem()
|
| 428 |
+
get_model_specs.model_time_step = '1 hour'
|
| 429 |
+
get_model_specs.reference_datetime_str = None
|
| 430 |
+
get_model_specs.reference_temperature = \
|
| 431 |
+
[215.58614815,
|
| 432 |
+
211.47405876,
|
| 433 |
+
205.87815406,
|
| 434 |
+
206.40755302,
|
| 435 |
+
210.43452345,
|
| 436 |
+
214.5683887,
|
| 437 |
+
218.75303863,
|
| 438 |
+
223.23145107,
|
| 439 |
+
227.9710687,
|
| 440 |
+
232.85381503,
|
| 441 |
+
237.53588735,
|
| 442 |
+
242.05068293,
|
| 443 |
+
246.29986585,
|
| 444 |
+
250.14294113,
|
| 445 |
+
253.74839535,
|
| 446 |
+
256.98024283,
|
| 447 |
+
259.94441031,
|
| 448 |
+
262.7041158,
|
| 449 |
+
265.21752838,
|
| 450 |
+
267.62333985,
|
| 451 |
+
269.94462121,
|
| 452 |
+
272.10056439,
|
| 453 |
+
274.12518288,
|
| 454 |
+
275.99833711,
|
| 455 |
+
277.72759392,
|
| 456 |
+
279.3292128,
|
| 457 |
+
280.79178708,
|
| 458 |
+
282.13507065,
|
| 459 |
+
283.41832023,
|
| 460 |
+
284.7682506,
|
| 461 |
+
286.33945487,
|
| 462 |
+
288.06707666]
|
| 463 |
+
|
| 464 |
+
# Parameters for get_physics_specs:
|
| 465 |
+
# ==============================================================================
|
| 466 |
+
get_physics_specs.construct_fn = @primitive_eq_specs_constructor
|
| 467 |
+
|
| 468 |
+
# Parameters for GridTL255:
|
| 469 |
+
# ==============================================================================
|
| 470 |
+
GridTL255.spherical_harmonics_impl = @RealSphericalHarmonicsWithZeroImag
|
| 471 |
+
|
| 472 |
+
# Parameters for DYCORE_GRID/GridWithWavenumbers:
|
| 473 |
+
# ==============================================================================
|
| 474 |
+
DYCORE_GRID/GridWithWavenumbers.dealiasing = 'quadratic'
|
| 475 |
+
DYCORE_GRID/GridWithWavenumbers.latitude_spacing = 'gauss'
|
| 476 |
+
DYCORE_GRID/GridWithWavenumbers.longitude_offset = 0.0
|
| 477 |
+
DYCORE_GRID/GridWithWavenumbers.longitude_wavenumbers = 254
|
| 478 |
+
DYCORE_GRID/GridWithWavenumbers.radius = None
|
| 479 |
+
DYCORE_GRID/GridWithWavenumbers.spherical_harmonics_impl = \
|
| 480 |
+
@RealSphericalHarmonicsWithZeroImag
|
| 481 |
+
|
| 482 |
+
# Parameters for advance/IdentityTransform:
|
| 483 |
+
# ==============================================================================
|
| 484 |
+
advance/IdentityTransform.name = None
|
| 485 |
+
|
| 486 |
+
# Parameters for sea_model/IdentityTransform:
|
| 487 |
+
# ==============================================================================
|
| 488 |
+
sea_model/IdentityTransform.name = None
|
| 489 |
+
|
| 490 |
+
# Parameters for imex_rk_sil3:
|
| 491 |
+
# ==============================================================================
|
| 492 |
+
# None.
|
| 493 |
+
|
| 494 |
+
# Parameters for InputClipTransform:
|
| 495 |
+
# ==============================================================================
|
| 496 |
+
InputClipTransform.name = None
|
| 497 |
+
InputClipTransform.wavenumbers_to_clip = %N_TO_CLIP
|
| 498 |
+
|
| 499 |
+
# Parameters for advance/InverseLevelScale:
|
| 500 |
+
# ==============================================================================
|
| 501 |
+
advance/InverseLevelScale.keys_to_scale = \
|
| 502 |
+
['specific_humidity',
|
| 503 |
+
'specific_humidity_del2',
|
| 504 |
+
'specific_humidity_dlat',
|
| 505 |
+
'specific_humidity_dlon']
|
| 506 |
+
advance/InverseLevelScale.name = None
|
| 507 |
+
advance/InverseLevelScale.scales = \
|
| 508 |
+
[8.822e-05,
|
| 509 |
+
7.126e-05,
|
| 510 |
+
0.0001047,
|
| 511 |
+
0.0001858,
|
| 512 |
+
0.0007601,
|
| 513 |
+
0.002642,
|
| 514 |
+
0.007242,
|
| 515 |
+
0.01568,
|
| 516 |
+
0.02907,
|
| 517 |
+
0.04654,
|
| 518 |
+
0.07084,
|
| 519 |
+
0.09971,
|
| 520 |
+
0.1355,
|
| 521 |
+
0.1762,
|
| 522 |
+
0.2243,
|
| 523 |
+
0.2822,
|
| 524 |
+
0.3459,
|
| 525 |
+
0.4172,
|
| 526 |
+
0.471,
|
| 527 |
+
0.5286,
|
| 528 |
+
0.5932,
|
| 529 |
+
0.6682,
|
| 530 |
+
0.7546,
|
| 531 |
+
0.8532,
|
| 532 |
+
0.9553,
|
| 533 |
+
1.058,
|
| 534 |
+
1.164,
|
| 535 |
+
1.278,
|
| 536 |
+
1.401,
|
| 537 |
+
1.553,
|
| 538 |
+
1.709,
|
| 539 |
+
1.791]
|
| 540 |
+
|
| 541 |
+
# Parameters for decoder_model/InverseLevelScale:
|
| 542 |
+
# ==============================================================================
|
| 543 |
+
decoder_model/InverseLevelScale.keys_to_scale = \
|
| 544 |
+
['specific_humidity',
|
| 545 |
+
'specific_humidity_del2',
|
| 546 |
+
'specific_humidity_dlat',
|
| 547 |
+
'specific_humidity_dlon']
|
| 548 |
+
decoder_model/InverseLevelScale.name = None
|
| 549 |
+
decoder_model/InverseLevelScale.scales = \
|
| 550 |
+
[8.822e-05,
|
| 551 |
+
7.126e-05,
|
| 552 |
+
0.0001047,
|
| 553 |
+
0.0001858,
|
| 554 |
+
0.0007601,
|
| 555 |
+
0.002642,
|
| 556 |
+
0.007242,
|
| 557 |
+
0.01568,
|
| 558 |
+
0.02907,
|
| 559 |
+
0.04654,
|
| 560 |
+
0.07084,
|
| 561 |
+
0.09971,
|
| 562 |
+
0.1355,
|
| 563 |
+
0.1762,
|
| 564 |
+
0.2243,
|
| 565 |
+
0.2822,
|
| 566 |
+
0.3459,
|
| 567 |
+
0.4172,
|
| 568 |
+
0.471,
|
| 569 |
+
0.5286,
|
| 570 |
+
0.5932,
|
| 571 |
+
0.6682,
|
| 572 |
+
0.7546,
|
| 573 |
+
0.8532,
|
| 574 |
+
0.9553,
|
| 575 |
+
1.058,
|
| 576 |
+
1.164,
|
| 577 |
+
1.278,
|
| 578 |
+
1.401,
|
| 579 |
+
1.553,
|
| 580 |
+
1.709,
|
| 581 |
+
1.791]
|
| 582 |
+
|
| 583 |
+
# Parameters for encoder_data/InverseLevelScale:
|
| 584 |
+
# ==============================================================================
|
| 585 |
+
encoder_data/InverseLevelScale.keys_to_scale = \
|
| 586 |
+
['specific_humidity',
|
| 587 |
+
'specific_humidity_del2',
|
| 588 |
+
'specific_humidity_dlat',
|
| 589 |
+
'specific_humidity_dlon']
|
| 590 |
+
encoder_data/InverseLevelScale.name = None
|
| 591 |
+
encoder_data/InverseLevelScale.scales = \
|
| 592 |
+
[3.823e-05,
|
| 593 |
+
5.985e-05,
|
| 594 |
+
7.103e-05,
|
| 595 |
+
8.198e-05,
|
| 596 |
+
8.836e-05,
|
| 597 |
+
9.113e-05,
|
| 598 |
+
7.745e-05,
|
| 599 |
+
7.234e-05,
|
| 600 |
+
8.308e-05,
|
| 601 |
+
9.885e-05,
|
| 602 |
+
0.0001443,
|
| 603 |
+
0.0002999,
|
| 604 |
+
0.001019,
|
| 605 |
+
0.002773,
|
| 606 |
+
0.006194,
|
| 607 |
+
0.01195,
|
| 608 |
+
0.02037,
|
| 609 |
+
0.04634,
|
| 610 |
+
0.08648,
|
| 611 |
+
0.1406,
|
| 612 |
+
0.2104,
|
| 613 |
+
0.3017,
|
| 614 |
+
0.4097,
|
| 615 |
+
0.4949,
|
| 616 |
+
0.5891,
|
| 617 |
+
0.716,
|
| 618 |
+
0.8645,
|
| 619 |
+
0.9418,
|
| 620 |
+
1.019,
|
| 621 |
+
1.098,
|
| 622 |
+
1.178,
|
| 623 |
+
1.262,
|
| 624 |
+
1.35,
|
| 625 |
+
1.454,
|
| 626 |
+
1.581,
|
| 627 |
+
1.677,
|
| 628 |
+
1.713]
|
| 629 |
+
|
| 630 |
+
# Parameters for decoder/InverseShiftAndNormalize:
|
| 631 |
+
# ==============================================================================
|
| 632 |
+
decoder/InverseShiftAndNormalize.global_scale = %CORRECTOR_SCALE
|
| 633 |
+
decoder/InverseShiftAndNormalize.name = None
|
| 634 |
+
decoder/InverseShiftAndNormalize.scales = \
|
| 635 |
+
{'sim_time': 0.0,
|
| 636 |
+
't': 27.99,
|
| 637 |
+
'tracers': {'specific_cloud_ice_water_content': 8.255e-06,
|
| 638 |
+
'specific_cloud_liquid_water_content': 2.182e-05,
|
| 639 |
+
'specific_humidity': 0.003493},
|
| 640 |
+
'u': 0.01935,
|
| 641 |
+
'v': 0.01038,
|
| 642 |
+
'z': 0.1496}
|
| 643 |
+
decoder/InverseShiftAndNormalize.shifts = \
|
| 644 |
+
{'sim_time': 0.0,
|
| 645 |
+
't': 0.0,
|
| 646 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 647 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 648 |
+
'specific_humidity': 0.0},
|
| 649 |
+
'u': 0.0,
|
| 650 |
+
'v': 0.0,
|
| 651 |
+
'z': 0.0}
|
| 652 |
+
|
| 653 |
+
# Parameters for div_curl_tendency_outputs/InverseShiftAndNormalize:
|
| 654 |
+
# ==============================================================================
|
| 655 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.global_scale = %GLOBAL_OUT_SCALE
|
| 656 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.name = None
|
| 657 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.scales = \
|
| 658 |
+
{'log_surface_pressure': 0.05021,
|
| 659 |
+
'sim_time': 0.0,
|
| 660 |
+
'temperature_variation': 33.94,
|
| 661 |
+
'tracers': {'specific_cloud_ice_water_content': 4.85e-05,
|
| 662 |
+
'specific_cloud_liquid_water_content': 9.693e-05,
|
| 663 |
+
'specific_humidity': 0.006182},
|
| 664 |
+
'u': 0.05863,
|
| 665 |
+
'v': 0.0516}
|
| 666 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.shifts = \
|
| 667 |
+
{'log_surface_pressure': 0.0,
|
| 668 |
+
'sim_time': 0.0,
|
| 669 |
+
'temperature_variation': 0.0,
|
| 670 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 671 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 672 |
+
'specific_humidity': 0.0},
|
| 673 |
+
'u': 0.0,
|
| 674 |
+
'v': 0.0}
|
| 675 |
+
|
| 676 |
+
# Parameters for encoder/InverseShiftAndNormalize:
|
| 677 |
+
# ==============================================================================
|
| 678 |
+
encoder/InverseShiftAndNormalize.global_scale = %CORRECTOR_SCALE
|
| 679 |
+
encoder/InverseShiftAndNormalize.name = None
|
| 680 |
+
encoder/InverseShiftAndNormalize.scales = \
|
| 681 |
+
{'divergence': 0.1078,
|
| 682 |
+
'log_surface_pressure': 0.1134,
|
| 683 |
+
'sim_time': 0.0,
|
| 684 |
+
'temperature_variation': 15.04,
|
| 685 |
+
'tracers': {'specific_cloud_ice_water_content': 9.812e-06,
|
| 686 |
+
'specific_cloud_liquid_water_content': 2.039e-05,
|
| 687 |
+
'specific_humidity': 0.003305},
|
| 688 |
+
'vorticity': 0.2831}
|
| 689 |
+
encoder/InverseShiftAndNormalize.shifts = \
|
| 690 |
+
{'divergence': 0.0,
|
| 691 |
+
'log_surface_pressure': 0.0,
|
| 692 |
+
'sim_time': 0.0,
|
| 693 |
+
'temperature_variation': 0.0,
|
| 694 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 695 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 696 |
+
'specific_humidity': 0.0},
|
| 697 |
+
'vorticity': 0.0}
|
| 698 |
+
|
| 699 |
+
# Parameters for advance/LatitudeFeatures:
|
| 700 |
+
# ==============================================================================
|
| 701 |
+
advance/LatitudeFeatures.name = None
|
| 702 |
+
|
| 703 |
+
# Parameters for decoder_model/LatitudeFeatures:
|
| 704 |
+
# ==============================================================================
|
| 705 |
+
decoder_model/LatitudeFeatures.name = None
|
| 706 |
+
|
| 707 |
+
# Parameters for encoder_data/LatitudeFeatures:
|
| 708 |
+
# ==============================================================================
|
| 709 |
+
encoder_data/LatitudeFeatures.name = None
|
| 710 |
+
|
| 711 |
+
# Parameters for LearnedOrography:
|
| 712 |
+
# ==============================================================================
|
| 713 |
+
LearnedOrography.base_orography_module = @FilteredCustomOrography
|
| 714 |
+
LearnedOrography.correction_scale = 2e-06
|
| 715 |
+
LearnedOrography.name = None
|
| 716 |
+
|
| 717 |
+
# Parameters for advance/LearnedPositionalFeatures:
|
| 718 |
+
# ==============================================================================
|
| 719 |
+
advance/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 720 |
+
advance/LearnedPositionalFeatures.name = None
|
| 721 |
+
advance/LearnedPositionalFeatures.scale = 1.0
|
| 722 |
+
|
| 723 |
+
# Parameters for decoder_model/LearnedPositionalFeatures:
|
| 724 |
+
# ==============================================================================
|
| 725 |
+
decoder_model/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 726 |
+
decoder_model/LearnedPositionalFeatures.name = None
|
| 727 |
+
decoder_model/LearnedPositionalFeatures.scale = 1.0
|
| 728 |
+
|
| 729 |
+
# Parameters for encoder_data/LearnedPositionalFeatures:
|
| 730 |
+
# ==============================================================================
|
| 731 |
+
encoder_data/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 732 |
+
encoder_data/LearnedPositionalFeatures.name = None
|
| 733 |
+
encoder_data/LearnedPositionalFeatures.scale = 1.0
|
| 734 |
+
|
| 735 |
+
# Parameters for decoder/LevelScale:
|
| 736 |
+
# ==============================================================================
|
| 737 |
+
decoder/LevelScale.keys_to_scale = ['specific_humidity']
|
| 738 |
+
decoder/LevelScale.name = None
|
| 739 |
+
decoder/LevelScale.scales = \
|
| 740 |
+
[3.823e-05,
|
| 741 |
+
5.985e-05,
|
| 742 |
+
7.103e-05,
|
| 743 |
+
8.198e-05,
|
| 744 |
+
8.836e-05,
|
| 745 |
+
9.113e-05,
|
| 746 |
+
7.745e-05,
|
| 747 |
+
7.234e-05,
|
| 748 |
+
8.308e-05,
|
| 749 |
+
9.885e-05,
|
| 750 |
+
0.0001443,
|
| 751 |
+
0.0002999,
|
| 752 |
+
0.001019,
|
| 753 |
+
0.002773,
|
| 754 |
+
0.006194,
|
| 755 |
+
0.01195,
|
| 756 |
+
0.02037,
|
| 757 |
+
0.04634,
|
| 758 |
+
0.08648,
|
| 759 |
+
0.1406,
|
| 760 |
+
0.2104,
|
| 761 |
+
0.3017,
|
| 762 |
+
0.4097,
|
| 763 |
+
0.4949,
|
| 764 |
+
0.5891,
|
| 765 |
+
0.716,
|
| 766 |
+
0.8645,
|
| 767 |
+
0.9418,
|
| 768 |
+
1.019,
|
| 769 |
+
1.098,
|
| 770 |
+
1.178,
|
| 771 |
+
1.262,
|
| 772 |
+
1.35,
|
| 773 |
+
1.454,
|
| 774 |
+
1.581,
|
| 775 |
+
1.677,
|
| 776 |
+
1.713]
|
| 777 |
+
|
| 778 |
+
# Parameters for div_curl_tendency_outputs/LevelScale:
|
| 779 |
+
# ==============================================================================
|
| 780 |
+
div_curl_tendency_outputs/LevelScale.keys_to_scale = ['specific_humidity']
|
| 781 |
+
div_curl_tendency_outputs/LevelScale.name = None
|
| 782 |
+
div_curl_tendency_outputs/LevelScale.scales = \
|
| 783 |
+
[0.0001303,
|
| 784 |
+
0.0002029,
|
| 785 |
+
0.0002682,
|
| 786 |
+
0.0004815,
|
| 787 |
+
0.001437,
|
| 788 |
+
0.004719,
|
| 789 |
+
0.01328,
|
| 790 |
+
0.03024,
|
| 791 |
+
0.05782,
|
| 792 |
+
0.09671,
|
| 793 |
+
0.1485,
|
| 794 |
+
0.2107,
|
| 795 |
+
0.2892,
|
| 796 |
+
0.3723,
|
| 797 |
+
0.4705,
|
| 798 |
+
0.5785,
|
| 799 |
+
0.6885,
|
| 800 |
+
0.8155,
|
| 801 |
+
0.9028,
|
| 802 |
+
0.9944,
|
| 803 |
+
1.103,
|
| 804 |
+
1.197,
|
| 805 |
+
1.31,
|
| 806 |
+
1.431,
|
| 807 |
+
1.526,
|
| 808 |
+
1.58,
|
| 809 |
+
1.61,
|
| 810 |
+
1.624,
|
| 811 |
+
1.634,
|
| 812 |
+
1.649,
|
| 813 |
+
1.729,
|
| 814 |
+
1.824]
|
| 815 |
+
|
| 816 |
+
# Parameters for encode/LevelScale:
|
| 817 |
+
# ==============================================================================
|
| 818 |
+
encode/LevelScale.keys_to_scale = ['specific_humidity']
|
| 819 |
+
encode/LevelScale.name = None
|
| 820 |
+
encode/LevelScale.scales = \
|
| 821 |
+
[8.822e-05,
|
| 822 |
+
7.126e-05,
|
| 823 |
+
0.0001047,
|
| 824 |
+
0.0001858,
|
| 825 |
+
0.0007601,
|
| 826 |
+
0.002642,
|
| 827 |
+
0.007242,
|
| 828 |
+
0.01568,
|
| 829 |
+
0.02907,
|
| 830 |
+
0.04654,
|
| 831 |
+
0.07084,
|
| 832 |
+
0.09971,
|
| 833 |
+
0.1355,
|
| 834 |
+
0.1762,
|
| 835 |
+
0.2243,
|
| 836 |
+
0.2822,
|
| 837 |
+
0.3459,
|
| 838 |
+
0.4172,
|
| 839 |
+
0.471,
|
| 840 |
+
0.5286,
|
| 841 |
+
0.5932,
|
| 842 |
+
0.6682,
|
| 843 |
+
0.7546,
|
| 844 |
+
0.8532,
|
| 845 |
+
0.9553,
|
| 846 |
+
1.058,
|
| 847 |
+
1.164,
|
| 848 |
+
1.278,
|
| 849 |
+
1.401,
|
| 850 |
+
1.553,
|
| 851 |
+
1.709,
|
| 852 |
+
1.791]
|
| 853 |
+
|
| 854 |
+
# Parameters for advance/MemoryVelocityAndValues:
|
| 855 |
+
# ==============================================================================
|
| 856 |
+
advance/MemoryVelocityAndValues.fields_to_include = None
|
| 857 |
+
advance/MemoryVelocityAndValues.name = None
|
| 858 |
+
|
| 859 |
+
# Parameters for decode/MlpUniform:
|
| 860 |
+
# ==============================================================================
|
| 861 |
+
decode/MlpUniform.activate_final = False
|
| 862 |
+
decode/MlpUniform.activation = %ACTIVATION
|
| 863 |
+
decode/MlpUniform.b_init = None
|
| 864 |
+
decode/MlpUniform.b_init_final = None
|
| 865 |
+
decode/MlpUniform.name = None
|
| 866 |
+
decode/MlpUniform.num_hidden_layers = 0
|
| 867 |
+
decode/MlpUniform.num_hidden_units = %LAYER_SIZE
|
| 868 |
+
decode/MlpUniform.w_init = None
|
| 869 |
+
decode/MlpUniform.w_init_final = None
|
| 870 |
+
decode/MlpUniform.with_bias = False
|
| 871 |
+
|
| 872 |
+
# Parameters for encode/MlpUniform:
|
| 873 |
+
# ==============================================================================
|
| 874 |
+
encode/MlpUniform.activate_final = False
|
| 875 |
+
encode/MlpUniform.activation = %ACTIVATION
|
| 876 |
+
encode/MlpUniform.b_init = None
|
| 877 |
+
encode/MlpUniform.b_init_final = None
|
| 878 |
+
encode/MlpUniform.name = None
|
| 879 |
+
encode/MlpUniform.num_hidden_layers = 0
|
| 880 |
+
encode/MlpUniform.num_hidden_units = 0
|
| 881 |
+
encode/MlpUniform.w_init = None
|
| 882 |
+
encode/MlpUniform.w_init_final = None
|
| 883 |
+
encode/MlpUniform.with_bias = True
|
| 884 |
+
|
| 885 |
+
# Parameters for process/MlpUniform:
|
| 886 |
+
# ==============================================================================
|
| 887 |
+
process/MlpUniform.activate_final = False
|
| 888 |
+
process/MlpUniform.activation = %ACTIVATION
|
| 889 |
+
process/MlpUniform.b_init = None
|
| 890 |
+
process/MlpUniform.b_init_final = None
|
| 891 |
+
process/MlpUniform.name = None
|
| 892 |
+
process/MlpUniform.num_hidden_layers = 3
|
| 893 |
+
process/MlpUniform.num_hidden_units = %LAYER_SIZE
|
| 894 |
+
process/MlpUniform.w_init = None
|
| 895 |
+
process/MlpUniform.w_init_final = None
|
| 896 |
+
process/MlpUniform.with_bias = True
|
| 897 |
+
|
| 898 |
+
# Parameters for surface_model_decode/MlpUniform:
|
| 899 |
+
# ==============================================================================
|
| 900 |
+
surface_model_decode/MlpUniform.activate_final = False
|
| 901 |
+
surface_model_decode/MlpUniform.activation = %ACTIVATION
|
| 902 |
+
surface_model_decode/MlpUniform.b_init = None
|
| 903 |
+
surface_model_decode/MlpUniform.b_init_final = None
|
| 904 |
+
surface_model_decode/MlpUniform.name = None
|
| 905 |
+
surface_model_decode/MlpUniform.num_hidden_layers = 1
|
| 906 |
+
surface_model_decode/MlpUniform.num_hidden_units = %SURFACE_MODEL_LAYER_SIZE
|
| 907 |
+
surface_model_decode/MlpUniform.w_init = None
|
| 908 |
+
surface_model_decode/MlpUniform.w_init_final = None
|
| 909 |
+
surface_model_decode/MlpUniform.with_bias = False
|
| 910 |
+
|
| 911 |
+
# Parameters for surface_model_encode/MlpUniform:
|
| 912 |
+
# ==============================================================================
|
| 913 |
+
surface_model_encode/MlpUniform.activate_final = False
|
| 914 |
+
surface_model_encode/MlpUniform.activation = %ACTIVATION
|
| 915 |
+
surface_model_encode/MlpUniform.b_init = None
|
| 916 |
+
surface_model_encode/MlpUniform.b_init_final = None
|
| 917 |
+
surface_model_encode/MlpUniform.name = None
|
| 918 |
+
surface_model_encode/MlpUniform.num_hidden_layers = 0
|
| 919 |
+
surface_model_encode/MlpUniform.num_hidden_units = 0
|
| 920 |
+
surface_model_encode/MlpUniform.w_init = None
|
| 921 |
+
surface_model_encode/MlpUniform.w_init_final = None
|
| 922 |
+
surface_model_encode/MlpUniform.with_bias = True
|
| 923 |
+
|
| 924 |
+
# Parameters for surface_model_process/MlpUniform:
|
| 925 |
+
# ==============================================================================
|
| 926 |
+
surface_model_process/MlpUniform.activate_final = False
|
| 927 |
+
surface_model_process/MlpUniform.activation = %ACTIVATION
|
| 928 |
+
surface_model_process/MlpUniform.b_init = None
|
| 929 |
+
surface_model_process/MlpUniform.b_init_final = None
|
| 930 |
+
surface_model_process/MlpUniform.name = None
|
| 931 |
+
surface_model_process/MlpUniform.num_hidden_layers = 3
|
| 932 |
+
surface_model_process/MlpUniform.num_hidden_units = %SURFACE_MODEL_LAYER_SIZE
|
| 933 |
+
surface_model_process/MlpUniform.w_init = None
|
| 934 |
+
surface_model_process/MlpUniform.w_init_final = None
|
| 935 |
+
surface_model_process/MlpUniform.with_bias = True
|
| 936 |
+
|
| 937 |
+
# Parameters for advance/ModalToNodalEmbedding:
|
| 938 |
+
# ==============================================================================
|
| 939 |
+
advance/ModalToNodalEmbedding.modal_to_nodal_features_module = \
|
| 940 |
+
@embedding_model/CombinedFeatures
|
| 941 |
+
advance/ModalToNodalEmbedding.name = None
|
| 942 |
+
advance/ModalToNodalEmbedding.nodal_mapping_module = @NodalVolumeMapping
|
| 943 |
+
advance/ModalToNodalEmbedding.output_transform_module = @IdentityTransform
|
| 944 |
+
|
| 945 |
+
# Parameters for sea_model/ModalToNodalEmbedding:
|
| 946 |
+
# ==============================================================================
|
| 947 |
+
sea_model/ModalToNodalEmbedding.modal_to_nodal_features_module = \
|
| 948 |
+
@sea_model/CombinedFeatures
|
| 949 |
+
sea_model/ModalToNodalEmbedding.name = None
|
| 950 |
+
sea_model/ModalToNodalEmbedding.nodal_mapping_module = @sea_model/NodalMapping
|
| 951 |
+
sea_model/ModalToNodalEmbedding.output_transform_module = @IdentityTransform
|
| 952 |
+
|
| 953 |
+
# Parameters for MoistPrimitiveEquationsWithCloudMoisture:
|
| 954 |
+
# ==============================================================================
|
| 955 |
+
MoistPrimitiveEquationsWithCloudMoisture.include_vertical_advection = True
|
| 956 |
+
MoistPrimitiveEquationsWithCloudMoisture.name = None
|
| 957 |
+
MoistPrimitiveEquationsWithCloudMoisture.orography_module = @LearnedOrography
|
| 958 |
+
|
| 959 |
+
# Parameters for advance/NodalLandSeaIceEmbedding:
|
| 960 |
+
# ==============================================================================
|
| 961 |
+
advance/NodalLandSeaIceEmbedding.land_embedding = None
|
| 962 |
+
advance/NodalLandSeaIceEmbedding.name = None
|
| 963 |
+
advance/NodalLandSeaIceEmbedding.sea_embedding = @sea_model/ModalToNodalEmbedding
|
| 964 |
+
advance/NodalLandSeaIceEmbedding.sea_ice_embedding = None
|
| 965 |
+
advance/NodalLandSeaIceEmbedding.static_vars_ds_path = None
|
| 966 |
+
|
| 967 |
+
# Parameters for NodalMapping:
|
| 968 |
+
# ==============================================================================
|
| 969 |
+
NodalMapping.name = None
|
| 970 |
+
NodalMapping.tower_factory = @EpdTower
|
| 971 |
+
|
| 972 |
+
# Parameters for sea_model/NodalMapping:
|
| 973 |
+
# ==============================================================================
|
| 974 |
+
sea_model/NodalMapping.name = None
|
| 975 |
+
sea_model/NodalMapping.tower_factory = @surface_model/EpdTower
|
| 976 |
+
|
| 977 |
+
# Parameters for NodalVolumeMapping:
|
| 978 |
+
# ==============================================================================
|
| 979 |
+
NodalVolumeMapping.name = None
|
| 980 |
+
NodalVolumeMapping.tower_factory = @VerticalConvTower
|
| 981 |
+
|
| 982 |
+
# Parameters for NullFeatures:
|
| 983 |
+
# ==============================================================================
|
| 984 |
+
NullFeatures.name = None
|
| 985 |
+
|
| 986 |
+
# Parameters for PerVariableDataFilter:
|
| 987 |
+
# ==============================================================================
|
| 988 |
+
PerVariableDataFilter.name = None
|
| 989 |
+
PerVariableDataFilter.per_variable_filters = \
|
| 990 |
+
{'divergence': @divergence/DataExponentialFilter,
|
| 991 |
+
'log_surface_pressure': @lsp/DataExponentialFilter,
|
| 992 |
+
'sim_time': @DataNoFilter,
|
| 993 |
+
'temperature_variation': @temperature_variation/DataExponentialFilter,
|
| 994 |
+
'tracers': {'specific_cloud_ice_water_content': @q/DataExponentialFilter,
|
| 995 |
+
'specific_cloud_liquid_water_content': @q/DataExponentialFilter,
|
| 996 |
+
'specific_humidity': @q/DataExponentialFilter},
|
| 997 |
+
'vorticity': @vorticity/DataExponentialFilter}
|
| 998 |
+
|
| 999 |
+
# Parameters for advance/PressureFeatures:
|
| 1000 |
+
# ==============================================================================
|
| 1001 |
+
advance/PressureFeatures.name = None
|
| 1002 |
+
|
| 1003 |
+
# Parameters for embedding_model/PressureFeatures:
|
| 1004 |
+
# ==============================================================================
|
| 1005 |
+
embedding_model/PressureFeatures.name = None
|
| 1006 |
+
|
| 1007 |
+
# Parameters for primitive_eq_specs_constructor:
|
| 1008 |
+
# ==============================================================================
|
| 1009 |
+
primitive_eq_specs_constructor.scale = @GET_ATMOSPHERIC_SCALE()
|
| 1010 |
+
|
| 1011 |
+
# Parameters for primitive_eq_to_xarray:
|
| 1012 |
+
# ==============================================================================
|
| 1013 |
+
# None.
|
| 1014 |
+
|
| 1015 |
+
# Parameters for PrimitiveToWeatherbenchDecoder:
|
| 1016 |
+
# ==============================================================================
|
| 1017 |
+
# None.
|
| 1018 |
+
|
| 1019 |
+
# Parameters for advance/RadiationFeatures:
|
| 1020 |
+
# ==============================================================================
|
| 1021 |
+
advance/RadiationFeatures.name = None
|
| 1022 |
+
|
| 1023 |
+
# Parameters for decoder_model/RadiationFeatures:
|
| 1024 |
+
# ==============================================================================
|
| 1025 |
+
decoder_model/RadiationFeatures.name = None
|
| 1026 |
+
|
| 1027 |
+
# Parameters for encoder_data/RadiationFeatures:
|
| 1028 |
+
# ==============================================================================
|
| 1029 |
+
encoder_data/RadiationFeatures.name = None
|
| 1030 |
+
|
| 1031 |
+
# Parameters for RealSphericalHarmonicsWithZeroImag:
|
| 1032 |
+
# ==============================================================================
|
| 1033 |
+
RealSphericalHarmonicsWithZeroImag.base_shape_multiple = %BASE_SHAPE_MULTIPLE
|
| 1034 |
+
RealSphericalHarmonicsWithZeroImag.reverse_einsum_arg_order = \
|
| 1035 |
+
%REVERSE_EINSUM_ARG_ORDER
|
| 1036 |
+
RealSphericalHarmonicsWithZeroImag.stacked_fourier_transforms = None
|
| 1037 |
+
RealSphericalHarmonicsWithZeroImag.transform_precision = 'tensorfloat32'
|
| 1038 |
+
|
| 1039 |
+
# Parameters for orography/RealSphericalHarmonicsWithZeroImag:
|
| 1040 |
+
# ==============================================================================
|
| 1041 |
+
orography/RealSphericalHarmonicsWithZeroImag.base_shape_multiple = \
|
| 1042 |
+
%BASE_SHAPE_MULTIPLE
|
| 1043 |
+
orography/RealSphericalHarmonicsWithZeroImag.reverse_einsum_arg_order = \
|
| 1044 |
+
%REVERSE_EINSUM_ARG_ORDER
|
| 1045 |
+
orography/RealSphericalHarmonicsWithZeroImag.stacked_fourier_transforms = None
|
| 1046 |
+
orography/RealSphericalHarmonicsWithZeroImag.transform_precision = 'tensorfloat32'
|
| 1047 |
+
|
| 1048 |
+
# Parameters for dycore/SequentialStepFilter:
|
| 1049 |
+
# ==============================================================================
|
| 1050 |
+
dycore/SequentialStepFilter.filter_modules = \
|
| 1051 |
+
(@dycore/ExponentialFilter, @stability/ExponentialFilter)
|
| 1052 |
+
dycore/SequentialStepFilter.name = None
|
| 1053 |
+
|
| 1054 |
+
# Parameters for ml/SequentialStepFilter:
|
| 1055 |
+
# ==============================================================================
|
| 1056 |
+
ml/SequentialStepFilter.filter_modules = (@stability/ExponentialFilter,)
|
| 1057 |
+
ml/SequentialStepFilter.name = None
|
| 1058 |
+
|
| 1059 |
+
# Parameters for advance/SequentialTransform:
|
| 1060 |
+
# ==============================================================================
|
| 1061 |
+
advance/SequentialTransform.name = None
|
| 1062 |
+
advance/SequentialTransform.transform_modules = \
|
| 1063 |
+
(@advance/ShiftAndNormalize,
|
| 1064 |
+
@advance/InverseLevelScale,
|
| 1065 |
+
@advance/TruncateSigmaLevels,
|
| 1066 |
+
@SoftClip)
|
| 1067 |
+
|
| 1068 |
+
# Parameters for decoder/SequentialTransform:
|
| 1069 |
+
# ==============================================================================
|
| 1070 |
+
decoder/SequentialTransform.name = None
|
| 1071 |
+
decoder/SequentialTransform.transform_modules = \
|
| 1072 |
+
(@decoder/InverseShiftAndNormalize, @decoder/LevelScale)
|
| 1073 |
+
|
| 1074 |
+
# Parameters for decoder_model/SequentialTransform:
|
| 1075 |
+
# ==============================================================================
|
| 1076 |
+
decoder_model/SequentialTransform.name = None
|
| 1077 |
+
decoder_model/SequentialTransform.transform_modules = \
|
| 1078 |
+
(@decoder_model/ShiftAndNormalize,
|
| 1079 |
+
@decoder_model/InverseLevelScale,
|
| 1080 |
+
@decoder_model/TruncateSigmaLevels)
|
| 1081 |
+
|
| 1082 |
+
# Parameters for div_curl_tendency_outputs/SequentialTransform:
|
| 1083 |
+
# ==============================================================================
|
| 1084 |
+
div_curl_tendency_outputs/SequentialTransform.name = None
|
| 1085 |
+
div_curl_tendency_outputs/SequentialTransform.transform_modules = \
|
| 1086 |
+
(@div_curl_tendency_outputs/InverseShiftAndNormalize,
|
| 1087 |
+
@div_curl_tendency_outputs/LevelScale)
|
| 1088 |
+
|
| 1089 |
+
# Parameters for encode/SequentialTransform:
|
| 1090 |
+
# ==============================================================================
|
| 1091 |
+
encode/SequentialTransform.name = None
|
| 1092 |
+
encode/SequentialTransform.transform_modules = \
|
| 1093 |
+
(@encoder/InverseShiftAndNormalize, @encode/LevelScale)
|
| 1094 |
+
|
| 1095 |
+
# Parameters for encoder_data/SequentialTransform:
|
| 1096 |
+
# ==============================================================================
|
| 1097 |
+
encoder_data/SequentialTransform.name = None
|
| 1098 |
+
encoder_data/SequentialTransform.transform_modules = \
|
| 1099 |
+
(@encoder_data/ShiftAndNormalize, @encoder_data/InverseLevelScale)
|
| 1100 |
+
|
| 1101 |
+
# Parameters for advance/ShiftAndNormalize:
|
| 1102 |
+
# ==============================================================================
|
| 1103 |
+
advance/ShiftAndNormalize.features_to_exclude = ()
|
| 1104 |
+
advance/ShiftAndNormalize.global_scale = None
|
| 1105 |
+
advance/ShiftAndNormalize.name = None
|
| 1106 |
+
advance/ShiftAndNormalize.scales = \
|
| 1107 |
+
{'CNN1D_0': 0.1,
|
| 1108 |
+
'CNN1D_1': 0.1,
|
| 1109 |
+
'CNN1D_10': 0.1,
|
| 1110 |
+
'CNN1D_11': 0.1,
|
| 1111 |
+
'CNN1D_12': 0.1,
|
| 1112 |
+
'CNN1D_13': 0.1,
|
| 1113 |
+
'CNN1D_14': 0.1,
|
| 1114 |
+
'CNN1D_15': 0.1,
|
| 1115 |
+
'CNN1D_16': 0.1,
|
| 1116 |
+
'CNN1D_17': 0.1,
|
| 1117 |
+
'CNN1D_18': 0.1,
|
| 1118 |
+
'CNN1D_19': 0.1,
|
| 1119 |
+
'CNN1D_2': 0.1,
|
| 1120 |
+
'CNN1D_20': 0.1,
|
| 1121 |
+
'CNN1D_21': 0.1,
|
| 1122 |
+
'CNN1D_22': 0.1,
|
| 1123 |
+
'CNN1D_23': 0.1,
|
| 1124 |
+
'CNN1D_24': 0.1,
|
| 1125 |
+
'CNN1D_25': 0.1,
|
| 1126 |
+
'CNN1D_26': 0.1,
|
| 1127 |
+
'CNN1D_27': 0.1,
|
| 1128 |
+
'CNN1D_28': 0.1,
|
| 1129 |
+
'CNN1D_29': 0.1,
|
| 1130 |
+
'CNN1D_3': 0.1,
|
| 1131 |
+
'CNN1D_30': 0.1,
|
| 1132 |
+
'CNN1D_31': 0.1,
|
| 1133 |
+
'CNN1D_32': 0.1,
|
| 1134 |
+
'CNN1D_33': 0.1,
|
| 1135 |
+
'CNN1D_34': 0.1,
|
| 1136 |
+
'CNN1D_35': 0.1,
|
| 1137 |
+
'CNN1D_36': 0.1,
|
| 1138 |
+
'CNN1D_37': 0.1,
|
| 1139 |
+
'CNN1D_38': 0.1,
|
| 1140 |
+
'CNN1D_39': 0.1,
|
| 1141 |
+
'CNN1D_4': 0.1,
|
| 1142 |
+
'CNN1D_40': 0.1,
|
| 1143 |
+
'CNN1D_41': 0.1,
|
| 1144 |
+
'CNN1D_42': 0.1,
|
| 1145 |
+
'CNN1D_43': 0.1,
|
| 1146 |
+
'CNN1D_44': 0.1,
|
| 1147 |
+
'CNN1D_45': 0.1,
|
| 1148 |
+
'CNN1D_46': 0.1,
|
| 1149 |
+
'CNN1D_47': 0.1,
|
| 1150 |
+
'CNN1D_48': 0.1,
|
| 1151 |
+
'CNN1D_49': 0.1,
|
| 1152 |
+
'CNN1D_5': 0.1,
|
| 1153 |
+
'CNN1D_50': 0.1,
|
| 1154 |
+
'CNN1D_51': 0.1,
|
| 1155 |
+
'CNN1D_52': 0.1,
|
| 1156 |
+
'CNN1D_53': 0.1,
|
| 1157 |
+
'CNN1D_54': 0.1,
|
| 1158 |
+
'CNN1D_55': 0.1,
|
| 1159 |
+
'CNN1D_56': 0.1,
|
| 1160 |
+
'CNN1D_57': 0.1,
|
| 1161 |
+
'CNN1D_58': 0.1,
|
| 1162 |
+
'CNN1D_59': 0.1,
|
| 1163 |
+
'CNN1D_6': 0.1,
|
| 1164 |
+
'CNN1D_60': 0.1,
|
| 1165 |
+
'CNN1D_61': 0.1,
|
| 1166 |
+
'CNN1D_62': 0.1,
|
| 1167 |
+
'CNN1D_63': 0.1,
|
| 1168 |
+
'CNN1D_7': 0.1,
|
| 1169 |
+
'CNN1D_8': 0.1,
|
| 1170 |
+
'CNN1D_9': 0.1,
|
| 1171 |
+
'cos_latitude': 0.3068,
|
| 1172 |
+
'divergence': 0.1078,
|
| 1173 |
+
'divergence_del2': 1825.0,
|
| 1174 |
+
'divergence_dlat': 8.5,
|
| 1175 |
+
'divergence_dlon': 8.842,
|
| 1176 |
+
'geopotential_at_surface': 0.009579,
|
| 1177 |
+
'geopotential_at_surface_del2': 28.66,
|
| 1178 |
+
'geopotential_at_surface_dlat': 0.1882,
|
| 1179 |
+
'geopotential_at_surface_dlon': 0.1564,
|
| 1180 |
+
'land_sea_mask': 0.456,
|
| 1181 |
+
'learned_positional_features': 1.0,
|
| 1182 |
+
'log_surface_pressure': 0.1134,
|
| 1183 |
+
'log_surface_pressure_del2': 264.6,
|
| 1184 |
+
'log_surface_pressure_dlat': 1.656,
|
| 1185 |
+
'log_surface_pressure_dlon': 1.645,
|
| 1186 |
+
'memory_divergence': 0.1078,
|
| 1187 |
+
'memory_log_surface_pressure': 0.1134,
|
| 1188 |
+
'memory_specific_cloud_ice_water_content': 9.812e-06,
|
| 1189 |
+
'memory_specific_cloud_liquid_water_content': 2.039e-05,
|
| 1190 |
+
'memory_specific_humidity': 0.003305,
|
| 1191 |
+
'memory_temperature_variation': 15.04,
|
| 1192 |
+
'memory_u': 0.01489,
|
| 1193 |
+
'memory_v': 0.01022,
|
| 1194 |
+
'memory_vorticity': 0.2831,
|
| 1195 |
+
'pressure': 1.644,
|
| 1196 |
+
'radiation': 0.2862,
|
| 1197 |
+
'sea_surface_temperature': 11.93,
|
| 1198 |
+
'sin_latitude': 0.7064,
|
| 1199 |
+
'specific_cloud_ice_water_content': 9.812e-06,
|
| 1200 |
+
'specific_cloud_ice_water_content_del2': 0.08441,
|
| 1201 |
+
'specific_cloud_ice_water_content_dlat': 0.0004684,
|
| 1202 |
+
'specific_cloud_ice_water_content_dlon': 0.0004646,
|
| 1203 |
+
'specific_cloud_liquid_water_content': 2.039e-05,
|
| 1204 |
+
'specific_cloud_liquid_water_content_del2': 0.1802,
|
| 1205 |
+
'specific_cloud_liquid_water_content_dlat': 0.0009877,
|
| 1206 |
+
'specific_cloud_liquid_water_content_dlon': 0.0009565,
|
| 1207 |
+
'specific_humidity': 0.003305,
|
| 1208 |
+
'specific_humidity_del2': 4.442,
|
| 1209 |
+
'specific_humidity_dlat': 0.0319,
|
| 1210 |
+
'specific_humidity_dlon': 0.02678,
|
| 1211 |
+
'surface_embedding': 1.0,
|
| 1212 |
+
'temperature_variation': 15.04,
|
| 1213 |
+
'temperature_variation_del2': 10460.0,
|
| 1214 |
+
'temperature_variation_dlat': 82.59,
|
| 1215 |
+
'temperature_variation_dlon': 77.07,
|
| 1216 |
+
'u': 0.01489,
|
| 1217 |
+
'u_del2': 18.46,
|
| 1218 |
+
'u_dlat': 0.2197,
|
| 1219 |
+
'u_dlon': 0.1677,
|
| 1220 |
+
'v': 0.01022,
|
| 1221 |
+
'v_del2': 17.2,
|
| 1222 |
+
'v_dlat': 0.169,
|
| 1223 |
+
'v_dlon': 0.2083,
|
| 1224 |
+
'vorticity': 0.2831,
|
| 1225 |
+
'vorticity_del2': 2831.0,
|
| 1226 |
+
'vorticity_dlat': 14.96,
|
| 1227 |
+
'vorticity_dlon': 14.91}
|
| 1228 |
+
advance/ShiftAndNormalize.shifts = \
|
| 1229 |
+
{'CNN1D_0': 0.0,
|
| 1230 |
+
'CNN1D_1': 0.0,
|
| 1231 |
+
'CNN1D_10': 0.0,
|
| 1232 |
+
'CNN1D_11': 0.0,
|
| 1233 |
+
'CNN1D_12': 0.0,
|
| 1234 |
+
'CNN1D_13': 0.0,
|
| 1235 |
+
'CNN1D_14': 0.0,
|
| 1236 |
+
'CNN1D_15': 0.0,
|
| 1237 |
+
'CNN1D_16': 0.0,
|
| 1238 |
+
'CNN1D_17': 0.0,
|
| 1239 |
+
'CNN1D_18': 0.0,
|
| 1240 |
+
'CNN1D_19': 0.0,
|
| 1241 |
+
'CNN1D_2': 0.0,
|
| 1242 |
+
'CNN1D_20': 0.0,
|
| 1243 |
+
'CNN1D_21': 0.0,
|
| 1244 |
+
'CNN1D_22': 0.0,
|
| 1245 |
+
'CNN1D_23': 0.0,
|
| 1246 |
+
'CNN1D_24': 0.0,
|
| 1247 |
+
'CNN1D_25': 0.0,
|
| 1248 |
+
'CNN1D_26': 0.0,
|
| 1249 |
+
'CNN1D_27': 0.0,
|
| 1250 |
+
'CNN1D_28': 0.0,
|
| 1251 |
+
'CNN1D_29': 0.0,
|
| 1252 |
+
'CNN1D_3': 0.0,
|
| 1253 |
+
'CNN1D_30': 0.0,
|
| 1254 |
+
'CNN1D_31': 0.0,
|
| 1255 |
+
'CNN1D_32': 0.0,
|
| 1256 |
+
'CNN1D_33': 0.0,
|
| 1257 |
+
'CNN1D_34': 0.0,
|
| 1258 |
+
'CNN1D_35': 0.0,
|
| 1259 |
+
'CNN1D_36': 0.0,
|
| 1260 |
+
'CNN1D_37': 0.0,
|
| 1261 |
+
'CNN1D_38': 0.0,
|
| 1262 |
+
'CNN1D_39': 0.0,
|
| 1263 |
+
'CNN1D_4': 0.0,
|
| 1264 |
+
'CNN1D_40': 0.0,
|
| 1265 |
+
'CNN1D_41': 0.0,
|
| 1266 |
+
'CNN1D_42': 0.0,
|
| 1267 |
+
'CNN1D_43': 0.0,
|
| 1268 |
+
'CNN1D_44': 0.0,
|
| 1269 |
+
'CNN1D_45': 0.0,
|
| 1270 |
+
'CNN1D_46': 0.0,
|
| 1271 |
+
'CNN1D_47': 0.0,
|
| 1272 |
+
'CNN1D_48': 0.0,
|
| 1273 |
+
'CNN1D_49': 0.0,
|
| 1274 |
+
'CNN1D_5': 0.0,
|
| 1275 |
+
'CNN1D_50': 0.0,
|
| 1276 |
+
'CNN1D_51': 0.0,
|
| 1277 |
+
'CNN1D_52': 0.0,
|
| 1278 |
+
'CNN1D_53': 0.0,
|
| 1279 |
+
'CNN1D_54': 0.0,
|
| 1280 |
+
'CNN1D_55': 0.0,
|
| 1281 |
+
'CNN1D_56': 0.0,
|
| 1282 |
+
'CNN1D_57': 0.0,
|
| 1283 |
+
'CNN1D_58': 0.0,
|
| 1284 |
+
'CNN1D_59': 0.0,
|
| 1285 |
+
'CNN1D_6': 0.0,
|
| 1286 |
+
'CNN1D_60': 0.0,
|
| 1287 |
+
'CNN1D_61': 0.0,
|
| 1288 |
+
'CNN1D_62': 0.0,
|
| 1289 |
+
'CNN1D_63': 0.0,
|
| 1290 |
+
'CNN1D_7': 0.0,
|
| 1291 |
+
'CNN1D_8': 0.0,
|
| 1292 |
+
'CNN1D_9': 0.0,
|
| 1293 |
+
'cos_latitude': 0.638,
|
| 1294 |
+
'divergence': -0.0,
|
| 1295 |
+
'divergence_del2': -0.015,
|
| 1296 |
+
'divergence_dlat': -0.001,
|
| 1297 |
+
'divergence_dlon': 0.0,
|
| 1298 |
+
'geopotential_at_surface': 0.004,
|
| 1299 |
+
'geopotential_at_surface_del2': -0.048,
|
| 1300 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1301 |
+
'geopotential_at_surface_dlon': -0.0,
|
| 1302 |
+
'land_sea_mask': 0.335,
|
| 1303 |
+
'learned_positional_features': 0.0,
|
| 1304 |
+
'log_surface_pressure': 1.715,
|
| 1305 |
+
'log_surface_pressure_del2': 0.59,
|
| 1306 |
+
'log_surface_pressure_dlat': 0.126,
|
| 1307 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1308 |
+
'memory_divergence': -0.0,
|
| 1309 |
+
'memory_log_surface_pressure': 1.715,
|
| 1310 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1311 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1312 |
+
'memory_specific_humidity': 0.0,
|
| 1313 |
+
'memory_temperature_variation': -4.98,
|
| 1314 |
+
'memory_u': 0.007,
|
| 1315 |
+
'memory_v': -0.0,
|
| 1316 |
+
'memory_vorticity': -0.002,
|
| 1317 |
+
'pressure': 2.796,
|
| 1318 |
+
'radiation': 0.213,
|
| 1319 |
+
'sea_surface_temperature': 285.14,
|
| 1320 |
+
'sin_latitude': -0.0,
|
| 1321 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1322 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1323 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1324 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1325 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1326 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1327 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1328 |
+
'specific_cloud_liquid_water_content_dlon': -0.0,
|
| 1329 |
+
'specific_humidity': 0.0,
|
| 1330 |
+
'specific_humidity_del2': 0.0,
|
| 1331 |
+
'specific_humidity_dlat': 0.0,
|
| 1332 |
+
'specific_humidity_dlon': 0.0,
|
| 1333 |
+
'surface_embedding': 0.0,
|
| 1334 |
+
'temperature_variation': -4.98,
|
| 1335 |
+
'temperature_variation_del2': 43.608,
|
| 1336 |
+
'temperature_variation_dlat': 4.843,
|
| 1337 |
+
'temperature_variation_dlon': 0.0,
|
| 1338 |
+
'u': 0.007,
|
| 1339 |
+
'u_del2': 0.242,
|
| 1340 |
+
'u_dlat': 0.002,
|
| 1341 |
+
'u_dlon': 0.0,
|
| 1342 |
+
'v': -0.0,
|
| 1343 |
+
'v_del2': -0.0,
|
| 1344 |
+
'v_dlat': -0.0,
|
| 1345 |
+
'v_dlon': 0.0,
|
| 1346 |
+
'vorticity': -0.002,
|
| 1347 |
+
'vorticity_del2': -0.365,
|
| 1348 |
+
'vorticity_dlat': 0.041,
|
| 1349 |
+
'vorticity_dlon': 0.0}
|
| 1350 |
+
|
| 1351 |
+
# Parameters for decoder_model/ShiftAndNormalize:
|
| 1352 |
+
# ==============================================================================
|
| 1353 |
+
decoder_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1354 |
+
decoder_model/ShiftAndNormalize.global_scale = None
|
| 1355 |
+
decoder_model/ShiftAndNormalize.name = None
|
| 1356 |
+
decoder_model/ShiftAndNormalize.scales = \
|
| 1357 |
+
{'CNN1D_0': 0.1,
|
| 1358 |
+
'CNN1D_1': 0.1,
|
| 1359 |
+
'CNN1D_10': 0.1,
|
| 1360 |
+
'CNN1D_11': 0.1,
|
| 1361 |
+
'CNN1D_12': 0.1,
|
| 1362 |
+
'CNN1D_13': 0.1,
|
| 1363 |
+
'CNN1D_14': 0.1,
|
| 1364 |
+
'CNN1D_15': 0.1,
|
| 1365 |
+
'CNN1D_16': 0.1,
|
| 1366 |
+
'CNN1D_17': 0.1,
|
| 1367 |
+
'CNN1D_18': 0.1,
|
| 1368 |
+
'CNN1D_19': 0.1,
|
| 1369 |
+
'CNN1D_2': 0.1,
|
| 1370 |
+
'CNN1D_20': 0.1,
|
| 1371 |
+
'CNN1D_21': 0.1,
|
| 1372 |
+
'CNN1D_22': 0.1,
|
| 1373 |
+
'CNN1D_23': 0.1,
|
| 1374 |
+
'CNN1D_24': 0.1,
|
| 1375 |
+
'CNN1D_25': 0.1,
|
| 1376 |
+
'CNN1D_26': 0.1,
|
| 1377 |
+
'CNN1D_27': 0.1,
|
| 1378 |
+
'CNN1D_28': 0.1,
|
| 1379 |
+
'CNN1D_29': 0.1,
|
| 1380 |
+
'CNN1D_3': 0.1,
|
| 1381 |
+
'CNN1D_30': 0.1,
|
| 1382 |
+
'CNN1D_31': 0.1,
|
| 1383 |
+
'CNN1D_32': 0.1,
|
| 1384 |
+
'CNN1D_33': 0.1,
|
| 1385 |
+
'CNN1D_34': 0.1,
|
| 1386 |
+
'CNN1D_35': 0.1,
|
| 1387 |
+
'CNN1D_36': 0.1,
|
| 1388 |
+
'CNN1D_37': 0.1,
|
| 1389 |
+
'CNN1D_38': 0.1,
|
| 1390 |
+
'CNN1D_39': 0.1,
|
| 1391 |
+
'CNN1D_4': 0.1,
|
| 1392 |
+
'CNN1D_40': 0.1,
|
| 1393 |
+
'CNN1D_41': 0.1,
|
| 1394 |
+
'CNN1D_42': 0.1,
|
| 1395 |
+
'CNN1D_43': 0.1,
|
| 1396 |
+
'CNN1D_44': 0.1,
|
| 1397 |
+
'CNN1D_45': 0.1,
|
| 1398 |
+
'CNN1D_46': 0.1,
|
| 1399 |
+
'CNN1D_47': 0.1,
|
| 1400 |
+
'CNN1D_48': 0.1,
|
| 1401 |
+
'CNN1D_49': 0.1,
|
| 1402 |
+
'CNN1D_5': 0.1,
|
| 1403 |
+
'CNN1D_50': 0.1,
|
| 1404 |
+
'CNN1D_51': 0.1,
|
| 1405 |
+
'CNN1D_52': 0.1,
|
| 1406 |
+
'CNN1D_53': 0.1,
|
| 1407 |
+
'CNN1D_54': 0.1,
|
| 1408 |
+
'CNN1D_55': 0.1,
|
| 1409 |
+
'CNN1D_56': 0.1,
|
| 1410 |
+
'CNN1D_57': 0.1,
|
| 1411 |
+
'CNN1D_58': 0.1,
|
| 1412 |
+
'CNN1D_59': 0.1,
|
| 1413 |
+
'CNN1D_6': 0.1,
|
| 1414 |
+
'CNN1D_60': 0.1,
|
| 1415 |
+
'CNN1D_61': 0.1,
|
| 1416 |
+
'CNN1D_62': 0.1,
|
| 1417 |
+
'CNN1D_63': 0.1,
|
| 1418 |
+
'CNN1D_7': 0.1,
|
| 1419 |
+
'CNN1D_8': 0.1,
|
| 1420 |
+
'CNN1D_9': 0.1,
|
| 1421 |
+
'cos_latitude': 0.3068,
|
| 1422 |
+
'divergence': 0.1078,
|
| 1423 |
+
'divergence_del2': 1825.0,
|
| 1424 |
+
'divergence_dlat': 8.5,
|
| 1425 |
+
'divergence_dlon': 8.842,
|
| 1426 |
+
'geopotential_at_surface': 0.009579,
|
| 1427 |
+
'geopotential_at_surface_del2': 28.66,
|
| 1428 |
+
'geopotential_at_surface_dlat': 0.1882,
|
| 1429 |
+
'geopotential_at_surface_dlon': 0.1564,
|
| 1430 |
+
'land_sea_mask': 0.456,
|
| 1431 |
+
'learned_positional_features': 1.0,
|
| 1432 |
+
'log_surface_pressure': 0.1134,
|
| 1433 |
+
'log_surface_pressure_del2': 264.6,
|
| 1434 |
+
'log_surface_pressure_dlat': 1.656,
|
| 1435 |
+
'log_surface_pressure_dlon': 1.645,
|
| 1436 |
+
'memory_divergence': 0.1078,
|
| 1437 |
+
'memory_log_surface_pressure': 0.1134,
|
| 1438 |
+
'memory_specific_cloud_ice_water_content': 9.812e-06,
|
| 1439 |
+
'memory_specific_cloud_liquid_water_content': 2.039e-05,
|
| 1440 |
+
'memory_specific_humidity': 0.003305,
|
| 1441 |
+
'memory_temperature_variation': 15.04,
|
| 1442 |
+
'memory_u': 0.01489,
|
| 1443 |
+
'memory_v': 0.01022,
|
| 1444 |
+
'memory_vorticity': 0.2831,
|
| 1445 |
+
'pressure': 1.644,
|
| 1446 |
+
'radiation': 0.2862,
|
| 1447 |
+
'sea_surface_temperature': 11.93,
|
| 1448 |
+
'sin_latitude': 0.7064,
|
| 1449 |
+
'specific_cloud_ice_water_content': 9.812e-06,
|
| 1450 |
+
'specific_cloud_ice_water_content_del2': 0.08441,
|
| 1451 |
+
'specific_cloud_ice_water_content_dlat': 0.0004684,
|
| 1452 |
+
'specific_cloud_ice_water_content_dlon': 0.0004646,
|
| 1453 |
+
'specific_cloud_liquid_water_content': 2.039e-05,
|
| 1454 |
+
'specific_cloud_liquid_water_content_del2': 0.1802,
|
| 1455 |
+
'specific_cloud_liquid_water_content_dlat': 0.0009877,
|
| 1456 |
+
'specific_cloud_liquid_water_content_dlon': 0.0009565,
|
| 1457 |
+
'specific_humidity': 0.003305,
|
| 1458 |
+
'specific_humidity_del2': 4.442,
|
| 1459 |
+
'specific_humidity_dlat': 0.0319,
|
| 1460 |
+
'specific_humidity_dlon': 0.02678,
|
| 1461 |
+
'surface_embedding': 1.0,
|
| 1462 |
+
'temperature_variation': 15.04,
|
| 1463 |
+
'temperature_variation_del2': 10460.0,
|
| 1464 |
+
'temperature_variation_dlat': 82.59,
|
| 1465 |
+
'temperature_variation_dlon': 77.07,
|
| 1466 |
+
'u': 0.01489,
|
| 1467 |
+
'u_del2': 18.46,
|
| 1468 |
+
'u_dlat': 0.2197,
|
| 1469 |
+
'u_dlon': 0.1677,
|
| 1470 |
+
'v': 0.01022,
|
| 1471 |
+
'v_del2': 17.2,
|
| 1472 |
+
'v_dlat': 0.169,
|
| 1473 |
+
'v_dlon': 0.2083,
|
| 1474 |
+
'vorticity': 0.2831,
|
| 1475 |
+
'vorticity_del2': 2831.0,
|
| 1476 |
+
'vorticity_dlat': 14.96,
|
| 1477 |
+
'vorticity_dlon': 14.91}
|
| 1478 |
+
decoder_model/ShiftAndNormalize.shifts = \
|
| 1479 |
+
{'CNN1D_0': 0.0,
|
| 1480 |
+
'CNN1D_1': 0.0,
|
| 1481 |
+
'CNN1D_10': 0.0,
|
| 1482 |
+
'CNN1D_11': 0.0,
|
| 1483 |
+
'CNN1D_12': 0.0,
|
| 1484 |
+
'CNN1D_13': 0.0,
|
| 1485 |
+
'CNN1D_14': 0.0,
|
| 1486 |
+
'CNN1D_15': 0.0,
|
| 1487 |
+
'CNN1D_16': 0.0,
|
| 1488 |
+
'CNN1D_17': 0.0,
|
| 1489 |
+
'CNN1D_18': 0.0,
|
| 1490 |
+
'CNN1D_19': 0.0,
|
| 1491 |
+
'CNN1D_2': 0.0,
|
| 1492 |
+
'CNN1D_20': 0.0,
|
| 1493 |
+
'CNN1D_21': 0.0,
|
| 1494 |
+
'CNN1D_22': 0.0,
|
| 1495 |
+
'CNN1D_23': 0.0,
|
| 1496 |
+
'CNN1D_24': 0.0,
|
| 1497 |
+
'CNN1D_25': 0.0,
|
| 1498 |
+
'CNN1D_26': 0.0,
|
| 1499 |
+
'CNN1D_27': 0.0,
|
| 1500 |
+
'CNN1D_28': 0.0,
|
| 1501 |
+
'CNN1D_29': 0.0,
|
| 1502 |
+
'CNN1D_3': 0.0,
|
| 1503 |
+
'CNN1D_30': 0.0,
|
| 1504 |
+
'CNN1D_31': 0.0,
|
| 1505 |
+
'CNN1D_32': 0.0,
|
| 1506 |
+
'CNN1D_33': 0.0,
|
| 1507 |
+
'CNN1D_34': 0.0,
|
| 1508 |
+
'CNN1D_35': 0.0,
|
| 1509 |
+
'CNN1D_36': 0.0,
|
| 1510 |
+
'CNN1D_37': 0.0,
|
| 1511 |
+
'CNN1D_38': 0.0,
|
| 1512 |
+
'CNN1D_39': 0.0,
|
| 1513 |
+
'CNN1D_4': 0.0,
|
| 1514 |
+
'CNN1D_40': 0.0,
|
| 1515 |
+
'CNN1D_41': 0.0,
|
| 1516 |
+
'CNN1D_42': 0.0,
|
| 1517 |
+
'CNN1D_43': 0.0,
|
| 1518 |
+
'CNN1D_44': 0.0,
|
| 1519 |
+
'CNN1D_45': 0.0,
|
| 1520 |
+
'CNN1D_46': 0.0,
|
| 1521 |
+
'CNN1D_47': 0.0,
|
| 1522 |
+
'CNN1D_48': 0.0,
|
| 1523 |
+
'CNN1D_49': 0.0,
|
| 1524 |
+
'CNN1D_5': 0.0,
|
| 1525 |
+
'CNN1D_50': 0.0,
|
| 1526 |
+
'CNN1D_51': 0.0,
|
| 1527 |
+
'CNN1D_52': 0.0,
|
| 1528 |
+
'CNN1D_53': 0.0,
|
| 1529 |
+
'CNN1D_54': 0.0,
|
| 1530 |
+
'CNN1D_55': 0.0,
|
| 1531 |
+
'CNN1D_56': 0.0,
|
| 1532 |
+
'CNN1D_57': 0.0,
|
| 1533 |
+
'CNN1D_58': 0.0,
|
| 1534 |
+
'CNN1D_59': 0.0,
|
| 1535 |
+
'CNN1D_6': 0.0,
|
| 1536 |
+
'CNN1D_60': 0.0,
|
| 1537 |
+
'CNN1D_61': 0.0,
|
| 1538 |
+
'CNN1D_62': 0.0,
|
| 1539 |
+
'CNN1D_63': 0.0,
|
| 1540 |
+
'CNN1D_7': 0.0,
|
| 1541 |
+
'CNN1D_8': 0.0,
|
| 1542 |
+
'CNN1D_9': 0.0,
|
| 1543 |
+
'cos_latitude': 0.638,
|
| 1544 |
+
'divergence': -0.0,
|
| 1545 |
+
'divergence_del2': -0.015,
|
| 1546 |
+
'divergence_dlat': -0.001,
|
| 1547 |
+
'divergence_dlon': 0.0,
|
| 1548 |
+
'geopotential_at_surface': 0.004,
|
| 1549 |
+
'geopotential_at_surface_del2': -0.048,
|
| 1550 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1551 |
+
'geopotential_at_surface_dlon': -0.0,
|
| 1552 |
+
'land_sea_mask': 0.335,
|
| 1553 |
+
'learned_positional_features': 0.0,
|
| 1554 |
+
'log_surface_pressure': 1.715,
|
| 1555 |
+
'log_surface_pressure_del2': 0.59,
|
| 1556 |
+
'log_surface_pressure_dlat': 0.126,
|
| 1557 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1558 |
+
'memory_divergence': -0.0,
|
| 1559 |
+
'memory_log_surface_pressure': 1.715,
|
| 1560 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1561 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1562 |
+
'memory_specific_humidity': 0.0,
|
| 1563 |
+
'memory_temperature_variation': -4.98,
|
| 1564 |
+
'memory_u': 0.007,
|
| 1565 |
+
'memory_v': -0.0,
|
| 1566 |
+
'memory_vorticity': -0.002,
|
| 1567 |
+
'pressure': 2.796,
|
| 1568 |
+
'radiation': 0.213,
|
| 1569 |
+
'sea_surface_temperature': 285.14,
|
| 1570 |
+
'sin_latitude': -0.0,
|
| 1571 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1572 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1573 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1574 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1575 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1576 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1577 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1578 |
+
'specific_cloud_liquid_water_content_dlon': -0.0,
|
| 1579 |
+
'specific_humidity': 0.0,
|
| 1580 |
+
'specific_humidity_del2': 0.0,
|
| 1581 |
+
'specific_humidity_dlat': 0.0,
|
| 1582 |
+
'specific_humidity_dlon': 0.0,
|
| 1583 |
+
'surface_embedding': 0.0,
|
| 1584 |
+
'temperature_variation': -4.98,
|
| 1585 |
+
'temperature_variation_del2': 43.608,
|
| 1586 |
+
'temperature_variation_dlat': 4.843,
|
| 1587 |
+
'temperature_variation_dlon': 0.0,
|
| 1588 |
+
'u': 0.007,
|
| 1589 |
+
'u_del2': 0.242,
|
| 1590 |
+
'u_dlat': 0.002,
|
| 1591 |
+
'u_dlon': 0.0,
|
| 1592 |
+
'v': -0.0,
|
| 1593 |
+
'v_del2': -0.0,
|
| 1594 |
+
'v_dlat': -0.0,
|
| 1595 |
+
'v_dlon': 0.0,
|
| 1596 |
+
'vorticity': -0.002,
|
| 1597 |
+
'vorticity_del2': -0.365,
|
| 1598 |
+
'vorticity_dlat': 0.041,
|
| 1599 |
+
'vorticity_dlon': 0.0}
|
| 1600 |
+
|
| 1601 |
+
# Parameters for embedding_model/ShiftAndNormalize:
|
| 1602 |
+
# ==============================================================================
|
| 1603 |
+
embedding_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1604 |
+
embedding_model/ShiftAndNormalize.global_scale = None
|
| 1605 |
+
embedding_model/ShiftAndNormalize.name = None
|
| 1606 |
+
embedding_model/ShiftAndNormalize.scales = \
|
| 1607 |
+
{'CNN1D_0': 0.1,
|
| 1608 |
+
'CNN1D_1': 0.1,
|
| 1609 |
+
'CNN1D_10': 0.1,
|
| 1610 |
+
'CNN1D_11': 0.1,
|
| 1611 |
+
'CNN1D_12': 0.1,
|
| 1612 |
+
'CNN1D_13': 0.1,
|
| 1613 |
+
'CNN1D_14': 0.1,
|
| 1614 |
+
'CNN1D_15': 0.1,
|
| 1615 |
+
'CNN1D_16': 0.1,
|
| 1616 |
+
'CNN1D_17': 0.1,
|
| 1617 |
+
'CNN1D_18': 0.1,
|
| 1618 |
+
'CNN1D_19': 0.1,
|
| 1619 |
+
'CNN1D_2': 0.1,
|
| 1620 |
+
'CNN1D_20': 0.1,
|
| 1621 |
+
'CNN1D_21': 0.1,
|
| 1622 |
+
'CNN1D_22': 0.1,
|
| 1623 |
+
'CNN1D_23': 0.1,
|
| 1624 |
+
'CNN1D_24': 0.1,
|
| 1625 |
+
'CNN1D_25': 0.1,
|
| 1626 |
+
'CNN1D_26': 0.1,
|
| 1627 |
+
'CNN1D_27': 0.1,
|
| 1628 |
+
'CNN1D_28': 0.1,
|
| 1629 |
+
'CNN1D_29': 0.1,
|
| 1630 |
+
'CNN1D_3': 0.1,
|
| 1631 |
+
'CNN1D_30': 0.1,
|
| 1632 |
+
'CNN1D_31': 0.1,
|
| 1633 |
+
'CNN1D_32': 0.1,
|
| 1634 |
+
'CNN1D_33': 0.1,
|
| 1635 |
+
'CNN1D_34': 0.1,
|
| 1636 |
+
'CNN1D_35': 0.1,
|
| 1637 |
+
'CNN1D_36': 0.1,
|
| 1638 |
+
'CNN1D_37': 0.1,
|
| 1639 |
+
'CNN1D_38': 0.1,
|
| 1640 |
+
'CNN1D_39': 0.1,
|
| 1641 |
+
'CNN1D_4': 0.1,
|
| 1642 |
+
'CNN1D_40': 0.1,
|
| 1643 |
+
'CNN1D_41': 0.1,
|
| 1644 |
+
'CNN1D_42': 0.1,
|
| 1645 |
+
'CNN1D_43': 0.1,
|
| 1646 |
+
'CNN1D_44': 0.1,
|
| 1647 |
+
'CNN1D_45': 0.1,
|
| 1648 |
+
'CNN1D_46': 0.1,
|
| 1649 |
+
'CNN1D_47': 0.1,
|
| 1650 |
+
'CNN1D_48': 0.1,
|
| 1651 |
+
'CNN1D_49': 0.1,
|
| 1652 |
+
'CNN1D_5': 0.1,
|
| 1653 |
+
'CNN1D_50': 0.1,
|
| 1654 |
+
'CNN1D_51': 0.1,
|
| 1655 |
+
'CNN1D_52': 0.1,
|
| 1656 |
+
'CNN1D_53': 0.1,
|
| 1657 |
+
'CNN1D_54': 0.1,
|
| 1658 |
+
'CNN1D_55': 0.1,
|
| 1659 |
+
'CNN1D_56': 0.1,
|
| 1660 |
+
'CNN1D_57': 0.1,
|
| 1661 |
+
'CNN1D_58': 0.1,
|
| 1662 |
+
'CNN1D_59': 0.1,
|
| 1663 |
+
'CNN1D_6': 0.1,
|
| 1664 |
+
'CNN1D_60': 0.1,
|
| 1665 |
+
'CNN1D_61': 0.1,
|
| 1666 |
+
'CNN1D_62': 0.1,
|
| 1667 |
+
'CNN1D_63': 0.1,
|
| 1668 |
+
'CNN1D_7': 0.1,
|
| 1669 |
+
'CNN1D_8': 0.1,
|
| 1670 |
+
'CNN1D_9': 0.1,
|
| 1671 |
+
'cos_latitude': 0.3068,
|
| 1672 |
+
'divergence': 0.1078,
|
| 1673 |
+
'divergence_del2': 1825.0,
|
| 1674 |
+
'divergence_dlat': 8.5,
|
| 1675 |
+
'divergence_dlon': 8.842,
|
| 1676 |
+
'geopotential_at_surface': 0.009579,
|
| 1677 |
+
'geopotential_at_surface_del2': 28.66,
|
| 1678 |
+
'geopotential_at_surface_dlat': 0.1882,
|
| 1679 |
+
'geopotential_at_surface_dlon': 0.1564,
|
| 1680 |
+
'land_sea_mask': 0.456,
|
| 1681 |
+
'learned_positional_features': 1.0,
|
| 1682 |
+
'log_surface_pressure': 0.1134,
|
| 1683 |
+
'log_surface_pressure_del2': 264.6,
|
| 1684 |
+
'log_surface_pressure_dlat': 1.656,
|
| 1685 |
+
'log_surface_pressure_dlon': 1.645,
|
| 1686 |
+
'memory_divergence': 0.1078,
|
| 1687 |
+
'memory_log_surface_pressure': 0.1134,
|
| 1688 |
+
'memory_specific_cloud_ice_water_content': 9.812e-06,
|
| 1689 |
+
'memory_specific_cloud_liquid_water_content': 2.039e-05,
|
| 1690 |
+
'memory_specific_humidity': 0.003305,
|
| 1691 |
+
'memory_temperature_variation': 15.04,
|
| 1692 |
+
'memory_u': 0.01489,
|
| 1693 |
+
'memory_v': 0.01022,
|
| 1694 |
+
'memory_vorticity': 0.2831,
|
| 1695 |
+
'pressure': 1.644,
|
| 1696 |
+
'radiation': 0.2862,
|
| 1697 |
+
'sea_surface_temperature': 11.93,
|
| 1698 |
+
'sin_latitude': 0.7064,
|
| 1699 |
+
'specific_cloud_ice_water_content': 9.812e-06,
|
| 1700 |
+
'specific_cloud_ice_water_content_del2': 0.08441,
|
| 1701 |
+
'specific_cloud_ice_water_content_dlat': 0.0004684,
|
| 1702 |
+
'specific_cloud_ice_water_content_dlon': 0.0004646,
|
| 1703 |
+
'specific_cloud_liquid_water_content': 2.039e-05,
|
| 1704 |
+
'specific_cloud_liquid_water_content_del2': 0.1802,
|
| 1705 |
+
'specific_cloud_liquid_water_content_dlat': 0.0009877,
|
| 1706 |
+
'specific_cloud_liquid_water_content_dlon': 0.0009565,
|
| 1707 |
+
'specific_humidity': 0.003305,
|
| 1708 |
+
'specific_humidity_del2': 4.442,
|
| 1709 |
+
'specific_humidity_dlat': 0.0319,
|
| 1710 |
+
'specific_humidity_dlon': 0.02678,
|
| 1711 |
+
'surface_embedding': 1.0,
|
| 1712 |
+
'temperature_variation': 15.04,
|
| 1713 |
+
'temperature_variation_del2': 10460.0,
|
| 1714 |
+
'temperature_variation_dlat': 82.59,
|
| 1715 |
+
'temperature_variation_dlon': 77.07,
|
| 1716 |
+
'u': 0.01489,
|
| 1717 |
+
'u_del2': 18.46,
|
| 1718 |
+
'u_dlat': 0.2197,
|
| 1719 |
+
'u_dlon': 0.1677,
|
| 1720 |
+
'v': 0.01022,
|
| 1721 |
+
'v_del2': 17.2,
|
| 1722 |
+
'v_dlat': 0.169,
|
| 1723 |
+
'v_dlon': 0.2083,
|
| 1724 |
+
'vorticity': 0.2831,
|
| 1725 |
+
'vorticity_del2': 2831.0,
|
| 1726 |
+
'vorticity_dlat': 14.96,
|
| 1727 |
+
'vorticity_dlon': 14.91}
|
| 1728 |
+
embedding_model/ShiftAndNormalize.shifts = \
|
| 1729 |
+
{'CNN1D_0': 0.0,
|
| 1730 |
+
'CNN1D_1': 0.0,
|
| 1731 |
+
'CNN1D_10': 0.0,
|
| 1732 |
+
'CNN1D_11': 0.0,
|
| 1733 |
+
'CNN1D_12': 0.0,
|
| 1734 |
+
'CNN1D_13': 0.0,
|
| 1735 |
+
'CNN1D_14': 0.0,
|
| 1736 |
+
'CNN1D_15': 0.0,
|
| 1737 |
+
'CNN1D_16': 0.0,
|
| 1738 |
+
'CNN1D_17': 0.0,
|
| 1739 |
+
'CNN1D_18': 0.0,
|
| 1740 |
+
'CNN1D_19': 0.0,
|
| 1741 |
+
'CNN1D_2': 0.0,
|
| 1742 |
+
'CNN1D_20': 0.0,
|
| 1743 |
+
'CNN1D_21': 0.0,
|
| 1744 |
+
'CNN1D_22': 0.0,
|
| 1745 |
+
'CNN1D_23': 0.0,
|
| 1746 |
+
'CNN1D_24': 0.0,
|
| 1747 |
+
'CNN1D_25': 0.0,
|
| 1748 |
+
'CNN1D_26': 0.0,
|
| 1749 |
+
'CNN1D_27': 0.0,
|
| 1750 |
+
'CNN1D_28': 0.0,
|
| 1751 |
+
'CNN1D_29': 0.0,
|
| 1752 |
+
'CNN1D_3': 0.0,
|
| 1753 |
+
'CNN1D_30': 0.0,
|
| 1754 |
+
'CNN1D_31': 0.0,
|
| 1755 |
+
'CNN1D_32': 0.0,
|
| 1756 |
+
'CNN1D_33': 0.0,
|
| 1757 |
+
'CNN1D_34': 0.0,
|
| 1758 |
+
'CNN1D_35': 0.0,
|
| 1759 |
+
'CNN1D_36': 0.0,
|
| 1760 |
+
'CNN1D_37': 0.0,
|
| 1761 |
+
'CNN1D_38': 0.0,
|
| 1762 |
+
'CNN1D_39': 0.0,
|
| 1763 |
+
'CNN1D_4': 0.0,
|
| 1764 |
+
'CNN1D_40': 0.0,
|
| 1765 |
+
'CNN1D_41': 0.0,
|
| 1766 |
+
'CNN1D_42': 0.0,
|
| 1767 |
+
'CNN1D_43': 0.0,
|
| 1768 |
+
'CNN1D_44': 0.0,
|
| 1769 |
+
'CNN1D_45': 0.0,
|
| 1770 |
+
'CNN1D_46': 0.0,
|
| 1771 |
+
'CNN1D_47': 0.0,
|
| 1772 |
+
'CNN1D_48': 0.0,
|
| 1773 |
+
'CNN1D_49': 0.0,
|
| 1774 |
+
'CNN1D_5': 0.0,
|
| 1775 |
+
'CNN1D_50': 0.0,
|
| 1776 |
+
'CNN1D_51': 0.0,
|
| 1777 |
+
'CNN1D_52': 0.0,
|
| 1778 |
+
'CNN1D_53': 0.0,
|
| 1779 |
+
'CNN1D_54': 0.0,
|
| 1780 |
+
'CNN1D_55': 0.0,
|
| 1781 |
+
'CNN1D_56': 0.0,
|
| 1782 |
+
'CNN1D_57': 0.0,
|
| 1783 |
+
'CNN1D_58': 0.0,
|
| 1784 |
+
'CNN1D_59': 0.0,
|
| 1785 |
+
'CNN1D_6': 0.0,
|
| 1786 |
+
'CNN1D_60': 0.0,
|
| 1787 |
+
'CNN1D_61': 0.0,
|
| 1788 |
+
'CNN1D_62': 0.0,
|
| 1789 |
+
'CNN1D_63': 0.0,
|
| 1790 |
+
'CNN1D_7': 0.0,
|
| 1791 |
+
'CNN1D_8': 0.0,
|
| 1792 |
+
'CNN1D_9': 0.0,
|
| 1793 |
+
'cos_latitude': 0.638,
|
| 1794 |
+
'divergence': -0.0,
|
| 1795 |
+
'divergence_del2': -0.015,
|
| 1796 |
+
'divergence_dlat': -0.001,
|
| 1797 |
+
'divergence_dlon': 0.0,
|
| 1798 |
+
'geopotential_at_surface': 0.004,
|
| 1799 |
+
'geopotential_at_surface_del2': -0.048,
|
| 1800 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1801 |
+
'geopotential_at_surface_dlon': -0.0,
|
| 1802 |
+
'land_sea_mask': 0.335,
|
| 1803 |
+
'learned_positional_features': 0.0,
|
| 1804 |
+
'log_surface_pressure': 1.715,
|
| 1805 |
+
'log_surface_pressure_del2': 0.59,
|
| 1806 |
+
'log_surface_pressure_dlat': 0.126,
|
| 1807 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1808 |
+
'memory_divergence': -0.0,
|
| 1809 |
+
'memory_log_surface_pressure': 1.715,
|
| 1810 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1811 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1812 |
+
'memory_specific_humidity': 0.0,
|
| 1813 |
+
'memory_temperature_variation': -4.98,
|
| 1814 |
+
'memory_u': 0.007,
|
| 1815 |
+
'memory_v': -0.0,
|
| 1816 |
+
'memory_vorticity': -0.002,
|
| 1817 |
+
'pressure': 2.796,
|
| 1818 |
+
'radiation': 0.213,
|
| 1819 |
+
'sea_surface_temperature': 285.14,
|
| 1820 |
+
'sin_latitude': -0.0,
|
| 1821 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1822 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1823 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1824 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1825 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1826 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1827 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1828 |
+
'specific_cloud_liquid_water_content_dlon': -0.0,
|
| 1829 |
+
'specific_humidity': 0.0,
|
| 1830 |
+
'specific_humidity_del2': 0.0,
|
| 1831 |
+
'specific_humidity_dlat': 0.0,
|
| 1832 |
+
'specific_humidity_dlon': 0.0,
|
| 1833 |
+
'surface_embedding': 0.0,
|
| 1834 |
+
'temperature_variation': -4.98,
|
| 1835 |
+
'temperature_variation_del2': 43.608,
|
| 1836 |
+
'temperature_variation_dlat': 4.843,
|
| 1837 |
+
'temperature_variation_dlon': 0.0,
|
| 1838 |
+
'u': 0.007,
|
| 1839 |
+
'u_del2': 0.242,
|
| 1840 |
+
'u_dlat': 0.002,
|
| 1841 |
+
'u_dlon': 0.0,
|
| 1842 |
+
'v': -0.0,
|
| 1843 |
+
'v_del2': -0.0,
|
| 1844 |
+
'v_dlat': -0.0,
|
| 1845 |
+
'v_dlon': 0.0,
|
| 1846 |
+
'vorticity': -0.002,
|
| 1847 |
+
'vorticity_del2': -0.365,
|
| 1848 |
+
'vorticity_dlat': 0.041,
|
| 1849 |
+
'vorticity_dlon': 0.0}
|
| 1850 |
+
|
| 1851 |
+
# Parameters for encoder_data/ShiftAndNormalize:
|
| 1852 |
+
# ==============================================================================
|
| 1853 |
+
encoder_data/ShiftAndNormalize.features_to_exclude = ()
|
| 1854 |
+
encoder_data/ShiftAndNormalize.global_scale = None
|
| 1855 |
+
encoder_data/ShiftAndNormalize.name = None
|
| 1856 |
+
encoder_data/ShiftAndNormalize.scales = \
|
| 1857 |
+
{'cos_latitude': 0.3068,
|
| 1858 |
+
'geopotential_at_surface': 0.009579,
|
| 1859 |
+
'geopotential_at_surface_del2': 28.66,
|
| 1860 |
+
'geopotential_at_surface_dlat': 0.1882,
|
| 1861 |
+
'geopotential_at_surface_dlon': 0.1564,
|
| 1862 |
+
'land_sea_mask': 0.456,
|
| 1863 |
+
'learned_positional_features': 1.0,
|
| 1864 |
+
'radiation': 0.2862,
|
| 1865 |
+
'sin_latitude': 0.7064,
|
| 1866 |
+
'specific_cloud_ice_water_content': 8.255e-06,
|
| 1867 |
+
'specific_cloud_ice_water_content_del2': 0.09943,
|
| 1868 |
+
'specific_cloud_ice_water_content_dlat': 0.0005741,
|
| 1869 |
+
'specific_cloud_ice_water_content_dlon': 0.0004452,
|
| 1870 |
+
'specific_cloud_liquid_water_content': 2.182e-05,
|
| 1871 |
+
'specific_cloud_liquid_water_content_del2': 0.2881,
|
| 1872 |
+
'specific_cloud_liquid_water_content_dlat': 0.001674,
|
| 1873 |
+
'specific_cloud_liquid_water_content_dlon': 0.001224,
|
| 1874 |
+
'specific_humidity': 0.003493,
|
| 1875 |
+
'specific_humidity_del2': 5.716,
|
| 1876 |
+
'specific_humidity_dlat': 0.0396,
|
| 1877 |
+
'specific_humidity_dlon': 0.02857,
|
| 1878 |
+
't': 27.99,
|
| 1879 |
+
't_del2': 9788.0,
|
| 1880 |
+
't_dlat': 71.14,
|
| 1881 |
+
't_dlon': 62.14,
|
| 1882 |
+
'u': 0.01935,
|
| 1883 |
+
'u_del2': 27.4,
|
| 1884 |
+
'u_dlat': 0.1876,
|
| 1885 |
+
'u_dlon': 0.1712,
|
| 1886 |
+
'v': 0.01038,
|
| 1887 |
+
'v_del2': 27.11,
|
| 1888 |
+
'v_dlat': 0.1326,
|
| 1889 |
+
'v_dlon': 0.1998,
|
| 1890 |
+
'z': 0.1496,
|
| 1891 |
+
'z_del2': 0.6108,
|
| 1892 |
+
'z_dlat': 0.01606,
|
| 1893 |
+
'z_dlon': 0.009139}
|
| 1894 |
+
encoder_data/ShiftAndNormalize.shifts = \
|
| 1895 |
+
{'cos_latitude': 0.638,
|
| 1896 |
+
'geopotential_at_surface': 0.004,
|
| 1897 |
+
'geopotential_at_surface_del2': -0.048,
|
| 1898 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1899 |
+
'geopotential_at_surface_dlon': -0.0,
|
| 1900 |
+
'land_sea_mask': 0.335,
|
| 1901 |
+
'learned_positional_features': 0.0,
|
| 1902 |
+
'radiation': 0.213,
|
| 1903 |
+
'sin_latitude': -0.0,
|
| 1904 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1905 |
+
'specific_cloud_ice_water_content_del2': -0.0,
|
| 1906 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1907 |
+
'specific_cloud_ice_water_content_dlon': 0.0,
|
| 1908 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1909 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1910 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1911 |
+
'specific_cloud_liquid_water_content_dlon': 0.0,
|
| 1912 |
+
'specific_humidity': 0.0,
|
| 1913 |
+
'specific_humidity_del2': 0.0,
|
| 1914 |
+
'specific_humidity_dlat': 0.0,
|
| 1915 |
+
'specific_humidity_dlon': 0.0,
|
| 1916 |
+
't': 247.045,
|
| 1917 |
+
't_del2': 37.572,
|
| 1918 |
+
't_dlat': 1.755,
|
| 1919 |
+
't_dlon': -0.0,
|
| 1920 |
+
'u': 0.007,
|
| 1921 |
+
'u_del2': 0.056,
|
| 1922 |
+
'u_dlat': 0.0,
|
| 1923 |
+
'u_dlon': 0.0,
|
| 1924 |
+
'v': 0.0,
|
| 1925 |
+
'v_del2': 0.008,
|
| 1926 |
+
'v_dlat': 0.0,
|
| 1927 |
+
'v_dlon': -0.0,
|
| 1928 |
+
'z': 0.145,
|
| 1929 |
+
'z_del2': 0.011,
|
| 1930 |
+
'z_dlat': 0.001,
|
| 1931 |
+
'z_dlon': 0.0}
|
| 1932 |
+
|
| 1933 |
+
# Parameters for sea_model/ShiftAndNormalize:
|
| 1934 |
+
# ==============================================================================
|
| 1935 |
+
sea_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1936 |
+
sea_model/ShiftAndNormalize.global_scale = None
|
| 1937 |
+
sea_model/ShiftAndNormalize.name = None
|
| 1938 |
+
sea_model/ShiftAndNormalize.scales = \
|
| 1939 |
+
{'CNN1D_0': 0.1,
|
| 1940 |
+
'CNN1D_1': 0.1,
|
| 1941 |
+
'CNN1D_10': 0.1,
|
| 1942 |
+
'CNN1D_11': 0.1,
|
| 1943 |
+
'CNN1D_12': 0.1,
|
| 1944 |
+
'CNN1D_13': 0.1,
|
| 1945 |
+
'CNN1D_14': 0.1,
|
| 1946 |
+
'CNN1D_15': 0.1,
|
| 1947 |
+
'CNN1D_16': 0.1,
|
| 1948 |
+
'CNN1D_17': 0.1,
|
| 1949 |
+
'CNN1D_18': 0.1,
|
| 1950 |
+
'CNN1D_19': 0.1,
|
| 1951 |
+
'CNN1D_2': 0.1,
|
| 1952 |
+
'CNN1D_20': 0.1,
|
| 1953 |
+
'CNN1D_21': 0.1,
|
| 1954 |
+
'CNN1D_22': 0.1,
|
| 1955 |
+
'CNN1D_23': 0.1,
|
| 1956 |
+
'CNN1D_24': 0.1,
|
| 1957 |
+
'CNN1D_25': 0.1,
|
| 1958 |
+
'CNN1D_26': 0.1,
|
| 1959 |
+
'CNN1D_27': 0.1,
|
| 1960 |
+
'CNN1D_28': 0.1,
|
| 1961 |
+
'CNN1D_29': 0.1,
|
| 1962 |
+
'CNN1D_3': 0.1,
|
| 1963 |
+
'CNN1D_30': 0.1,
|
| 1964 |
+
'CNN1D_31': 0.1,
|
| 1965 |
+
'CNN1D_32': 0.1,
|
| 1966 |
+
'CNN1D_33': 0.1,
|
| 1967 |
+
'CNN1D_34': 0.1,
|
| 1968 |
+
'CNN1D_35': 0.1,
|
| 1969 |
+
'CNN1D_36': 0.1,
|
| 1970 |
+
'CNN1D_37': 0.1,
|
| 1971 |
+
'CNN1D_38': 0.1,
|
| 1972 |
+
'CNN1D_39': 0.1,
|
| 1973 |
+
'CNN1D_4': 0.1,
|
| 1974 |
+
'CNN1D_40': 0.1,
|
| 1975 |
+
'CNN1D_41': 0.1,
|
| 1976 |
+
'CNN1D_42': 0.1,
|
| 1977 |
+
'CNN1D_43': 0.1,
|
| 1978 |
+
'CNN1D_44': 0.1,
|
| 1979 |
+
'CNN1D_45': 0.1,
|
| 1980 |
+
'CNN1D_46': 0.1,
|
| 1981 |
+
'CNN1D_47': 0.1,
|
| 1982 |
+
'CNN1D_48': 0.1,
|
| 1983 |
+
'CNN1D_49': 0.1,
|
| 1984 |
+
'CNN1D_5': 0.1,
|
| 1985 |
+
'CNN1D_50': 0.1,
|
| 1986 |
+
'CNN1D_51': 0.1,
|
| 1987 |
+
'CNN1D_52': 0.1,
|
| 1988 |
+
'CNN1D_53': 0.1,
|
| 1989 |
+
'CNN1D_54': 0.1,
|
| 1990 |
+
'CNN1D_55': 0.1,
|
| 1991 |
+
'CNN1D_56': 0.1,
|
| 1992 |
+
'CNN1D_57': 0.1,
|
| 1993 |
+
'CNN1D_58': 0.1,
|
| 1994 |
+
'CNN1D_59': 0.1,
|
| 1995 |
+
'CNN1D_6': 0.1,
|
| 1996 |
+
'CNN1D_60': 0.1,
|
| 1997 |
+
'CNN1D_61': 0.1,
|
| 1998 |
+
'CNN1D_62': 0.1,
|
| 1999 |
+
'CNN1D_63': 0.1,
|
| 2000 |
+
'CNN1D_7': 0.1,
|
| 2001 |
+
'CNN1D_8': 0.1,
|
| 2002 |
+
'CNN1D_9': 0.1,
|
| 2003 |
+
'cos_latitude': 0.3068,
|
| 2004 |
+
'divergence': 0.1078,
|
| 2005 |
+
'divergence_del2': 1825.0,
|
| 2006 |
+
'divergence_dlat': 8.5,
|
| 2007 |
+
'divergence_dlon': 8.842,
|
| 2008 |
+
'geopotential_at_surface': 0.009579,
|
| 2009 |
+
'geopotential_at_surface_del2': 28.66,
|
| 2010 |
+
'geopotential_at_surface_dlat': 0.1882,
|
| 2011 |
+
'geopotential_at_surface_dlon': 0.1564,
|
| 2012 |
+
'land_sea_mask': 0.456,
|
| 2013 |
+
'learned_positional_features': 1.0,
|
| 2014 |
+
'log_surface_pressure': 0.1134,
|
| 2015 |
+
'log_surface_pressure_del2': 264.6,
|
| 2016 |
+
'log_surface_pressure_dlat': 1.656,
|
| 2017 |
+
'log_surface_pressure_dlon': 1.645,
|
| 2018 |
+
'memory_divergence': 0.1078,
|
| 2019 |
+
'memory_log_surface_pressure': 0.1134,
|
| 2020 |
+
'memory_specific_cloud_ice_water_content': 9.812e-06,
|
| 2021 |
+
'memory_specific_cloud_liquid_water_content': 2.039e-05,
|
| 2022 |
+
'memory_specific_humidity': 0.003305,
|
| 2023 |
+
'memory_temperature_variation': 15.04,
|
| 2024 |
+
'memory_u': 0.01489,
|
| 2025 |
+
'memory_v': 0.01022,
|
| 2026 |
+
'memory_vorticity': 0.2831,
|
| 2027 |
+
'pressure': 1.644,
|
| 2028 |
+
'radiation': 0.2862,
|
| 2029 |
+
'sea_surface_temperature': 11.93,
|
| 2030 |
+
'sin_latitude': 0.7064,
|
| 2031 |
+
'specific_cloud_ice_water_content': 9.812e-06,
|
| 2032 |
+
'specific_cloud_ice_water_content_del2': 0.08441,
|
| 2033 |
+
'specific_cloud_ice_water_content_dlat': 0.0004684,
|
| 2034 |
+
'specific_cloud_ice_water_content_dlon': 0.0004646,
|
| 2035 |
+
'specific_cloud_liquid_water_content': 2.039e-05,
|
| 2036 |
+
'specific_cloud_liquid_water_content_del2': 0.1802,
|
| 2037 |
+
'specific_cloud_liquid_water_content_dlat': 0.0009877,
|
| 2038 |
+
'specific_cloud_liquid_water_content_dlon': 0.0009565,
|
| 2039 |
+
'specific_humidity': 0.003305,
|
| 2040 |
+
'specific_humidity_del2': 4.442,
|
| 2041 |
+
'specific_humidity_dlat': 0.0319,
|
| 2042 |
+
'specific_humidity_dlon': 0.02678,
|
| 2043 |
+
'surface_embedding': 1.0,
|
| 2044 |
+
'temperature_variation': 15.04,
|
| 2045 |
+
'temperature_variation_del2': 10460.0,
|
| 2046 |
+
'temperature_variation_dlat': 82.59,
|
| 2047 |
+
'temperature_variation_dlon': 77.07,
|
| 2048 |
+
'u': 0.01489,
|
| 2049 |
+
'u_del2': 18.46,
|
| 2050 |
+
'u_dlat': 0.2197,
|
| 2051 |
+
'u_dlon': 0.1677,
|
| 2052 |
+
'v': 0.01022,
|
| 2053 |
+
'v_del2': 17.2,
|
| 2054 |
+
'v_dlat': 0.169,
|
| 2055 |
+
'v_dlon': 0.2083,
|
| 2056 |
+
'vorticity': 0.2831,
|
| 2057 |
+
'vorticity_del2': 2831.0,
|
| 2058 |
+
'vorticity_dlat': 14.96,
|
| 2059 |
+
'vorticity_dlon': 14.91}
|
| 2060 |
+
sea_model/ShiftAndNormalize.shifts = \
|
| 2061 |
+
{'CNN1D_0': 0.0,
|
| 2062 |
+
'CNN1D_1': 0.0,
|
| 2063 |
+
'CNN1D_10': 0.0,
|
| 2064 |
+
'CNN1D_11': 0.0,
|
| 2065 |
+
'CNN1D_12': 0.0,
|
| 2066 |
+
'CNN1D_13': 0.0,
|
| 2067 |
+
'CNN1D_14': 0.0,
|
| 2068 |
+
'CNN1D_15': 0.0,
|
| 2069 |
+
'CNN1D_16': 0.0,
|
| 2070 |
+
'CNN1D_17': 0.0,
|
| 2071 |
+
'CNN1D_18': 0.0,
|
| 2072 |
+
'CNN1D_19': 0.0,
|
| 2073 |
+
'CNN1D_2': 0.0,
|
| 2074 |
+
'CNN1D_20': 0.0,
|
| 2075 |
+
'CNN1D_21': 0.0,
|
| 2076 |
+
'CNN1D_22': 0.0,
|
| 2077 |
+
'CNN1D_23': 0.0,
|
| 2078 |
+
'CNN1D_24': 0.0,
|
| 2079 |
+
'CNN1D_25': 0.0,
|
| 2080 |
+
'CNN1D_26': 0.0,
|
| 2081 |
+
'CNN1D_27': 0.0,
|
| 2082 |
+
'CNN1D_28': 0.0,
|
| 2083 |
+
'CNN1D_29': 0.0,
|
| 2084 |
+
'CNN1D_3': 0.0,
|
| 2085 |
+
'CNN1D_30': 0.0,
|
| 2086 |
+
'CNN1D_31': 0.0,
|
| 2087 |
+
'CNN1D_32': 0.0,
|
| 2088 |
+
'CNN1D_33': 0.0,
|
| 2089 |
+
'CNN1D_34': 0.0,
|
| 2090 |
+
'CNN1D_35': 0.0,
|
| 2091 |
+
'CNN1D_36': 0.0,
|
| 2092 |
+
'CNN1D_37': 0.0,
|
| 2093 |
+
'CNN1D_38': 0.0,
|
| 2094 |
+
'CNN1D_39': 0.0,
|
| 2095 |
+
'CNN1D_4': 0.0,
|
| 2096 |
+
'CNN1D_40': 0.0,
|
| 2097 |
+
'CNN1D_41': 0.0,
|
| 2098 |
+
'CNN1D_42': 0.0,
|
| 2099 |
+
'CNN1D_43': 0.0,
|
| 2100 |
+
'CNN1D_44': 0.0,
|
| 2101 |
+
'CNN1D_45': 0.0,
|
| 2102 |
+
'CNN1D_46': 0.0,
|
| 2103 |
+
'CNN1D_47': 0.0,
|
| 2104 |
+
'CNN1D_48': 0.0,
|
| 2105 |
+
'CNN1D_49': 0.0,
|
| 2106 |
+
'CNN1D_5': 0.0,
|
| 2107 |
+
'CNN1D_50': 0.0,
|
| 2108 |
+
'CNN1D_51': 0.0,
|
| 2109 |
+
'CNN1D_52': 0.0,
|
| 2110 |
+
'CNN1D_53': 0.0,
|
| 2111 |
+
'CNN1D_54': 0.0,
|
| 2112 |
+
'CNN1D_55': 0.0,
|
| 2113 |
+
'CNN1D_56': 0.0,
|
| 2114 |
+
'CNN1D_57': 0.0,
|
| 2115 |
+
'CNN1D_58': 0.0,
|
| 2116 |
+
'CNN1D_59': 0.0,
|
| 2117 |
+
'CNN1D_6': 0.0,
|
| 2118 |
+
'CNN1D_60': 0.0,
|
| 2119 |
+
'CNN1D_61': 0.0,
|
| 2120 |
+
'CNN1D_62': 0.0,
|
| 2121 |
+
'CNN1D_63': 0.0,
|
| 2122 |
+
'CNN1D_7': 0.0,
|
| 2123 |
+
'CNN1D_8': 0.0,
|
| 2124 |
+
'CNN1D_9': 0.0,
|
| 2125 |
+
'cos_latitude': 0.638,
|
| 2126 |
+
'divergence': -0.0,
|
| 2127 |
+
'divergence_del2': -0.015,
|
| 2128 |
+
'divergence_dlat': -0.001,
|
| 2129 |
+
'divergence_dlon': 0.0,
|
| 2130 |
+
'geopotential_at_surface': 0.004,
|
| 2131 |
+
'geopotential_at_surface_del2': -0.048,
|
| 2132 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 2133 |
+
'geopotential_at_surface_dlon': -0.0,
|
| 2134 |
+
'land_sea_mask': 0.335,
|
| 2135 |
+
'learned_positional_features': 0.0,
|
| 2136 |
+
'log_surface_pressure': 1.715,
|
| 2137 |
+
'log_surface_pressure_del2': 0.59,
|
| 2138 |
+
'log_surface_pressure_dlat': 0.126,
|
| 2139 |
+
'log_surface_pressure_dlon': 0.0,
|
| 2140 |
+
'memory_divergence': -0.0,
|
| 2141 |
+
'memory_log_surface_pressure': 1.715,
|
| 2142 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 2143 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 2144 |
+
'memory_specific_humidity': 0.0,
|
| 2145 |
+
'memory_temperature_variation': -4.98,
|
| 2146 |
+
'memory_u': 0.007,
|
| 2147 |
+
'memory_v': -0.0,
|
| 2148 |
+
'memory_vorticity': -0.002,
|
| 2149 |
+
'pressure': 2.796,
|
| 2150 |
+
'radiation': 0.213,
|
| 2151 |
+
'sea_surface_temperature': 285.14,
|
| 2152 |
+
'sin_latitude': -0.0,
|
| 2153 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 2154 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 2155 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 2156 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 2157 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 2158 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 2159 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 2160 |
+
'specific_cloud_liquid_water_content_dlon': -0.0,
|
| 2161 |
+
'specific_humidity': 0.0,
|
| 2162 |
+
'specific_humidity_del2': 0.0,
|
| 2163 |
+
'specific_humidity_dlat': 0.0,
|
| 2164 |
+
'specific_humidity_dlon': 0.0,
|
| 2165 |
+
'surface_embedding': 0.0,
|
| 2166 |
+
'temperature_variation': -4.98,
|
| 2167 |
+
'temperature_variation_del2': 43.608,
|
| 2168 |
+
'temperature_variation_dlat': 4.843,
|
| 2169 |
+
'temperature_variation_dlon': 0.0,
|
| 2170 |
+
'u': 0.007,
|
| 2171 |
+
'u_del2': 0.242,
|
| 2172 |
+
'u_dlat': 0.002,
|
| 2173 |
+
'u_dlon': 0.0,
|
| 2174 |
+
'v': -0.0,
|
| 2175 |
+
'v_del2': -0.0,
|
| 2176 |
+
'v_dlat': -0.0,
|
| 2177 |
+
'v_dlon': 0.0,
|
| 2178 |
+
'vorticity': -0.002,
|
| 2179 |
+
'vorticity_del2': -0.365,
|
| 2180 |
+
'vorticity_dlat': 0.041,
|
| 2181 |
+
'vorticity_dlon': 0.0}
|
| 2182 |
+
|
| 2183 |
+
# Parameters for SigmaCoordinatesEquidistant:
|
| 2184 |
+
# ==============================================================================
|
| 2185 |
+
SigmaCoordinatesEquidistant.layers = %N_SIGMA_LAYERS
|
| 2186 |
+
|
| 2187 |
+
# Parameters for custom_corrds/SigmaCoordinatesEquidistant:
|
| 2188 |
+
# ==============================================================================
|
| 2189 |
+
custom_corrds/SigmaCoordinatesEquidistant.layers = %N_SIGMA_LAYERS
|
| 2190 |
+
|
| 2191 |
+
# Parameters for advance/SoftClip:
|
| 2192 |
+
# ==============================================================================
|
| 2193 |
+
advance/SoftClip.hinge_softness = 1.0
|
| 2194 |
+
advance/SoftClip.max_value = 16
|
| 2195 |
+
advance/SoftClip.name = None
|
| 2196 |
+
|
| 2197 |
+
# Parameters for StochasticModularStepModel:
|
| 2198 |
+
# ==============================================================================
|
| 2199 |
+
StochasticModularStepModel.advance_module = @StochasticPhysicsParameterizationStep
|
| 2200 |
+
StochasticModularStepModel.decoder_module = \
|
| 2201 |
+
@DimensionalLearnedPrimitiveToWeatherbenchDecoder
|
| 2202 |
+
StochasticModularStepModel.encoder_module = \
|
| 2203 |
+
@DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder
|
| 2204 |
+
StochasticModularStepModel.forcing_module = @DynamicDataForcing
|
| 2205 |
+
StochasticModularStepModel.name = None
|
| 2206 |
+
|
| 2207 |
+
# Parameters for StochasticPhysicsParameterizationStep:
|
| 2208 |
+
# ==============================================================================
|
| 2209 |
+
StochasticPhysicsParameterizationStep.checkpoint_substep = False
|
| 2210 |
+
StochasticPhysicsParameterizationStep.corrector_module = %CORRECTOR_MODULE
|
| 2211 |
+
StochasticPhysicsParameterizationStep.name = None
|
| 2212 |
+
StochasticPhysicsParameterizationStep.num_substeps = %NUM_SUBSTEPS
|
| 2213 |
+
StochasticPhysicsParameterizationStep.physics_parameterization_module = \
|
| 2214 |
+
@DivCurlNeuralParameterization
|
| 2215 |
+
StochasticPhysicsParameterizationStep.randomness_module = @ZerosRandomField
|
| 2216 |
+
|
| 2217 |
+
# Parameters for advance/ToModalDiffOperators:
|
| 2218 |
+
# ==============================================================================
|
| 2219 |
+
advance/ToModalDiffOperators.name = None
|
| 2220 |
+
|
| 2221 |
+
# Parameters for encoder_data/ToModalDiffOperators:
|
| 2222 |
+
# ==============================================================================
|
| 2223 |
+
encoder_data/ToModalDiffOperators.name = None
|
| 2224 |
+
|
| 2225 |
+
# Parameters for with_grads/ToModalDiffOperators:
|
| 2226 |
+
# ==============================================================================
|
| 2227 |
+
with_grads/ToModalDiffOperators.name = None
|
| 2228 |
+
|
| 2229 |
+
# Parameters for trajectory_from_step:
|
| 2230 |
+
# ==============================================================================
|
| 2231 |
+
trajectory_from_step.checkpoint_multistep = False
|
| 2232 |
+
trajectory_from_step.checkpoint_post_process = True
|
| 2233 |
+
trajectory_from_step.checkpoint_step = True
|
| 2234 |
+
|
| 2235 |
+
# Parameters for advance/TruncateSigmaLevels:
|
| 2236 |
+
# ==============================================================================
|
| 2237 |
+
advance/TruncateSigmaLevels.name = None
|
| 2238 |
+
advance/TruncateSigmaLevels.sigma_ranges = \
|
| 2239 |
+
{'divergence': (0.0, 1),
|
| 2240 |
+
'geopotential': (0.0, 1),
|
| 2241 |
+
'specific_cloud_ice_water_content': (0.0, 1),
|
| 2242 |
+
'specific_cloud_liquid_water_content': (0.0, 1),
|
| 2243 |
+
'specific_humidity': (0.0, 1),
|
| 2244 |
+
't': (0.0, 1),
|
| 2245 |
+
'temperature_variation': (0.0, 1),
|
| 2246 |
+
'u': (0.0, 1),
|
| 2247 |
+
'u_component_of_wind': (0.0, 1),
|
| 2248 |
+
'v': (0.0, 1),
|
| 2249 |
+
'v_component_of_wind': (0.0, 1),
|
| 2250 |
+
'vorticity': (0.0, 1),
|
| 2251 |
+
'z': (0.0, 1)}
|
| 2252 |
+
|
| 2253 |
+
# Parameters for decoder_model/TruncateSigmaLevels:
|
| 2254 |
+
# ==============================================================================
|
| 2255 |
+
decoder_model/TruncateSigmaLevels.name = None
|
| 2256 |
+
decoder_model/TruncateSigmaLevels.sigma_ranges = \
|
| 2257 |
+
{'divergence': (0.0, 1),
|
| 2258 |
+
'geopotential': (0.0, 1),
|
| 2259 |
+
'specific_cloud_ice_water_content': (0.0, 1),
|
| 2260 |
+
'specific_cloud_liquid_water_content': (0.0, 1),
|
| 2261 |
+
'specific_humidity': (0.0, 1),
|
| 2262 |
+
't': (0.0, 1),
|
| 2263 |
+
'temperature_variation': (0.0, 1),
|
| 2264 |
+
'u': (0.0, 1),
|
| 2265 |
+
'u_component_of_wind': (0.0, 1),
|
| 2266 |
+
'v': (0.0, 1),
|
| 2267 |
+
'v_component_of_wind': (0.0, 1),
|
| 2268 |
+
'vorticity': (0.0, 1),
|
| 2269 |
+
'z': (0.0, 1)}
|
| 2270 |
+
|
| 2271 |
+
# Parameters for advance/VelocityAndPrognostics:
|
| 2272 |
+
# ==============================================================================
|
| 2273 |
+
advance/VelocityAndPrognostics.compute_gradients_module = @ToModalDiffOperators
|
| 2274 |
+
advance/VelocityAndPrognostics.fields_to_include = \
|
| 2275 |
+
['divergence',
|
| 2276 |
+
'vorticity',
|
| 2277 |
+
'u',
|
| 2278 |
+
'v',
|
| 2279 |
+
'temperature_variation',
|
| 2280 |
+
'log_surface_pressure',
|
| 2281 |
+
'specific_humidity',
|
| 2282 |
+
'specific_cloud_liquid_water_content',
|
| 2283 |
+
'specific_cloud_ice_water_content']
|
| 2284 |
+
advance/VelocityAndPrognostics.name = None
|
| 2285 |
+
|
| 2286 |
+
# Parameters for embedding_model/VelocityAndPrognostics:
|
| 2287 |
+
# ==============================================================================
|
| 2288 |
+
embedding_model/VelocityAndPrognostics.fields_to_include = \
|
| 2289 |
+
['divergence',
|
| 2290 |
+
'vorticity',
|
| 2291 |
+
'u',
|
| 2292 |
+
'v',
|
| 2293 |
+
'temperature_variation',
|
| 2294 |
+
'specific_humidity',
|
| 2295 |
+
'specific_cloud_liquid_water_content',
|
| 2296 |
+
'specific_cloud_ice_water_content']
|
| 2297 |
+
embedding_model/VelocityAndPrognostics.name = None
|
| 2298 |
+
|
| 2299 |
+
# Parameters for encoder_data/VelocityAndPrognostics:
|
| 2300 |
+
# ==============================================================================
|
| 2301 |
+
encoder_data/VelocityAndPrognostics.compute_gradients_module = \
|
| 2302 |
+
@ToModalDiffOperators
|
| 2303 |
+
encoder_data/VelocityAndPrognostics.fields_to_include = \
|
| 2304 |
+
['u',
|
| 2305 |
+
'v',
|
| 2306 |
+
't',
|
| 2307 |
+
'z',
|
| 2308 |
+
'specific_humidity',
|
| 2309 |
+
'specific_cloud_liquid_water_content',
|
| 2310 |
+
'specific_cloud_ice_water_content']
|
| 2311 |
+
encoder_data/VelocityAndPrognostics.name = None
|
| 2312 |
+
|
| 2313 |
+
# Parameters for model/VelocityAndPrognostics:
|
| 2314 |
+
# ==============================================================================
|
| 2315 |
+
model/VelocityAndPrognostics.fields_to_include = None
|
| 2316 |
+
model/VelocityAndPrognostics.name = None
|
| 2317 |
+
|
| 2318 |
+
# Parameters for VerticalConvTower:
|
| 2319 |
+
# ==============================================================================
|
| 2320 |
+
VerticalConvTower.activate_final = False
|
| 2321 |
+
VerticalConvTower.activation = %ACTIVATION
|
| 2322 |
+
VerticalConvTower.channels = [64, 64, 64, 64]
|
| 2323 |
+
VerticalConvTower.checkpoint_tower = True
|
| 2324 |
+
VerticalConvTower.kernel_shape = 5
|
| 2325 |
+
VerticalConvTower.name = None
|
| 2326 |
+
VerticalConvTower.with_bias = True
|
| 2327 |
+
|
| 2328 |
+
# Parameters for WhirlModel:
|
| 2329 |
+
# ==============================================================================
|
| 2330 |
+
WhirlModel.from_xarray_fn = @xarray_to_state_and_dynamic_covariate_data
|
| 2331 |
+
WhirlModel.model_cls = @StochasticModularStepModel
|
| 2332 |
+
WhirlModel.to_xarray_fn = @data_to_xarray_with_renaming
|
| 2333 |
+
|
| 2334 |
+
# Parameters for xarray_to_data_with_renaming:
|
| 2335 |
+
# ==============================================================================
|
| 2336 |
+
xarray_to_data_with_renaming.renaming_dict = \
|
| 2337 |
+
{'geopotential': 'z',
|
| 2338 |
+
'latitude': 'lat',
|
| 2339 |
+
'longitude': 'lon',
|
| 2340 |
+
'temperature': 't',
|
| 2341 |
+
'u_component_of_wind': 'u',
|
| 2342 |
+
'v_component_of_wind': 'v'}
|
| 2343 |
+
xarray_to_data_with_renaming.xarray_to_data_fn = @xarray_to_weatherbench_data
|
| 2344 |
+
|
| 2345 |
+
# Parameters for xarray_to_dynamic_covariate_data:
|
| 2346 |
+
# ==============================================================================
|
| 2347 |
+
xarray_to_dynamic_covariate_data.covariates_to_include = \
|
| 2348 |
+
('sea_ice_cover', 'sea_surface_temperature')
|
| 2349 |
+
|
| 2350 |
+
# Parameters for xarray_to_state_and_dynamic_covariate_data:
|
| 2351 |
+
# ==============================================================================
|
| 2352 |
+
xarray_to_state_and_dynamic_covariate_data.values = 'values'
|
| 2353 |
+
xarray_to_state_and_dynamic_covariate_data.xarray_to_dynamic_covariate_data_fn = \
|
| 2354 |
+
@xarray_to_dynamic_covariate_data
|
| 2355 |
+
xarray_to_state_and_dynamic_covariate_data.xarray_to_state_data_fn = \
|
| 2356 |
+
@xarray_to_data_with_renaming
|
| 2357 |
+
|
| 2358 |
+
# Parameters for xarray_to_weatherbench_data:
|
| 2359 |
+
# ==============================================================================
|
| 2360 |
+
xarray_to_weatherbench_data.diagnostics_to_include = ()
|
| 2361 |
+
xarray_to_weatherbench_data.tracers_to_include = \
|
| 2362 |
+
('specific_humidity',
|
| 2363 |
+
'specific_cloud_liquid_water_content',
|
| 2364 |
+
'specific_cloud_ice_water_content')
|
| 2365 |
+
|
| 2366 |
+
# Parameters for ZerosRandomField:
|
| 2367 |
+
# ==============================================================================
|
| 2368 |
+
ZerosRandomField.prefer_nodal = True
|
model/reference_code/paper_configs/deterministic_1_4_deg.gin
ADDED
|
@@ -0,0 +1,2371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Macros:
|
| 2 |
+
# ==============================================================================
|
| 3 |
+
ACTIVATION = @gelu
|
| 4 |
+
CORRECTOR_MODULE = @CustomCoordsCorrector
|
| 5 |
+
CORRECTOR_SCALE = 0.01
|
| 6 |
+
DATA_FILTER_ATTENUATION = 0.0
|
| 7 |
+
DYCORE_FILTER_ORDER = 3
|
| 8 |
+
DYCORE_GRID = @GridWithWavenumbers()
|
| 9 |
+
DYCORE_INTEGRATOR = @imex_rk_sil3
|
| 10 |
+
DYCORE_TAU = '120 minutes'
|
| 11 |
+
GLOBAL_OUT_SCALE = 0.02
|
| 12 |
+
LATENT_SIZE = 384
|
| 13 |
+
LAYER_SIZE = 384
|
| 14 |
+
N_CNN_FEATURES = 32
|
| 15 |
+
N_INNER_DYCORE_STEPS = 5
|
| 16 |
+
N_SIGMA_LAYERS = 32
|
| 17 |
+
N_TO_CLIP = 1
|
| 18 |
+
NUM_BLOCKS = 5
|
| 19 |
+
NUM_SUBSTEPS = 2
|
| 20 |
+
PARAMETERIZATION_FILTER = @ml/SequentialStepFilter
|
| 21 |
+
POSITIONAL_LATENT_SIZE = 32
|
| 22 |
+
STABILITY_TAU = '8 minutes'
|
| 23 |
+
SURFACE_MODEL_LATENT_SIZE = 8
|
| 24 |
+
SURFACE_MODEL_LAYER_SIZE = 8
|
| 25 |
+
SURFACE_MODEL_OUTPUT_SIZE = 8
|
| 26 |
+
|
| 27 |
+
# Parameters for decode/ColumnTower:
|
| 28 |
+
# ==============================================================================
|
| 29 |
+
decode/ColumnTower.checkpoint_tower = False
|
| 30 |
+
decode/ColumnTower.column_net_factory = @decode/MlpUniform
|
| 31 |
+
decode/ColumnTower.name = 'decode_tower'
|
| 32 |
+
|
| 33 |
+
# Parameters for encode/ColumnTower:
|
| 34 |
+
# ==============================================================================
|
| 35 |
+
encode/ColumnTower.checkpoint_tower = False
|
| 36 |
+
encode/ColumnTower.column_net_factory = @encode/MlpUniform
|
| 37 |
+
encode/ColumnTower.name = 'encode_tower'
|
| 38 |
+
|
| 39 |
+
# Parameters for process/ColumnTower:
|
| 40 |
+
# ==============================================================================
|
| 41 |
+
process/ColumnTower.checkpoint_tower = False
|
| 42 |
+
process/ColumnTower.column_net_factory = @process/MlpUniform
|
| 43 |
+
process/ColumnTower.name = 'process_tower'
|
| 44 |
+
|
| 45 |
+
# Parameters for surface_model_decode/ColumnTower:
|
| 46 |
+
# ==============================================================================
|
| 47 |
+
surface_model_decode/ColumnTower.checkpoint_tower = False
|
| 48 |
+
surface_model_decode/ColumnTower.column_net_factory = \
|
| 49 |
+
@surface_model_decode/MlpUniform
|
| 50 |
+
surface_model_decode/ColumnTower.name = 'surface_model_decode_tower'
|
| 51 |
+
|
| 52 |
+
# Parameters for surface_model_encode/ColumnTower:
|
| 53 |
+
# ==============================================================================
|
| 54 |
+
surface_model_encode/ColumnTower.checkpoint_tower = False
|
| 55 |
+
surface_model_encode/ColumnTower.column_net_factory = \
|
| 56 |
+
@surface_model_encode/MlpUniform
|
| 57 |
+
surface_model_encode/ColumnTower.name = 'surface_model_encode_tower'
|
| 58 |
+
|
| 59 |
+
# Parameters for surface_model_process/ColumnTower:
|
| 60 |
+
# ==============================================================================
|
| 61 |
+
surface_model_process/ColumnTower.checkpoint_tower = False
|
| 62 |
+
surface_model_process/ColumnTower.column_net_factory = \
|
| 63 |
+
@surface_model_process/MlpUniform
|
| 64 |
+
surface_model_process/ColumnTower.name = 'surface_model_process_tower'
|
| 65 |
+
|
| 66 |
+
# Parameters for advance/CombinedFeatures:
|
| 67 |
+
# ==============================================================================
|
| 68 |
+
advance/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 69 |
+
advance/CombinedFeatures.feature_modules = \
|
| 70 |
+
(@EmbeddingSurfaceFeatures,
|
| 71 |
+
@EmbeddingVolumeFeatures,
|
| 72 |
+
@PressureFeatures,
|
| 73 |
+
@RadiationFeatures,
|
| 74 |
+
@LatitudeFeatures,
|
| 75 |
+
@advance/VelocityAndPrognostics,
|
| 76 |
+
@MemoryVelocityAndValues,
|
| 77 |
+
@with_grads/FloatDataFeatures,
|
| 78 |
+
@without_grads/FloatDataFeatures,
|
| 79 |
+
@LearnedPositionalFeatures)
|
| 80 |
+
advance/CombinedFeatures.features_to_exclude = ()
|
| 81 |
+
advance/CombinedFeatures.features_transform_module = @advance/SequentialTransform
|
| 82 |
+
advance/CombinedFeatures.name = None
|
| 83 |
+
|
| 84 |
+
# Parameters for decoder_model/CombinedFeatures:
|
| 85 |
+
# ==============================================================================
|
| 86 |
+
decoder_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 87 |
+
decoder_model/CombinedFeatures.feature_modules = \
|
| 88 |
+
(@RadiationFeatures,
|
| 89 |
+
@LatitudeFeatures,
|
| 90 |
+
@model/VelocityAndPrognostics,
|
| 91 |
+
@with_grads/FloatDataFeatures,
|
| 92 |
+
@without_grads/FloatDataFeatures,
|
| 93 |
+
@LearnedPositionalFeatures)
|
| 94 |
+
decoder_model/CombinedFeatures.features_to_exclude = ()
|
| 95 |
+
decoder_model/CombinedFeatures.features_transform_module = \
|
| 96 |
+
@decoder_model/SequentialTransform
|
| 97 |
+
decoder_model/CombinedFeatures.name = None
|
| 98 |
+
|
| 99 |
+
# Parameters for embedding_model/CombinedFeatures:
|
| 100 |
+
# ==============================================================================
|
| 101 |
+
embedding_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 102 |
+
embedding_model/CombinedFeatures.feature_modules = \
|
| 103 |
+
(@embedding_model/VelocityAndPrognostics, @PressureFeatures)
|
| 104 |
+
embedding_model/CombinedFeatures.features_to_exclude = ()
|
| 105 |
+
embedding_model/CombinedFeatures.features_transform_module = \
|
| 106 |
+
@embedding_model/ShiftAndNormalize
|
| 107 |
+
embedding_model/CombinedFeatures.name = None
|
| 108 |
+
|
| 109 |
+
# Parameters for encoder_data/CombinedFeatures:
|
| 110 |
+
# ==============================================================================
|
| 111 |
+
encoder_data/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 112 |
+
encoder_data/CombinedFeatures.feature_modules = \
|
| 113 |
+
(@RadiationFeatures,
|
| 114 |
+
@LatitudeFeatures,
|
| 115 |
+
@encoder_data/VelocityAndPrognostics,
|
| 116 |
+
@with_grads/FloatDataFeatures,
|
| 117 |
+
@without_grads/FloatDataFeatures,
|
| 118 |
+
@LearnedPositionalFeatures)
|
| 119 |
+
encoder_data/CombinedFeatures.features_to_exclude = ()
|
| 120 |
+
encoder_data/CombinedFeatures.features_transform_module = \
|
| 121 |
+
@encoder_data/SequentialTransform
|
| 122 |
+
encoder_data/CombinedFeatures.name = None
|
| 123 |
+
|
| 124 |
+
# Parameters for sea_model/CombinedFeatures:
|
| 125 |
+
# ==============================================================================
|
| 126 |
+
sea_model/CombinedFeatures.feature_module_names_to_exclude = ()
|
| 127 |
+
sea_model/CombinedFeatures.feature_modules = (@sea_model/ForcingFeatures,)
|
| 128 |
+
sea_model/CombinedFeatures.features_to_exclude = ()
|
| 129 |
+
sea_model/CombinedFeatures.features_transform_module = @sea_model/ShiftAndNormalize
|
| 130 |
+
sea_model/CombinedFeatures.name = None
|
| 131 |
+
|
| 132 |
+
# Parameters for coordinate_system_from_dataset:
|
| 133 |
+
# ==============================================================================
|
| 134 |
+
coordinate_system_from_dataset.spherical_harmonics_impl = \
|
| 135 |
+
@RealSphericalHarmonicsWithZeroImag
|
| 136 |
+
coordinate_system_from_dataset.truncation = 'LINEAR'
|
| 137 |
+
|
| 138 |
+
# Parameters for CoordinateSystem:
|
| 139 |
+
# ==============================================================================
|
| 140 |
+
CoordinateSystem.horizontal = @GridTL127()
|
| 141 |
+
CoordinateSystem.vertical = @SigmaCoordinatesEquidistant()
|
| 142 |
+
|
| 143 |
+
# Parameters for custom_corrds/CoordinateSystem:
|
| 144 |
+
# ==============================================================================
|
| 145 |
+
custom_corrds/CoordinateSystem.horizontal = %DYCORE_GRID
|
| 146 |
+
custom_corrds/CoordinateSystem.vertical = @SigmaCoordinatesEquidistant()
|
| 147 |
+
|
| 148 |
+
# Parameters for CustomCoordsCorrector:
|
| 149 |
+
# ==============================================================================
|
| 150 |
+
CustomCoordsCorrector.corrector_module = @DycoreWithPhysicsCorrector
|
| 151 |
+
CustomCoordsCorrector.custom_coords = @custom_corrds/CoordinateSystem()
|
| 152 |
+
CustomCoordsCorrector.name = None
|
| 153 |
+
|
| 154 |
+
# Parameters for data_to_xarray_with_renaming:
|
| 155 |
+
# ==============================================================================
|
| 156 |
+
data_to_xarray_with_renaming.additional_coords = None
|
| 157 |
+
data_to_xarray_with_renaming.attrs = None
|
| 158 |
+
data_to_xarray_with_renaming.renaming_dict = \
|
| 159 |
+
{'geopotential': 'z',
|
| 160 |
+
'latitude': 'lat',
|
| 161 |
+
'longitude': 'lon',
|
| 162 |
+
'temperature': 't',
|
| 163 |
+
'u_component_of_wind': 'u',
|
| 164 |
+
'v_component_of_wind': 'v'}
|
| 165 |
+
data_to_xarray_with_renaming.sample_ids = None
|
| 166 |
+
data_to_xarray_with_renaming.to_xarray_fn = @primitive_eq_to_xarray
|
| 167 |
+
|
| 168 |
+
# Parameters for divergence/DataExponentialFilter:
|
| 169 |
+
# ==============================================================================
|
| 170 |
+
divergence/DataExponentialFilter.attenuation = 14.0
|
| 171 |
+
divergence/DataExponentialFilter.cutoff = 0.62
|
| 172 |
+
divergence/DataExponentialFilter.name = None
|
| 173 |
+
divergence/DataExponentialFilter.order = 2
|
| 174 |
+
|
| 175 |
+
# Parameters for lsp/DataExponentialFilter:
|
| 176 |
+
# ==============================================================================
|
| 177 |
+
lsp/DataExponentialFilter.attenuation = 8
|
| 178 |
+
lsp/DataExponentialFilter.cutoff = 0.82
|
| 179 |
+
lsp/DataExponentialFilter.name = None
|
| 180 |
+
lsp/DataExponentialFilter.order = 1
|
| 181 |
+
|
| 182 |
+
# Parameters for orography/DataExponentialFilter:
|
| 183 |
+
# ==============================================================================
|
| 184 |
+
orography/DataExponentialFilter.attenuation = %DATA_FILTER_ATTENUATION
|
| 185 |
+
orography/DataExponentialFilter.cutoff = 0
|
| 186 |
+
orography/DataExponentialFilter.name = None
|
| 187 |
+
orography/DataExponentialFilter.order = 1
|
| 188 |
+
|
| 189 |
+
# Parameters for q/DataExponentialFilter:
|
| 190 |
+
# ==============================================================================
|
| 191 |
+
q/DataExponentialFilter.attenuation = 14
|
| 192 |
+
q/DataExponentialFilter.cutoff = 0.62
|
| 193 |
+
q/DataExponentialFilter.name = None
|
| 194 |
+
q/DataExponentialFilter.order = 2
|
| 195 |
+
|
| 196 |
+
# Parameters for temperature_variation/DataExponentialFilter:
|
| 197 |
+
# ==============================================================================
|
| 198 |
+
temperature_variation/DataExponentialFilter.attenuation = 13.0
|
| 199 |
+
temperature_variation/DataExponentialFilter.cutoff = 0.45
|
| 200 |
+
temperature_variation/DataExponentialFilter.name = None
|
| 201 |
+
temperature_variation/DataExponentialFilter.order = 3
|
| 202 |
+
|
| 203 |
+
# Parameters for vorticity/DataExponentialFilter:
|
| 204 |
+
# ==============================================================================
|
| 205 |
+
vorticity/DataExponentialFilter.attenuation = 14
|
| 206 |
+
vorticity/DataExponentialFilter.cutoff = 0.62
|
| 207 |
+
vorticity/DataExponentialFilter.name = None
|
| 208 |
+
vorticity/DataExponentialFilter.order = 2
|
| 209 |
+
|
| 210 |
+
# Parameters for DataNoFilter:
|
| 211 |
+
# ==============================================================================
|
| 212 |
+
DataNoFilter.name = None
|
| 213 |
+
|
| 214 |
+
# Parameters for DimensionalLearnedPrimitiveToWeatherbenchDecoder:
|
| 215 |
+
# ==============================================================================
|
| 216 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.correction_transform_module = \
|
| 217 |
+
@decoder/SequentialTransform
|
| 218 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.inputs_to_units_mapping = \
|
| 219 |
+
{'sim_time': 'dimensionless',
|
| 220 |
+
't': 'kelvin',
|
| 221 |
+
'tracers': {'specific_cloud_ice_water_content': 'dimensionless',
|
| 222 |
+
'specific_cloud_liquid_water_content': 'dimensionless',
|
| 223 |
+
'specific_humidity': 'dimensionless'},
|
| 224 |
+
'u': 'meter / second',
|
| 225 |
+
'v': 'meter / second',
|
| 226 |
+
'z': 'm**2 s**-2'}
|
| 227 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.modal_to_nodal_data_features_module = \
|
| 228 |
+
@NullFeatures
|
| 229 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.modal_to_nodal_model_features_module = \
|
| 230 |
+
@decoder_model/CombinedFeatures
|
| 231 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.name = None
|
| 232 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.nodal_mapping_module = \
|
| 233 |
+
@NodalMapping
|
| 234 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.orography_module = \
|
| 235 |
+
@LearnedOrography
|
| 236 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.prediction_mask = \
|
| 237 |
+
{'sim_time': False,
|
| 238 |
+
't': True,
|
| 239 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 240 |
+
'specific_cloud_liquid_water_content': True,
|
| 241 |
+
'specific_humidity': True},
|
| 242 |
+
'u': True,
|
| 243 |
+
'v': True,
|
| 244 |
+
'z': True}
|
| 245 |
+
DimensionalLearnedPrimitiveToWeatherbenchDecoder.time_axis = 0
|
| 246 |
+
|
| 247 |
+
# Parameters for DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder:
|
| 248 |
+
# ==============================================================================
|
| 249 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.correction_transform_module = \
|
| 250 |
+
@encode/SequentialTransform
|
| 251 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.inputs_to_units_mapping = \
|
| 252 |
+
{'sim_time': 'dimensionless',
|
| 253 |
+
't': 'kelvin',
|
| 254 |
+
'tracers': {'specific_cloud_ice_water_content': 'dimensionless',
|
| 255 |
+
'specific_cloud_liquid_water_content': 'dimensionless',
|
| 256 |
+
'specific_humidity': 'dimensionless'},
|
| 257 |
+
'u': 'meter / second',
|
| 258 |
+
'v': 'meter / second',
|
| 259 |
+
'z': 'm**2 s**-2'}
|
| 260 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.modal_to_nodal_data_features_module = \
|
| 261 |
+
@encoder_data/CombinedFeatures
|
| 262 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.modal_to_nodal_model_features_module = \
|
| 263 |
+
@NullFeatures
|
| 264 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.name = None
|
| 265 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.nodal_mapping_module = \
|
| 266 |
+
@NodalMapping
|
| 267 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.orography_module = \
|
| 268 |
+
@LearnedOrography
|
| 269 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.prediction_mask = \
|
| 270 |
+
{'divergence': True,
|
| 271 |
+
'log_surface_pressure': True,
|
| 272 |
+
'sim_time': False,
|
| 273 |
+
'temperature_variation': True,
|
| 274 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 275 |
+
'specific_cloud_liquid_water_content': True,
|
| 276 |
+
'specific_humidity': True},
|
| 277 |
+
'vorticity': True}
|
| 278 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.time_axis = 0
|
| 279 |
+
DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder.transform_module = \
|
| 280 |
+
@EncoderCombinedTransform
|
| 281 |
+
|
| 282 |
+
# Parameters for DivCurlNeuralParameterization:
|
| 283 |
+
# ==============================================================================
|
| 284 |
+
DivCurlNeuralParameterization.filter_module = %PARAMETERIZATION_FILTER
|
| 285 |
+
DivCurlNeuralParameterization.modal_to_nodal_features_module = \
|
| 286 |
+
@advance/CombinedFeatures
|
| 287 |
+
DivCurlNeuralParameterization.name = None
|
| 288 |
+
DivCurlNeuralParameterization.nodal_mapping_module = @NodalMapping
|
| 289 |
+
DivCurlNeuralParameterization.prediction_mask = \
|
| 290 |
+
{'divergence': True,
|
| 291 |
+
'log_surface_pressure': False,
|
| 292 |
+
'sim_time': False,
|
| 293 |
+
'temperature_variation': True,
|
| 294 |
+
'tracers': {'specific_cloud_ice_water_content': True,
|
| 295 |
+
'specific_cloud_liquid_water_content': True,
|
| 296 |
+
'specific_humidity': True},
|
| 297 |
+
'vorticity': True}
|
| 298 |
+
DivCurlNeuralParameterization.tendency_transform_module = \
|
| 299 |
+
@div_curl_tendency_outputs/SequentialTransform
|
| 300 |
+
|
| 301 |
+
# Parameters for DycoreWithPhysicsCorrector:
|
| 302 |
+
# ==============================================================================
|
| 303 |
+
DycoreWithPhysicsCorrector.checkpoint_explicit_terms = True
|
| 304 |
+
DycoreWithPhysicsCorrector.dycore_equation_module = \
|
| 305 |
+
@MoistPrimitiveEquationsWithCloudMoisture
|
| 306 |
+
DycoreWithPhysicsCorrector.dycore_substeps = %N_INNER_DYCORE_STEPS
|
| 307 |
+
DycoreWithPhysicsCorrector.filter_module = @dycore/SequentialStepFilter
|
| 308 |
+
DycoreWithPhysicsCorrector.name = None
|
| 309 |
+
DycoreWithPhysicsCorrector.time_integrator = %DYCORE_INTEGRATOR
|
| 310 |
+
|
| 311 |
+
# Parameters for DynamicDataForcing:
|
| 312 |
+
# ==============================================================================
|
| 313 |
+
DynamicDataForcing.check_sim_time_errors = False
|
| 314 |
+
DynamicDataForcing.data_time_step = '12 hours'
|
| 315 |
+
DynamicDataForcing.dt_tolerance = '1 year'
|
| 316 |
+
DynamicDataForcing.inputs_to_units_mapping = \
|
| 317 |
+
{'sea_ice_cover': 'dimensionless',
|
| 318 |
+
'sea_surface_temperature': 'kelvin',
|
| 319 |
+
'sim_time': 'dimensionless'}
|
| 320 |
+
DynamicDataForcing.name = None
|
| 321 |
+
DynamicDataForcing.time_axis = 0
|
| 322 |
+
|
| 323 |
+
# Parameters for advance/EmbeddingSurfaceFeatures:
|
| 324 |
+
# ==============================================================================
|
| 325 |
+
advance/EmbeddingSurfaceFeatures.embedding_module = @NodalLandSeaIceEmbedding
|
| 326 |
+
advance/EmbeddingSurfaceFeatures.feature_name = 'surface_embedding'
|
| 327 |
+
advance/EmbeddingSurfaceFeatures.name = None
|
| 328 |
+
advance/EmbeddingSurfaceFeatures.output_size = %SURFACE_MODEL_OUTPUT_SIZE
|
| 329 |
+
|
| 330 |
+
# Parameters for advance/EmbeddingVolumeFeatures:
|
| 331 |
+
# ==============================================================================
|
| 332 |
+
advance/EmbeddingVolumeFeatures.embedding_module = @ModalToNodalEmbedding
|
| 333 |
+
advance/EmbeddingVolumeFeatures.feature_name = 'CNN1D'
|
| 334 |
+
advance/EmbeddingVolumeFeatures.name = None
|
| 335 |
+
advance/EmbeddingVolumeFeatures.output_size = %N_CNN_FEATURES
|
| 336 |
+
|
| 337 |
+
# Parameters for EncoderCombinedTransform:
|
| 338 |
+
# ==============================================================================
|
| 339 |
+
EncoderCombinedTransform.name = None
|
| 340 |
+
EncoderCombinedTransform.transforms = \
|
| 341 |
+
(@InputClipTransform, @EncoderFilterTransform)
|
| 342 |
+
|
| 343 |
+
# Parameters for EncoderFilterTransform:
|
| 344 |
+
# ==============================================================================
|
| 345 |
+
EncoderFilterTransform.filter_modules = (@PerVariableDataFilter,)
|
| 346 |
+
EncoderFilterTransform.name = None
|
| 347 |
+
|
| 348 |
+
# Parameters for EpdTower:
|
| 349 |
+
# ==============================================================================
|
| 350 |
+
EpdTower.decode_tower_factory = @decode/ColumnTower
|
| 351 |
+
EpdTower.encode_tower_factory = @encode/ColumnTower
|
| 352 |
+
EpdTower.final_activation = None
|
| 353 |
+
EpdTower.latent_size = %LATENT_SIZE
|
| 354 |
+
EpdTower.name = None
|
| 355 |
+
EpdTower.num_process_blocks = %NUM_BLOCKS
|
| 356 |
+
EpdTower.post_encode_activation = None
|
| 357 |
+
EpdTower.pre_decode_activation = None
|
| 358 |
+
EpdTower.process_tower_factory = @process/ColumnTower
|
| 359 |
+
|
| 360 |
+
# Parameters for surface_model/EpdTower:
|
| 361 |
+
# ==============================================================================
|
| 362 |
+
surface_model/EpdTower.decode_tower_factory = @surface_model_decode/ColumnTower
|
| 363 |
+
surface_model/EpdTower.encode_tower_factory = @surface_model_encode/ColumnTower
|
| 364 |
+
surface_model/EpdTower.final_activation = None
|
| 365 |
+
surface_model/EpdTower.latent_size = %SURFACE_MODEL_LATENT_SIZE
|
| 366 |
+
surface_model/EpdTower.name = None
|
| 367 |
+
surface_model/EpdTower.num_process_blocks = 1
|
| 368 |
+
surface_model/EpdTower.post_encode_activation = None
|
| 369 |
+
surface_model/EpdTower.pre_decode_activation = None
|
| 370 |
+
surface_model/EpdTower.process_tower_factory = @surface_model_process/ColumnTower
|
| 371 |
+
|
| 372 |
+
# Parameters for dycore/ExponentialFilter:
|
| 373 |
+
# ==============================================================================
|
| 374 |
+
dycore/ExponentialFilter.cutoff = 0
|
| 375 |
+
dycore/ExponentialFilter.name = None
|
| 376 |
+
dycore/ExponentialFilter.order = %DYCORE_FILTER_ORDER
|
| 377 |
+
dycore/ExponentialFilter.tau = %DYCORE_TAU
|
| 378 |
+
|
| 379 |
+
# Parameters for stability/ExponentialFilter:
|
| 380 |
+
# ==============================================================================
|
| 381 |
+
stability/ExponentialFilter.cutoff = 0.4
|
| 382 |
+
stability/ExponentialFilter.name = None
|
| 383 |
+
stability/ExponentialFilter.order = 6
|
| 384 |
+
stability/ExponentialFilter.tau = %STABILITY_TAU
|
| 385 |
+
|
| 386 |
+
# Parameters for FilteredCustomOrography:
|
| 387 |
+
# ==============================================================================
|
| 388 |
+
FilteredCustomOrography.filter_modules = (@orography/DataExponentialFilter,)
|
| 389 |
+
FilteredCustomOrography.name = None
|
| 390 |
+
FilteredCustomOrography.orography_data_path = None
|
| 391 |
+
FilteredCustomOrography.renaming_dict = {'latitude': 'lat', 'longitude': 'lon'}
|
| 392 |
+
|
| 393 |
+
# Parameters for with_grads/FloatDataFeatures:
|
| 394 |
+
# ==============================================================================
|
| 395 |
+
with_grads/FloatDataFeatures.compute_gradients_module = @ToModalDiffOperators
|
| 396 |
+
with_grads/FloatDataFeatures.covariate_data_path = None
|
| 397 |
+
with_grads/FloatDataFeatures.covariate_keys = ('geopotential_at_surface',)
|
| 398 |
+
with_grads/FloatDataFeatures.name = None
|
| 399 |
+
with_grads/FloatDataFeatures.renaming_dict = \
|
| 400 |
+
{'latitude': 'lat', 'longitude': 'lon'}
|
| 401 |
+
|
| 402 |
+
# Parameters for without_grads/FloatDataFeatures:
|
| 403 |
+
# ==============================================================================
|
| 404 |
+
without_grads/FloatDataFeatures.covariate_data_path = None
|
| 405 |
+
without_grads/FloatDataFeatures.covariate_keys = ('land_sea_mask',)
|
| 406 |
+
without_grads/FloatDataFeatures.name = None
|
| 407 |
+
without_grads/FloatDataFeatures.renaming_dict = \
|
| 408 |
+
{'latitude': 'lat', 'longitude': 'lon'}
|
| 409 |
+
|
| 410 |
+
# Parameters for sea_model/ForcingFeatures:
|
| 411 |
+
# ==============================================================================
|
| 412 |
+
sea_model/ForcingFeatures.forcing_to_include = ('sea_surface_temperature',)
|
| 413 |
+
sea_model/ForcingFeatures.name = None
|
| 414 |
+
|
| 415 |
+
# Parameters for gelu:
|
| 416 |
+
# ==============================================================================
|
| 417 |
+
gelu.approximate = True
|
| 418 |
+
|
| 419 |
+
# Parameters for GET_ATMOSPHERIC_SCALE:
|
| 420 |
+
# ==============================================================================
|
| 421 |
+
# None.
|
| 422 |
+
|
| 423 |
+
# Parameters for get_model_specs:
|
| 424 |
+
# ==============================================================================
|
| 425 |
+
get_model_specs.custom_coords = @CoordinateSystem()
|
| 426 |
+
get_model_specs.model_time_step = '1 hour'
|
| 427 |
+
get_model_specs.reference_datetime_str = None
|
| 428 |
+
get_model_specs.reference_temperature = \
|
| 429 |
+
[215.58614815,
|
| 430 |
+
211.47405876,
|
| 431 |
+
205.87815406,
|
| 432 |
+
206.40755302,
|
| 433 |
+
210.43452345,
|
| 434 |
+
214.5683887,
|
| 435 |
+
218.75303863,
|
| 436 |
+
223.23145107,
|
| 437 |
+
227.9710687,
|
| 438 |
+
232.85381503,
|
| 439 |
+
237.53588735,
|
| 440 |
+
242.05068293,
|
| 441 |
+
246.29986585,
|
| 442 |
+
250.14294113,
|
| 443 |
+
253.74839535,
|
| 444 |
+
256.98024283,
|
| 445 |
+
259.94441031,
|
| 446 |
+
262.7041158,
|
| 447 |
+
265.21752838,
|
| 448 |
+
267.62333985,
|
| 449 |
+
269.94462121,
|
| 450 |
+
272.10056439,
|
| 451 |
+
274.12518288,
|
| 452 |
+
275.99833711,
|
| 453 |
+
277.72759392,
|
| 454 |
+
279.3292128,
|
| 455 |
+
280.79178708,
|
| 456 |
+
282.13507065,
|
| 457 |
+
283.41832023,
|
| 458 |
+
284.7682506,
|
| 459 |
+
286.33945487,
|
| 460 |
+
288.06707666]
|
| 461 |
+
|
| 462 |
+
# Parameters for get_physics_specs:
|
| 463 |
+
# ==============================================================================
|
| 464 |
+
get_physics_specs.construct_fn = @primitive_eq_specs_constructor
|
| 465 |
+
|
| 466 |
+
# Parameters for GridTL127:
|
| 467 |
+
# ==============================================================================
|
| 468 |
+
GridTL127.spherical_harmonics_impl = @RealSphericalHarmonicsWithZeroImag
|
| 469 |
+
|
| 470 |
+
# Parameters for DYCORE_GRID/GridWithWavenumbers:
|
| 471 |
+
# ==============================================================================
|
| 472 |
+
DYCORE_GRID/GridWithWavenumbers.dealiasing = 'quadratic'
|
| 473 |
+
DYCORE_GRID/GridWithWavenumbers.latitude_spacing = 'gauss'
|
| 474 |
+
DYCORE_GRID/GridWithWavenumbers.longitude_offset = 0.0
|
| 475 |
+
DYCORE_GRID/GridWithWavenumbers.longitude_wavenumbers = 126
|
| 476 |
+
DYCORE_GRID/GridWithWavenumbers.radius = None
|
| 477 |
+
DYCORE_GRID/GridWithWavenumbers.spherical_harmonics_impl = \
|
| 478 |
+
@RealSphericalHarmonicsWithZeroImag
|
| 479 |
+
|
| 480 |
+
# Parameters for advance/IdentityTransform:
|
| 481 |
+
# ==============================================================================
|
| 482 |
+
advance/IdentityTransform.name = None
|
| 483 |
+
|
| 484 |
+
# Parameters for sea_model/IdentityTransform:
|
| 485 |
+
# ==============================================================================
|
| 486 |
+
sea_model/IdentityTransform.name = None
|
| 487 |
+
|
| 488 |
+
# Parameters for imex_rk_sil3:
|
| 489 |
+
# ==============================================================================
|
| 490 |
+
# None.
|
| 491 |
+
|
| 492 |
+
# Parameters for InputClipTransform:
|
| 493 |
+
# ==============================================================================
|
| 494 |
+
InputClipTransform.name = None
|
| 495 |
+
InputClipTransform.wavenumbers_to_clip = %N_TO_CLIP
|
| 496 |
+
|
| 497 |
+
# Parameters for advance/InverseLevelScale:
|
| 498 |
+
# ==============================================================================
|
| 499 |
+
advance/InverseLevelScale.keys_to_scale = \
|
| 500 |
+
['specific_humidity',
|
| 501 |
+
'specific_humidity_del2',
|
| 502 |
+
'specific_humidity_dlat',
|
| 503 |
+
'specific_humidity_dlon']
|
| 504 |
+
advance/InverseLevelScale.name = None
|
| 505 |
+
advance/InverseLevelScale.scales = \
|
| 506 |
+
[8.824e-05,
|
| 507 |
+
7.105e-05,
|
| 508 |
+
0.0001046,
|
| 509 |
+
0.0001851,
|
| 510 |
+
0.0007562,
|
| 511 |
+
0.002625,
|
| 512 |
+
0.007185,
|
| 513 |
+
0.01554,
|
| 514 |
+
0.02886,
|
| 515 |
+
0.04613,
|
| 516 |
+
0.07032,
|
| 517 |
+
0.09901,
|
| 518 |
+
0.1344,
|
| 519 |
+
0.175,
|
| 520 |
+
0.2226,
|
| 521 |
+
0.2802,
|
| 522 |
+
0.3438,
|
| 523 |
+
0.4144,
|
| 524 |
+
0.4684,
|
| 525 |
+
0.5257,
|
| 526 |
+
0.5895,
|
| 527 |
+
0.6645,
|
| 528 |
+
0.7505,
|
| 529 |
+
0.8483,
|
| 530 |
+
0.9499,
|
| 531 |
+
1.053,
|
| 532 |
+
1.159,
|
| 533 |
+
1.274,
|
| 534 |
+
1.399,
|
| 535 |
+
1.552,
|
| 536 |
+
1.709,
|
| 537 |
+
1.791]
|
| 538 |
+
|
| 539 |
+
# Parameters for decoder_model/InverseLevelScale:
|
| 540 |
+
# ==============================================================================
|
| 541 |
+
decoder_model/InverseLevelScale.keys_to_scale = \
|
| 542 |
+
['specific_humidity',
|
| 543 |
+
'specific_humidity_del2',
|
| 544 |
+
'specific_humidity_dlat',
|
| 545 |
+
'specific_humidity_dlon']
|
| 546 |
+
decoder_model/InverseLevelScale.name = None
|
| 547 |
+
decoder_model/InverseLevelScale.scales = \
|
| 548 |
+
[8.824e-05,
|
| 549 |
+
7.105e-05,
|
| 550 |
+
0.0001046,
|
| 551 |
+
0.0001851,
|
| 552 |
+
0.0007562,
|
| 553 |
+
0.002625,
|
| 554 |
+
0.007185,
|
| 555 |
+
0.01554,
|
| 556 |
+
0.02886,
|
| 557 |
+
0.04613,
|
| 558 |
+
0.07032,
|
| 559 |
+
0.09901,
|
| 560 |
+
0.1344,
|
| 561 |
+
0.175,
|
| 562 |
+
0.2226,
|
| 563 |
+
0.2802,
|
| 564 |
+
0.3438,
|
| 565 |
+
0.4144,
|
| 566 |
+
0.4684,
|
| 567 |
+
0.5257,
|
| 568 |
+
0.5895,
|
| 569 |
+
0.6645,
|
| 570 |
+
0.7505,
|
| 571 |
+
0.8483,
|
| 572 |
+
0.9499,
|
| 573 |
+
1.053,
|
| 574 |
+
1.159,
|
| 575 |
+
1.274,
|
| 576 |
+
1.399,
|
| 577 |
+
1.552,
|
| 578 |
+
1.709,
|
| 579 |
+
1.791]
|
| 580 |
+
|
| 581 |
+
# Parameters for encoder_data/InverseLevelScale:
|
| 582 |
+
# ==============================================================================
|
| 583 |
+
encoder_data/InverseLevelScale.keys_to_scale = \
|
| 584 |
+
['specific_humidity',
|
| 585 |
+
'specific_humidity_del2',
|
| 586 |
+
'specific_humidity_dlat',
|
| 587 |
+
'specific_humidity_dlon']
|
| 588 |
+
encoder_data/InverseLevelScale.name = None
|
| 589 |
+
encoder_data/InverseLevelScale.scales = \
|
| 590 |
+
[3.818e-05,
|
| 591 |
+
5.988e-05,
|
| 592 |
+
7.108e-05,
|
| 593 |
+
8.204e-05,
|
| 594 |
+
8.841e-05,
|
| 595 |
+
9.115e-05,
|
| 596 |
+
7.736e-05,
|
| 597 |
+
7.213e-05,
|
| 598 |
+
8.284e-05,
|
| 599 |
+
9.873e-05,
|
| 600 |
+
0.0001438,
|
| 601 |
+
0.0002976,
|
| 602 |
+
0.001013,
|
| 603 |
+
0.002753,
|
| 604 |
+
0.006139,
|
| 605 |
+
0.01182,
|
| 606 |
+
0.02014,
|
| 607 |
+
0.04587,
|
| 608 |
+
0.08568,
|
| 609 |
+
0.1393,
|
| 610 |
+
0.2085,
|
| 611 |
+
0.2993,
|
| 612 |
+
0.4066,
|
| 613 |
+
0.4914,
|
| 614 |
+
0.5848,
|
| 615 |
+
0.7112,
|
| 616 |
+
0.8588,
|
| 617 |
+
0.936,
|
| 618 |
+
1.013,
|
| 619 |
+
1.092,
|
| 620 |
+
1.173,
|
| 621 |
+
1.257,
|
| 622 |
+
1.346,
|
| 623 |
+
1.452,
|
| 624 |
+
1.58,
|
| 625 |
+
1.677,
|
| 626 |
+
1.713]
|
| 627 |
+
|
| 628 |
+
# Parameters for decoder/InverseShiftAndNormalize:
|
| 629 |
+
# ==============================================================================
|
| 630 |
+
decoder/InverseShiftAndNormalize.global_scale = %CORRECTOR_SCALE
|
| 631 |
+
decoder/InverseShiftAndNormalize.name = None
|
| 632 |
+
decoder/InverseShiftAndNormalize.scales = \
|
| 633 |
+
{'sim_time': 0.0,
|
| 634 |
+
't': 28.0,
|
| 635 |
+
'tracers': {'specific_cloud_ice_water_content': 7.538e-06,
|
| 636 |
+
'specific_cloud_liquid_water_content': 1.979e-05,
|
| 637 |
+
'specific_humidity': 0.003487},
|
| 638 |
+
'u': 0.01933,
|
| 639 |
+
'v': 0.01033,
|
| 640 |
+
'z': 0.1496}
|
| 641 |
+
decoder/InverseShiftAndNormalize.shifts = \
|
| 642 |
+
{'sim_time': 0.0,
|
| 643 |
+
't': 0.0,
|
| 644 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 645 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 646 |
+
'specific_humidity': 0.0},
|
| 647 |
+
'u': 0.0,
|
| 648 |
+
'v': 0.0,
|
| 649 |
+
'z': 0.0}
|
| 650 |
+
|
| 651 |
+
# Parameters for div_curl_tendency_outputs/InverseShiftAndNormalize:
|
| 652 |
+
# ==============================================================================
|
| 653 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.global_scale = %GLOBAL_OUT_SCALE
|
| 654 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.name = None
|
| 655 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.scales = \
|
| 656 |
+
{'log_surface_pressure': 0.05008,
|
| 657 |
+
'sim_time': 0.0,
|
| 658 |
+
'temperature_variation': 33.85,
|
| 659 |
+
'tracers': {'specific_cloud_ice_water_content': 4.471e-05,
|
| 660 |
+
'specific_cloud_liquid_water_content': 8.884e-05,
|
| 661 |
+
'specific_humidity': 0.00608},
|
| 662 |
+
'u': 0.05839,
|
| 663 |
+
'v': 0.05138}
|
| 664 |
+
div_curl_tendency_outputs/InverseShiftAndNormalize.shifts = \
|
| 665 |
+
{'log_surface_pressure': 0.0,
|
| 666 |
+
'sim_time': 0.0,
|
| 667 |
+
'temperature_variation': 0.0,
|
| 668 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 669 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 670 |
+
'specific_humidity': 0.0},
|
| 671 |
+
'u': 0.0,
|
| 672 |
+
'v': 0.0}
|
| 673 |
+
|
| 674 |
+
# Parameters for encoder/InverseShiftAndNormalize:
|
| 675 |
+
# ==============================================================================
|
| 676 |
+
encoder/InverseShiftAndNormalize.global_scale = %CORRECTOR_SCALE
|
| 677 |
+
encoder/InverseShiftAndNormalize.name = None
|
| 678 |
+
encoder/InverseShiftAndNormalize.scales = \
|
| 679 |
+
{'divergence': 0.08243,
|
| 680 |
+
'log_surface_pressure': 0.1123,
|
| 681 |
+
'sim_time': 0.0,
|
| 682 |
+
'temperature_variation': 14.99,
|
| 683 |
+
'tracers': {'specific_cloud_ice_water_content': 9.111e-06,
|
| 684 |
+
'specific_cloud_liquid_water_content': 1.897e-05,
|
| 685 |
+
'specific_humidity': 0.003298},
|
| 686 |
+
'vorticity': 0.2579}
|
| 687 |
+
encoder/InverseShiftAndNormalize.shifts = \
|
| 688 |
+
{'divergence': 0.0,
|
| 689 |
+
'log_surface_pressure': 0.0,
|
| 690 |
+
'sim_time': 0.0,
|
| 691 |
+
'temperature_variation': 0.0,
|
| 692 |
+
'tracers': {'specific_cloud_ice_water_content': 0.0,
|
| 693 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 694 |
+
'specific_humidity': 0.0},
|
| 695 |
+
'vorticity': 0.0}
|
| 696 |
+
|
| 697 |
+
# Parameters for advance/LatitudeFeatures:
|
| 698 |
+
# ==============================================================================
|
| 699 |
+
advance/LatitudeFeatures.name = None
|
| 700 |
+
|
| 701 |
+
# Parameters for decoder_model/LatitudeFeatures:
|
| 702 |
+
# ==============================================================================
|
| 703 |
+
decoder_model/LatitudeFeatures.name = None
|
| 704 |
+
|
| 705 |
+
# Parameters for encoder_data/LatitudeFeatures:
|
| 706 |
+
# ==============================================================================
|
| 707 |
+
encoder_data/LatitudeFeatures.name = None
|
| 708 |
+
|
| 709 |
+
# Parameters for LearnedOrography:
|
| 710 |
+
# ==============================================================================
|
| 711 |
+
LearnedOrography.base_orography_module = @FilteredCustomOrography
|
| 712 |
+
LearnedOrography.correction_scale = 2e-06
|
| 713 |
+
LearnedOrography.name = None
|
| 714 |
+
|
| 715 |
+
# Parameters for advance/LearnedPositionalFeatures:
|
| 716 |
+
# ==============================================================================
|
| 717 |
+
advance/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 718 |
+
advance/LearnedPositionalFeatures.name = None
|
| 719 |
+
advance/LearnedPositionalFeatures.scale = 1.0
|
| 720 |
+
|
| 721 |
+
# Parameters for decoder_model/LearnedPositionalFeatures:
|
| 722 |
+
# ==============================================================================
|
| 723 |
+
decoder_model/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 724 |
+
decoder_model/LearnedPositionalFeatures.name = None
|
| 725 |
+
decoder_model/LearnedPositionalFeatures.scale = 1.0
|
| 726 |
+
|
| 727 |
+
# Parameters for encoder_data/LearnedPositionalFeatures:
|
| 728 |
+
# ==============================================================================
|
| 729 |
+
encoder_data/LearnedPositionalFeatures.latent_size = %POSITIONAL_LATENT_SIZE
|
| 730 |
+
encoder_data/LearnedPositionalFeatures.name = None
|
| 731 |
+
encoder_data/LearnedPositionalFeatures.scale = 1.0
|
| 732 |
+
|
| 733 |
+
# Parameters for decoder/LevelScale:
|
| 734 |
+
# ==============================================================================
|
| 735 |
+
decoder/LevelScale.keys_to_scale = ['specific_humidity']
|
| 736 |
+
decoder/LevelScale.name = None
|
| 737 |
+
decoder/LevelScale.scales = \
|
| 738 |
+
[3.818e-05,
|
| 739 |
+
5.988e-05,
|
| 740 |
+
7.108e-05,
|
| 741 |
+
8.204e-05,
|
| 742 |
+
8.841e-05,
|
| 743 |
+
9.115e-05,
|
| 744 |
+
7.736e-05,
|
| 745 |
+
7.213e-05,
|
| 746 |
+
8.284e-05,
|
| 747 |
+
9.873e-05,
|
| 748 |
+
0.0001438,
|
| 749 |
+
0.0002976,
|
| 750 |
+
0.001013,
|
| 751 |
+
0.002753,
|
| 752 |
+
0.006139,
|
| 753 |
+
0.01182,
|
| 754 |
+
0.02014,
|
| 755 |
+
0.04587,
|
| 756 |
+
0.08568,
|
| 757 |
+
0.1393,
|
| 758 |
+
0.2085,
|
| 759 |
+
0.2993,
|
| 760 |
+
0.4066,
|
| 761 |
+
0.4914,
|
| 762 |
+
0.5848,
|
| 763 |
+
0.7112,
|
| 764 |
+
0.8588,
|
| 765 |
+
0.936,
|
| 766 |
+
1.013,
|
| 767 |
+
1.092,
|
| 768 |
+
1.173,
|
| 769 |
+
1.257,
|
| 770 |
+
1.346,
|
| 771 |
+
1.452,
|
| 772 |
+
1.58,
|
| 773 |
+
1.677,
|
| 774 |
+
1.713]
|
| 775 |
+
|
| 776 |
+
# Parameters for div_curl_tendency_outputs/LevelScale:
|
| 777 |
+
# ==============================================================================
|
| 778 |
+
div_curl_tendency_outputs/LevelScale.keys_to_scale = ['specific_humidity']
|
| 779 |
+
div_curl_tendency_outputs/LevelScale.name = None
|
| 780 |
+
div_curl_tendency_outputs/LevelScale.scales = \
|
| 781 |
+
[0.000132,
|
| 782 |
+
0.0002049,
|
| 783 |
+
0.000272,
|
| 784 |
+
0.0004865,
|
| 785 |
+
0.001441,
|
| 786 |
+
0.004704,
|
| 787 |
+
0.01322,
|
| 788 |
+
0.03009,
|
| 789 |
+
0.05781,
|
| 790 |
+
0.09648,
|
| 791 |
+
0.1486,
|
| 792 |
+
0.211,
|
| 793 |
+
0.2892,
|
| 794 |
+
0.3729,
|
| 795 |
+
0.471,
|
| 796 |
+
0.579,
|
| 797 |
+
0.6898,
|
| 798 |
+
0.8158,
|
| 799 |
+
0.9043,
|
| 800 |
+
0.9958,
|
| 801 |
+
1.102,
|
| 802 |
+
1.197,
|
| 803 |
+
1.308,
|
| 804 |
+
1.427,
|
| 805 |
+
1.519,
|
| 806 |
+
1.571,
|
| 807 |
+
1.599,
|
| 808 |
+
1.614,
|
| 809 |
+
1.629,
|
| 810 |
+
1.656,
|
| 811 |
+
1.745,
|
| 812 |
+
1.841]
|
| 813 |
+
|
| 814 |
+
# Parameters for encode/LevelScale:
|
| 815 |
+
# ==============================================================================
|
| 816 |
+
encode/LevelScale.keys_to_scale = ['specific_humidity']
|
| 817 |
+
encode/LevelScale.name = None
|
| 818 |
+
encode/LevelScale.scales = \
|
| 819 |
+
[8.824e-05,
|
| 820 |
+
7.105e-05,
|
| 821 |
+
0.0001046,
|
| 822 |
+
0.0001851,
|
| 823 |
+
0.0007562,
|
| 824 |
+
0.002625,
|
| 825 |
+
0.007185,
|
| 826 |
+
0.01554,
|
| 827 |
+
0.02886,
|
| 828 |
+
0.04613,
|
| 829 |
+
0.07032,
|
| 830 |
+
0.09901,
|
| 831 |
+
0.1344,
|
| 832 |
+
0.175,
|
| 833 |
+
0.2226,
|
| 834 |
+
0.2802,
|
| 835 |
+
0.3438,
|
| 836 |
+
0.4144,
|
| 837 |
+
0.4684,
|
| 838 |
+
0.5257,
|
| 839 |
+
0.5895,
|
| 840 |
+
0.6645,
|
| 841 |
+
0.7505,
|
| 842 |
+
0.8483,
|
| 843 |
+
0.9499,
|
| 844 |
+
1.053,
|
| 845 |
+
1.159,
|
| 846 |
+
1.274,
|
| 847 |
+
1.399,
|
| 848 |
+
1.552,
|
| 849 |
+
1.709,
|
| 850 |
+
1.791]
|
| 851 |
+
|
| 852 |
+
# Parameters for advance/MemoryVelocityAndValues:
|
| 853 |
+
# ==============================================================================
|
| 854 |
+
advance/MemoryVelocityAndValues.fields_to_include = None
|
| 855 |
+
advance/MemoryVelocityAndValues.name = None
|
| 856 |
+
|
| 857 |
+
# Parameters for decode/MlpUniform:
|
| 858 |
+
# ==============================================================================
|
| 859 |
+
decode/MlpUniform.activate_final = False
|
| 860 |
+
decode/MlpUniform.activation = %ACTIVATION
|
| 861 |
+
decode/MlpUniform.b_init = None
|
| 862 |
+
decode/MlpUniform.b_init_final = None
|
| 863 |
+
decode/MlpUniform.name = None
|
| 864 |
+
decode/MlpUniform.num_hidden_layers = 0
|
| 865 |
+
decode/MlpUniform.num_hidden_units = %LAYER_SIZE
|
| 866 |
+
decode/MlpUniform.w_init = None
|
| 867 |
+
decode/MlpUniform.w_init_final = None
|
| 868 |
+
decode/MlpUniform.with_bias = False
|
| 869 |
+
|
| 870 |
+
# Parameters for encode/MlpUniform:
|
| 871 |
+
# ==============================================================================
|
| 872 |
+
encode/MlpUniform.activate_final = False
|
| 873 |
+
encode/MlpUniform.activation = %ACTIVATION
|
| 874 |
+
encode/MlpUniform.b_init = None
|
| 875 |
+
encode/MlpUniform.b_init_final = None
|
| 876 |
+
encode/MlpUniform.name = None
|
| 877 |
+
encode/MlpUniform.num_hidden_layers = 0
|
| 878 |
+
encode/MlpUniform.num_hidden_units = 0
|
| 879 |
+
encode/MlpUniform.w_init = None
|
| 880 |
+
encode/MlpUniform.w_init_final = None
|
| 881 |
+
encode/MlpUniform.with_bias = True
|
| 882 |
+
|
| 883 |
+
# Parameters for process/MlpUniform:
|
| 884 |
+
# ==============================================================================
|
| 885 |
+
process/MlpUniform.activate_final = False
|
| 886 |
+
process/MlpUniform.activation = %ACTIVATION
|
| 887 |
+
process/MlpUniform.b_init = None
|
| 888 |
+
process/MlpUniform.b_init_final = None
|
| 889 |
+
process/MlpUniform.name = None
|
| 890 |
+
process/MlpUniform.num_hidden_layers = 3
|
| 891 |
+
process/MlpUniform.num_hidden_units = %LAYER_SIZE
|
| 892 |
+
process/MlpUniform.w_init = None
|
| 893 |
+
process/MlpUniform.w_init_final = None
|
| 894 |
+
process/MlpUniform.with_bias = True
|
| 895 |
+
|
| 896 |
+
# Parameters for surface_model_decode/MlpUniform:
|
| 897 |
+
# ==============================================================================
|
| 898 |
+
surface_model_decode/MlpUniform.activate_final = False
|
| 899 |
+
surface_model_decode/MlpUniform.activation = %ACTIVATION
|
| 900 |
+
surface_model_decode/MlpUniform.b_init = None
|
| 901 |
+
surface_model_decode/MlpUniform.b_init_final = None
|
| 902 |
+
surface_model_decode/MlpUniform.name = None
|
| 903 |
+
surface_model_decode/MlpUniform.num_hidden_layers = 1
|
| 904 |
+
surface_model_decode/MlpUniform.num_hidden_units = %SURFACE_MODEL_LAYER_SIZE
|
| 905 |
+
surface_model_decode/MlpUniform.w_init = None
|
| 906 |
+
surface_model_decode/MlpUniform.w_init_final = None
|
| 907 |
+
surface_model_decode/MlpUniform.with_bias = False
|
| 908 |
+
|
| 909 |
+
# Parameters for surface_model_encode/MlpUniform:
|
| 910 |
+
# ==============================================================================
|
| 911 |
+
surface_model_encode/MlpUniform.activate_final = False
|
| 912 |
+
surface_model_encode/MlpUniform.activation = %ACTIVATION
|
| 913 |
+
surface_model_encode/MlpUniform.b_init = None
|
| 914 |
+
surface_model_encode/MlpUniform.b_init_final = None
|
| 915 |
+
surface_model_encode/MlpUniform.name = None
|
| 916 |
+
surface_model_encode/MlpUniform.num_hidden_layers = 0
|
| 917 |
+
surface_model_encode/MlpUniform.num_hidden_units = 0
|
| 918 |
+
surface_model_encode/MlpUniform.w_init = None
|
| 919 |
+
surface_model_encode/MlpUniform.w_init_final = None
|
| 920 |
+
surface_model_encode/MlpUniform.with_bias = True
|
| 921 |
+
|
| 922 |
+
# Parameters for surface_model_process/MlpUniform:
|
| 923 |
+
# ==============================================================================
|
| 924 |
+
surface_model_process/MlpUniform.activate_final = False
|
| 925 |
+
surface_model_process/MlpUniform.activation = %ACTIVATION
|
| 926 |
+
surface_model_process/MlpUniform.b_init = None
|
| 927 |
+
surface_model_process/MlpUniform.b_init_final = None
|
| 928 |
+
surface_model_process/MlpUniform.name = None
|
| 929 |
+
surface_model_process/MlpUniform.num_hidden_layers = 3
|
| 930 |
+
surface_model_process/MlpUniform.num_hidden_units = %SURFACE_MODEL_LAYER_SIZE
|
| 931 |
+
surface_model_process/MlpUniform.w_init = None
|
| 932 |
+
surface_model_process/MlpUniform.w_init_final = None
|
| 933 |
+
surface_model_process/MlpUniform.with_bias = True
|
| 934 |
+
|
| 935 |
+
# Parameters for advance/ModalToNodalEmbedding:
|
| 936 |
+
# ==============================================================================
|
| 937 |
+
advance/ModalToNodalEmbedding.modal_to_nodal_features_module = \
|
| 938 |
+
@embedding_model/CombinedFeatures
|
| 939 |
+
advance/ModalToNodalEmbedding.name = None
|
| 940 |
+
advance/ModalToNodalEmbedding.nodal_mapping_module = @NodalVolumeMapping
|
| 941 |
+
advance/ModalToNodalEmbedding.output_transform_module = @IdentityTransform
|
| 942 |
+
|
| 943 |
+
# Parameters for sea_model/ModalToNodalEmbedding:
|
| 944 |
+
# ==============================================================================
|
| 945 |
+
sea_model/ModalToNodalEmbedding.modal_to_nodal_features_module = \
|
| 946 |
+
@sea_model/CombinedFeatures
|
| 947 |
+
sea_model/ModalToNodalEmbedding.name = None
|
| 948 |
+
sea_model/ModalToNodalEmbedding.nodal_mapping_module = @sea_model/NodalMapping
|
| 949 |
+
sea_model/ModalToNodalEmbedding.output_transform_module = @IdentityTransform
|
| 950 |
+
|
| 951 |
+
# Parameters for MoistPrimitiveEquationsWithCloudMoisture:
|
| 952 |
+
# ==============================================================================
|
| 953 |
+
MoistPrimitiveEquationsWithCloudMoisture.include_vertical_advection = True
|
| 954 |
+
MoistPrimitiveEquationsWithCloudMoisture.name = None
|
| 955 |
+
MoistPrimitiveEquationsWithCloudMoisture.orography_module = @LearnedOrography
|
| 956 |
+
|
| 957 |
+
# Parameters for advance/NodalLandSeaIceEmbedding:
|
| 958 |
+
# ==============================================================================
|
| 959 |
+
advance/NodalLandSeaIceEmbedding.land_embedding = None
|
| 960 |
+
advance/NodalLandSeaIceEmbedding.name = None
|
| 961 |
+
advance/NodalLandSeaIceEmbedding.sea_embedding = @sea_model/ModalToNodalEmbedding
|
| 962 |
+
advance/NodalLandSeaIceEmbedding.sea_ice_embedding = None
|
| 963 |
+
advance/NodalLandSeaIceEmbedding.static_vars_ds_path = None
|
| 964 |
+
|
| 965 |
+
# Parameters for NodalMapping:
|
| 966 |
+
# ==============================================================================
|
| 967 |
+
NodalMapping.name = None
|
| 968 |
+
NodalMapping.tower_factory = @EpdTower
|
| 969 |
+
|
| 970 |
+
# Parameters for sea_model/NodalMapping:
|
| 971 |
+
# ==============================================================================
|
| 972 |
+
sea_model/NodalMapping.name = None
|
| 973 |
+
sea_model/NodalMapping.tower_factory = @surface_model/EpdTower
|
| 974 |
+
|
| 975 |
+
# Parameters for NodalVolumeMapping:
|
| 976 |
+
# ==============================================================================
|
| 977 |
+
NodalVolumeMapping.name = None
|
| 978 |
+
NodalVolumeMapping.tower_factory = @VerticalConvTower
|
| 979 |
+
|
| 980 |
+
# Parameters for NullFeatures:
|
| 981 |
+
# ==============================================================================
|
| 982 |
+
NullFeatures.name = None
|
| 983 |
+
|
| 984 |
+
# Parameters for PerVariableDataFilter:
|
| 985 |
+
# ==============================================================================
|
| 986 |
+
PerVariableDataFilter.name = None
|
| 987 |
+
PerVariableDataFilter.per_variable_filters = \
|
| 988 |
+
{'divergence': @divergence/DataExponentialFilter,
|
| 989 |
+
'log_surface_pressure': @lsp/DataExponentialFilter,
|
| 990 |
+
'sim_time': @DataNoFilter,
|
| 991 |
+
'temperature_variation': @temperature_variation/DataExponentialFilter,
|
| 992 |
+
'tracers': {'specific_cloud_ice_water_content': @q/DataExponentialFilter,
|
| 993 |
+
'specific_cloud_liquid_water_content': @q/DataExponentialFilter,
|
| 994 |
+
'specific_humidity': @q/DataExponentialFilter},
|
| 995 |
+
'vorticity': @vorticity/DataExponentialFilter}
|
| 996 |
+
|
| 997 |
+
# Parameters for advance/PressureFeatures:
|
| 998 |
+
# ==============================================================================
|
| 999 |
+
advance/PressureFeatures.name = None
|
| 1000 |
+
|
| 1001 |
+
# Parameters for embedding_model/PressureFeatures:
|
| 1002 |
+
# ==============================================================================
|
| 1003 |
+
embedding_model/PressureFeatures.name = None
|
| 1004 |
+
|
| 1005 |
+
# Parameters for primitive_eq_specs_constructor:
|
| 1006 |
+
# ==============================================================================
|
| 1007 |
+
primitive_eq_specs_constructor.scale = @GET_ATMOSPHERIC_SCALE()
|
| 1008 |
+
|
| 1009 |
+
# Parameters for primitive_eq_to_xarray:
|
| 1010 |
+
# ==============================================================================
|
| 1011 |
+
# None.
|
| 1012 |
+
|
| 1013 |
+
# Parameters for PrimitiveToWeatherbenchDecoder:
|
| 1014 |
+
# ==============================================================================
|
| 1015 |
+
# None.
|
| 1016 |
+
|
| 1017 |
+
# Parameters for advance/RadiationFeatures:
|
| 1018 |
+
# ==============================================================================
|
| 1019 |
+
advance/RadiationFeatures.name = None
|
| 1020 |
+
|
| 1021 |
+
# Parameters for decoder_model/RadiationFeatures:
|
| 1022 |
+
# ==============================================================================
|
| 1023 |
+
decoder_model/RadiationFeatures.name = None
|
| 1024 |
+
|
| 1025 |
+
# Parameters for encoder_data/RadiationFeatures:
|
| 1026 |
+
# ==============================================================================
|
| 1027 |
+
encoder_data/RadiationFeatures.name = None
|
| 1028 |
+
|
| 1029 |
+
# Parameters for RealSphericalHarmonicsWithZeroImag:
|
| 1030 |
+
# ==============================================================================
|
| 1031 |
+
RealSphericalHarmonicsWithZeroImag.base_shape_multiple = None
|
| 1032 |
+
RealSphericalHarmonicsWithZeroImag.reverse_einsum_arg_order = None
|
| 1033 |
+
RealSphericalHarmonicsWithZeroImag.stacked_fourier_transforms = None
|
| 1034 |
+
RealSphericalHarmonicsWithZeroImag.transform_precision = 'tensorfloat32'
|
| 1035 |
+
|
| 1036 |
+
# Parameters for orography/RealSphericalHarmonicsWithZeroImag:
|
| 1037 |
+
# ==============================================================================
|
| 1038 |
+
orography/RealSphericalHarmonicsWithZeroImag.base_shape_multiple = None
|
| 1039 |
+
orography/RealSphericalHarmonicsWithZeroImag.reverse_einsum_arg_order = None
|
| 1040 |
+
orography/RealSphericalHarmonicsWithZeroImag.stacked_fourier_transforms = None
|
| 1041 |
+
orography/RealSphericalHarmonicsWithZeroImag.transform_precision = 'tensorfloat32'
|
| 1042 |
+
|
| 1043 |
+
# Parameters for dycore/SequentialStepFilter:
|
| 1044 |
+
# ==============================================================================
|
| 1045 |
+
dycore/SequentialStepFilter.filter_modules = \
|
| 1046 |
+
(@dycore/ExponentialFilter, @stability/ExponentialFilter)
|
| 1047 |
+
dycore/SequentialStepFilter.name = None
|
| 1048 |
+
|
| 1049 |
+
# Parameters for ml/SequentialStepFilter:
|
| 1050 |
+
# ==============================================================================
|
| 1051 |
+
ml/SequentialStepFilter.filter_modules = (@stability/ExponentialFilter,)
|
| 1052 |
+
ml/SequentialStepFilter.name = None
|
| 1053 |
+
|
| 1054 |
+
# Parameters for advance/SequentialTransform:
|
| 1055 |
+
# ==============================================================================
|
| 1056 |
+
advance/SequentialTransform.name = None
|
| 1057 |
+
advance/SequentialTransform.transform_modules = \
|
| 1058 |
+
(@advance/ShiftAndNormalize,
|
| 1059 |
+
@advance/InverseLevelScale,
|
| 1060 |
+
@advance/TruncateSigmaLevels,
|
| 1061 |
+
@SoftClip)
|
| 1062 |
+
|
| 1063 |
+
# Parameters for decoder/SequentialTransform:
|
| 1064 |
+
# ==============================================================================
|
| 1065 |
+
decoder/SequentialTransform.name = None
|
| 1066 |
+
decoder/SequentialTransform.transform_modules = \
|
| 1067 |
+
(@decoder/InverseShiftAndNormalize, @decoder/LevelScale)
|
| 1068 |
+
|
| 1069 |
+
# Parameters for decoder_model/SequentialTransform:
|
| 1070 |
+
# ==============================================================================
|
| 1071 |
+
decoder_model/SequentialTransform.name = None
|
| 1072 |
+
decoder_model/SequentialTransform.transform_modules = \
|
| 1073 |
+
(@decoder_model/ShiftAndNormalize,
|
| 1074 |
+
@decoder_model/InverseLevelScale,
|
| 1075 |
+
@decoder_model/TruncateSigmaLevels)
|
| 1076 |
+
|
| 1077 |
+
# Parameters for div_curl_tendency_outputs/SequentialTransform:
|
| 1078 |
+
# ==============================================================================
|
| 1079 |
+
div_curl_tendency_outputs/SequentialTransform.name = None
|
| 1080 |
+
div_curl_tendency_outputs/SequentialTransform.transform_modules = \
|
| 1081 |
+
(@div_curl_tendency_outputs/InverseShiftAndNormalize,
|
| 1082 |
+
@div_curl_tendency_outputs/LevelScale)
|
| 1083 |
+
|
| 1084 |
+
# Parameters for encode/SequentialTransform:
|
| 1085 |
+
# ==============================================================================
|
| 1086 |
+
encode/SequentialTransform.name = None
|
| 1087 |
+
encode/SequentialTransform.transform_modules = \
|
| 1088 |
+
(@encoder/InverseShiftAndNormalize, @encode/LevelScale)
|
| 1089 |
+
|
| 1090 |
+
# Parameters for encoder_data/SequentialTransform:
|
| 1091 |
+
# ==============================================================================
|
| 1092 |
+
encoder_data/SequentialTransform.name = None
|
| 1093 |
+
encoder_data/SequentialTransform.transform_modules = \
|
| 1094 |
+
(@encoder_data/ShiftAndNormalize, @encoder_data/InverseLevelScale)
|
| 1095 |
+
|
| 1096 |
+
# Parameters for advance/ShiftAndNormalize:
|
| 1097 |
+
# ==============================================================================
|
| 1098 |
+
advance/ShiftAndNormalize.features_to_exclude = ()
|
| 1099 |
+
advance/ShiftAndNormalize.global_scale = None
|
| 1100 |
+
advance/ShiftAndNormalize.name = None
|
| 1101 |
+
advance/ShiftAndNormalize.scales = \
|
| 1102 |
+
{'CNN1D_0': 0.1,
|
| 1103 |
+
'CNN1D_1': 0.1,
|
| 1104 |
+
'CNN1D_10': 0.1,
|
| 1105 |
+
'CNN1D_11': 0.1,
|
| 1106 |
+
'CNN1D_12': 0.1,
|
| 1107 |
+
'CNN1D_13': 0.1,
|
| 1108 |
+
'CNN1D_14': 0.1,
|
| 1109 |
+
'CNN1D_15': 0.1,
|
| 1110 |
+
'CNN1D_16': 0.1,
|
| 1111 |
+
'CNN1D_17': 0.1,
|
| 1112 |
+
'CNN1D_18': 0.1,
|
| 1113 |
+
'CNN1D_19': 0.1,
|
| 1114 |
+
'CNN1D_2': 0.1,
|
| 1115 |
+
'CNN1D_20': 0.1,
|
| 1116 |
+
'CNN1D_21': 0.1,
|
| 1117 |
+
'CNN1D_22': 0.1,
|
| 1118 |
+
'CNN1D_23': 0.1,
|
| 1119 |
+
'CNN1D_24': 0.1,
|
| 1120 |
+
'CNN1D_25': 0.1,
|
| 1121 |
+
'CNN1D_26': 0.1,
|
| 1122 |
+
'CNN1D_27': 0.1,
|
| 1123 |
+
'CNN1D_28': 0.1,
|
| 1124 |
+
'CNN1D_29': 0.1,
|
| 1125 |
+
'CNN1D_3': 0.1,
|
| 1126 |
+
'CNN1D_30': 0.1,
|
| 1127 |
+
'CNN1D_31': 0.1,
|
| 1128 |
+
'CNN1D_32': 0.1,
|
| 1129 |
+
'CNN1D_33': 0.1,
|
| 1130 |
+
'CNN1D_34': 0.1,
|
| 1131 |
+
'CNN1D_35': 0.1,
|
| 1132 |
+
'CNN1D_36': 0.1,
|
| 1133 |
+
'CNN1D_37': 0.1,
|
| 1134 |
+
'CNN1D_38': 0.1,
|
| 1135 |
+
'CNN1D_39': 0.1,
|
| 1136 |
+
'CNN1D_4': 0.1,
|
| 1137 |
+
'CNN1D_40': 0.1,
|
| 1138 |
+
'CNN1D_41': 0.1,
|
| 1139 |
+
'CNN1D_42': 0.1,
|
| 1140 |
+
'CNN1D_43': 0.1,
|
| 1141 |
+
'CNN1D_44': 0.1,
|
| 1142 |
+
'CNN1D_45': 0.1,
|
| 1143 |
+
'CNN1D_46': 0.1,
|
| 1144 |
+
'CNN1D_47': 0.1,
|
| 1145 |
+
'CNN1D_48': 0.1,
|
| 1146 |
+
'CNN1D_49': 0.1,
|
| 1147 |
+
'CNN1D_5': 0.1,
|
| 1148 |
+
'CNN1D_50': 0.1,
|
| 1149 |
+
'CNN1D_51': 0.1,
|
| 1150 |
+
'CNN1D_52': 0.1,
|
| 1151 |
+
'CNN1D_53': 0.1,
|
| 1152 |
+
'CNN1D_54': 0.1,
|
| 1153 |
+
'CNN1D_55': 0.1,
|
| 1154 |
+
'CNN1D_56': 0.1,
|
| 1155 |
+
'CNN1D_57': 0.1,
|
| 1156 |
+
'CNN1D_58': 0.1,
|
| 1157 |
+
'CNN1D_59': 0.1,
|
| 1158 |
+
'CNN1D_6': 0.1,
|
| 1159 |
+
'CNN1D_60': 0.1,
|
| 1160 |
+
'CNN1D_61': 0.1,
|
| 1161 |
+
'CNN1D_62': 0.1,
|
| 1162 |
+
'CNN1D_63': 0.1,
|
| 1163 |
+
'CNN1D_7': 0.1,
|
| 1164 |
+
'CNN1D_8': 0.1,
|
| 1165 |
+
'CNN1D_9': 0.1,
|
| 1166 |
+
'cos_latitude': 0.3057,
|
| 1167 |
+
'divergence': 0.08243,
|
| 1168 |
+
'divergence_del2': 629.5,
|
| 1169 |
+
'divergence_dlat': 6.037,
|
| 1170 |
+
'divergence_dlon': 4.505,
|
| 1171 |
+
'geopotential_at_surface': 0.009482,
|
| 1172 |
+
'geopotential_at_surface_del2': 11.13,
|
| 1173 |
+
'geopotential_at_surface_dlat': 0.1337,
|
| 1174 |
+
'geopotential_at_surface_dlon': 0.1168,
|
| 1175 |
+
'land_sea_mask': 0.4503,
|
| 1176 |
+
'learned_positional_features': 1.0,
|
| 1177 |
+
'log_surface_pressure': 0.1123,
|
| 1178 |
+
'log_surface_pressure_del2': 120.6,
|
| 1179 |
+
'log_surface_pressure_dlat': 1.41,
|
| 1180 |
+
'log_surface_pressure_dlon': 1.285,
|
| 1181 |
+
'memory_divergence': 0.08243,
|
| 1182 |
+
'memory_log_surface_pressure': 0.1123,
|
| 1183 |
+
'memory_specific_cloud_ice_water_content': 9.111e-06,
|
| 1184 |
+
'memory_specific_cloud_liquid_water_content': 1.897e-05,
|
| 1185 |
+
'memory_specific_humidity': 0.003298,
|
| 1186 |
+
'memory_temperature_variation': 14.99,
|
| 1187 |
+
'memory_u': 0.01485,
|
| 1188 |
+
'memory_v': 0.01017,
|
| 1189 |
+
'memory_vorticity': 0.2579,
|
| 1190 |
+
'pressure': 1.644,
|
| 1191 |
+
'radiation': 0.2864,
|
| 1192 |
+
'sea_ice_cover': 0.387,
|
| 1193 |
+
'sea_surface_temperature': 11.93,
|
| 1194 |
+
'sin_latitude': 0.7057,
|
| 1195 |
+
'specific_cloud_ice_water_content': 9.111e-06,
|
| 1196 |
+
'specific_cloud_ice_water_content_del2': 0.03864,
|
| 1197 |
+
'specific_cloud_ice_water_content_dlat': 0.0003813,
|
| 1198 |
+
'specific_cloud_ice_water_content_dlon': 0.0003149,
|
| 1199 |
+
'specific_cloud_liquid_water_content': 1.897e-05,
|
| 1200 |
+
'specific_cloud_liquid_water_content_del2': 0.08082,
|
| 1201 |
+
'specific_cloud_liquid_water_content_dlat': 0.0008049,
|
| 1202 |
+
'specific_cloud_liquid_water_content_dlon': 0.0006388,
|
| 1203 |
+
'specific_humidity': 0.003298,
|
| 1204 |
+
'specific_humidity_del2': 2.389,
|
| 1205 |
+
'specific_humidity_dlat': 0.02819,
|
| 1206 |
+
'specific_humidity_dlon': 0.021,
|
| 1207 |
+
'surface_embedding': 1.0,
|
| 1208 |
+
'temperature_variation': 14.99,
|
| 1209 |
+
'temperature_variation_del2': 5793.0,
|
| 1210 |
+
'temperature_variation_dlat': 75.47,
|
| 1211 |
+
'temperature_variation_dlon': 64.98,
|
| 1212 |
+
'u': 0.01485,
|
| 1213 |
+
'u_del2': 13.13,
|
| 1214 |
+
'u_dlat': 1.223,
|
| 1215 |
+
'u_dlon': 0.1378,
|
| 1216 |
+
'v': 0.01017,
|
| 1217 |
+
'v_del2': 11.26,
|
| 1218 |
+
'v_dlat': 0.764,
|
| 1219 |
+
'v_dlon': 0.1781,
|
| 1220 |
+
'vorticity': 0.2579,
|
| 1221 |
+
'vorticity_del2': 1189.0,
|
| 1222 |
+
'vorticity_dlat': 11.67,
|
| 1223 |
+
'vorticity_dlon': 9.514}
|
| 1224 |
+
advance/ShiftAndNormalize.shifts = \
|
| 1225 |
+
{'CNN1D_0': 0.0,
|
| 1226 |
+
'CNN1D_1': 0.0,
|
| 1227 |
+
'CNN1D_10': 0.0,
|
| 1228 |
+
'CNN1D_11': 0.0,
|
| 1229 |
+
'CNN1D_12': 0.0,
|
| 1230 |
+
'CNN1D_13': 0.0,
|
| 1231 |
+
'CNN1D_14': 0.0,
|
| 1232 |
+
'CNN1D_15': 0.0,
|
| 1233 |
+
'CNN1D_16': 0.0,
|
| 1234 |
+
'CNN1D_17': 0.0,
|
| 1235 |
+
'CNN1D_18': 0.0,
|
| 1236 |
+
'CNN1D_19': 0.0,
|
| 1237 |
+
'CNN1D_2': 0.0,
|
| 1238 |
+
'CNN1D_20': 0.0,
|
| 1239 |
+
'CNN1D_21': 0.0,
|
| 1240 |
+
'CNN1D_22': 0.0,
|
| 1241 |
+
'CNN1D_23': 0.0,
|
| 1242 |
+
'CNN1D_24': 0.0,
|
| 1243 |
+
'CNN1D_25': 0.0,
|
| 1244 |
+
'CNN1D_26': 0.0,
|
| 1245 |
+
'CNN1D_27': 0.0,
|
| 1246 |
+
'CNN1D_28': 0.0,
|
| 1247 |
+
'CNN1D_29': 0.0,
|
| 1248 |
+
'CNN1D_3': 0.0,
|
| 1249 |
+
'CNN1D_30': 0.0,
|
| 1250 |
+
'CNN1D_31': 0.0,
|
| 1251 |
+
'CNN1D_32': 0.0,
|
| 1252 |
+
'CNN1D_33': 0.0,
|
| 1253 |
+
'CNN1D_34': 0.0,
|
| 1254 |
+
'CNN1D_35': 0.0,
|
| 1255 |
+
'CNN1D_36': 0.0,
|
| 1256 |
+
'CNN1D_37': 0.0,
|
| 1257 |
+
'CNN1D_38': 0.0,
|
| 1258 |
+
'CNN1D_39': 0.0,
|
| 1259 |
+
'CNN1D_4': 0.0,
|
| 1260 |
+
'CNN1D_40': 0.0,
|
| 1261 |
+
'CNN1D_41': 0.0,
|
| 1262 |
+
'CNN1D_42': 0.0,
|
| 1263 |
+
'CNN1D_43': 0.0,
|
| 1264 |
+
'CNN1D_44': 0.0,
|
| 1265 |
+
'CNN1D_45': 0.0,
|
| 1266 |
+
'CNN1D_46': 0.0,
|
| 1267 |
+
'CNN1D_47': 0.0,
|
| 1268 |
+
'CNN1D_48': 0.0,
|
| 1269 |
+
'CNN1D_49': 0.0,
|
| 1270 |
+
'CNN1D_5': 0.0,
|
| 1271 |
+
'CNN1D_50': 0.0,
|
| 1272 |
+
'CNN1D_51': 0.0,
|
| 1273 |
+
'CNN1D_52': 0.0,
|
| 1274 |
+
'CNN1D_53': 0.0,
|
| 1275 |
+
'CNN1D_54': 0.0,
|
| 1276 |
+
'CNN1D_55': 0.0,
|
| 1277 |
+
'CNN1D_56': 0.0,
|
| 1278 |
+
'CNN1D_57': 0.0,
|
| 1279 |
+
'CNN1D_58': 0.0,
|
| 1280 |
+
'CNN1D_59': 0.0,
|
| 1281 |
+
'CNN1D_6': 0.0,
|
| 1282 |
+
'CNN1D_60': 0.0,
|
| 1283 |
+
'CNN1D_61': 0.0,
|
| 1284 |
+
'CNN1D_62': 0.0,
|
| 1285 |
+
'CNN1D_63': 0.0,
|
| 1286 |
+
'CNN1D_7': 0.0,
|
| 1287 |
+
'CNN1D_8': 0.0,
|
| 1288 |
+
'CNN1D_9': 0.0,
|
| 1289 |
+
'cos_latitude': 0.639,
|
| 1290 |
+
'divergence': -0.0,
|
| 1291 |
+
'divergence_del2': -0.009,
|
| 1292 |
+
'divergence_dlat': -0.001,
|
| 1293 |
+
'divergence_dlon': 0.0,
|
| 1294 |
+
'geopotential_at_surface': 0.004,
|
| 1295 |
+
'geopotential_at_surface_del2': -0.069,
|
| 1296 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1297 |
+
'geopotential_at_surface_dlon': 0.0,
|
| 1298 |
+
'land_sea_mask': 0.335,
|
| 1299 |
+
'learned_positional_features': 0.0,
|
| 1300 |
+
'log_surface_pressure': 1.716,
|
| 1301 |
+
'log_surface_pressure_del2': 0.824,
|
| 1302 |
+
'log_surface_pressure_dlat': 0.13,
|
| 1303 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1304 |
+
'memory_divergence': -0.0,
|
| 1305 |
+
'memory_log_surface_pressure': 1.716,
|
| 1306 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1307 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1308 |
+
'memory_specific_humidity': 0.003298,
|
| 1309 |
+
'memory_temperature_variation': -4.946,
|
| 1310 |
+
'memory_u': 0.007,
|
| 1311 |
+
'memory_v': -0.0,
|
| 1312 |
+
'memory_vorticity': -0.002,
|
| 1313 |
+
'pressure': 2.796,
|
| 1314 |
+
'radiation': 0.214,
|
| 1315 |
+
'sea_ice_cover': 0.24,
|
| 1316 |
+
'sea_surface_temperature': 285.14,
|
| 1317 |
+
'sin_latitude': -0.0,
|
| 1318 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1319 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1320 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1321 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1322 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1323 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1324 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1325 |
+
'specific_cloud_liquid_water_content_dlon': 0.0,
|
| 1326 |
+
'specific_humidity': 0.0,
|
| 1327 |
+
'specific_humidity_del2': 0.0,
|
| 1328 |
+
'specific_humidity_dlat': 0.0,
|
| 1329 |
+
'specific_humidity_dlon': 0.0,
|
| 1330 |
+
'surface_embedding': 0.0,
|
| 1331 |
+
'temperature_variation': -4.946,
|
| 1332 |
+
'temperature_variation_del2': 47.3,
|
| 1333 |
+
'temperature_variation_dlat': 4.913,
|
| 1334 |
+
'temperature_variation_dlon': -0.0,
|
| 1335 |
+
'u': 0.007,
|
| 1336 |
+
'u_del2': 0.187,
|
| 1337 |
+
'u_dlat': 0.002,
|
| 1338 |
+
'u_dlon': -0.0,
|
| 1339 |
+
'v': -0.0,
|
| 1340 |
+
'v_del2': -0.002,
|
| 1341 |
+
'v_dlat': -0.0,
|
| 1342 |
+
'v_dlon': 0.0,
|
| 1343 |
+
'vorticity': -0.002,
|
| 1344 |
+
'vorticity_del2': -0.37,
|
| 1345 |
+
'vorticity_dlat': 0.042,
|
| 1346 |
+
'vorticity_dlon': -0.0}
|
| 1347 |
+
|
| 1348 |
+
# Parameters for decoder_model/ShiftAndNormalize:
|
| 1349 |
+
# ==============================================================================
|
| 1350 |
+
decoder_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1351 |
+
decoder_model/ShiftAndNormalize.global_scale = None
|
| 1352 |
+
decoder_model/ShiftAndNormalize.name = None
|
| 1353 |
+
decoder_model/ShiftAndNormalize.scales = \
|
| 1354 |
+
{'CNN1D_0': 0.1,
|
| 1355 |
+
'CNN1D_1': 0.1,
|
| 1356 |
+
'CNN1D_10': 0.1,
|
| 1357 |
+
'CNN1D_11': 0.1,
|
| 1358 |
+
'CNN1D_12': 0.1,
|
| 1359 |
+
'CNN1D_13': 0.1,
|
| 1360 |
+
'CNN1D_14': 0.1,
|
| 1361 |
+
'CNN1D_15': 0.1,
|
| 1362 |
+
'CNN1D_16': 0.1,
|
| 1363 |
+
'CNN1D_17': 0.1,
|
| 1364 |
+
'CNN1D_18': 0.1,
|
| 1365 |
+
'CNN1D_19': 0.1,
|
| 1366 |
+
'CNN1D_2': 0.1,
|
| 1367 |
+
'CNN1D_20': 0.1,
|
| 1368 |
+
'CNN1D_21': 0.1,
|
| 1369 |
+
'CNN1D_22': 0.1,
|
| 1370 |
+
'CNN1D_23': 0.1,
|
| 1371 |
+
'CNN1D_24': 0.1,
|
| 1372 |
+
'CNN1D_25': 0.1,
|
| 1373 |
+
'CNN1D_26': 0.1,
|
| 1374 |
+
'CNN1D_27': 0.1,
|
| 1375 |
+
'CNN1D_28': 0.1,
|
| 1376 |
+
'CNN1D_29': 0.1,
|
| 1377 |
+
'CNN1D_3': 0.1,
|
| 1378 |
+
'CNN1D_30': 0.1,
|
| 1379 |
+
'CNN1D_31': 0.1,
|
| 1380 |
+
'CNN1D_32': 0.1,
|
| 1381 |
+
'CNN1D_33': 0.1,
|
| 1382 |
+
'CNN1D_34': 0.1,
|
| 1383 |
+
'CNN1D_35': 0.1,
|
| 1384 |
+
'CNN1D_36': 0.1,
|
| 1385 |
+
'CNN1D_37': 0.1,
|
| 1386 |
+
'CNN1D_38': 0.1,
|
| 1387 |
+
'CNN1D_39': 0.1,
|
| 1388 |
+
'CNN1D_4': 0.1,
|
| 1389 |
+
'CNN1D_40': 0.1,
|
| 1390 |
+
'CNN1D_41': 0.1,
|
| 1391 |
+
'CNN1D_42': 0.1,
|
| 1392 |
+
'CNN1D_43': 0.1,
|
| 1393 |
+
'CNN1D_44': 0.1,
|
| 1394 |
+
'CNN1D_45': 0.1,
|
| 1395 |
+
'CNN1D_46': 0.1,
|
| 1396 |
+
'CNN1D_47': 0.1,
|
| 1397 |
+
'CNN1D_48': 0.1,
|
| 1398 |
+
'CNN1D_49': 0.1,
|
| 1399 |
+
'CNN1D_5': 0.1,
|
| 1400 |
+
'CNN1D_50': 0.1,
|
| 1401 |
+
'CNN1D_51': 0.1,
|
| 1402 |
+
'CNN1D_52': 0.1,
|
| 1403 |
+
'CNN1D_53': 0.1,
|
| 1404 |
+
'CNN1D_54': 0.1,
|
| 1405 |
+
'CNN1D_55': 0.1,
|
| 1406 |
+
'CNN1D_56': 0.1,
|
| 1407 |
+
'CNN1D_57': 0.1,
|
| 1408 |
+
'CNN1D_58': 0.1,
|
| 1409 |
+
'CNN1D_59': 0.1,
|
| 1410 |
+
'CNN1D_6': 0.1,
|
| 1411 |
+
'CNN1D_60': 0.1,
|
| 1412 |
+
'CNN1D_61': 0.1,
|
| 1413 |
+
'CNN1D_62': 0.1,
|
| 1414 |
+
'CNN1D_63': 0.1,
|
| 1415 |
+
'CNN1D_7': 0.1,
|
| 1416 |
+
'CNN1D_8': 0.1,
|
| 1417 |
+
'CNN1D_9': 0.1,
|
| 1418 |
+
'cos_latitude': 0.3057,
|
| 1419 |
+
'divergence': 0.08243,
|
| 1420 |
+
'divergence_del2': 629.5,
|
| 1421 |
+
'divergence_dlat': 6.037,
|
| 1422 |
+
'divergence_dlon': 4.505,
|
| 1423 |
+
'geopotential_at_surface': 0.009482,
|
| 1424 |
+
'geopotential_at_surface_del2': 11.13,
|
| 1425 |
+
'geopotential_at_surface_dlat': 0.1337,
|
| 1426 |
+
'geopotential_at_surface_dlon': 0.1168,
|
| 1427 |
+
'land_sea_mask': 0.4503,
|
| 1428 |
+
'learned_positional_features': 1.0,
|
| 1429 |
+
'log_surface_pressure': 0.1123,
|
| 1430 |
+
'log_surface_pressure_del2': 120.6,
|
| 1431 |
+
'log_surface_pressure_dlat': 1.41,
|
| 1432 |
+
'log_surface_pressure_dlon': 1.285,
|
| 1433 |
+
'memory_divergence': 0.08243,
|
| 1434 |
+
'memory_log_surface_pressure': 0.1123,
|
| 1435 |
+
'memory_specific_cloud_ice_water_content': 9.111e-06,
|
| 1436 |
+
'memory_specific_cloud_liquid_water_content': 1.897e-05,
|
| 1437 |
+
'memory_specific_humidity': 0.003298,
|
| 1438 |
+
'memory_temperature_variation': 14.99,
|
| 1439 |
+
'memory_u': 0.01485,
|
| 1440 |
+
'memory_v': 0.01017,
|
| 1441 |
+
'memory_vorticity': 0.2579,
|
| 1442 |
+
'pressure': 1.644,
|
| 1443 |
+
'radiation': 0.2864,
|
| 1444 |
+
'sea_ice_cover': 0.387,
|
| 1445 |
+
'sea_surface_temperature': 11.93,
|
| 1446 |
+
'sin_latitude': 0.7057,
|
| 1447 |
+
'specific_cloud_ice_water_content': 9.111e-06,
|
| 1448 |
+
'specific_cloud_ice_water_content_del2': 0.03864,
|
| 1449 |
+
'specific_cloud_ice_water_content_dlat': 0.0003813,
|
| 1450 |
+
'specific_cloud_ice_water_content_dlon': 0.0003149,
|
| 1451 |
+
'specific_cloud_liquid_water_content': 1.897e-05,
|
| 1452 |
+
'specific_cloud_liquid_water_content_del2': 0.08082,
|
| 1453 |
+
'specific_cloud_liquid_water_content_dlat': 0.0008049,
|
| 1454 |
+
'specific_cloud_liquid_water_content_dlon': 0.0006388,
|
| 1455 |
+
'specific_humidity': 0.003298,
|
| 1456 |
+
'specific_humidity_del2': 2.389,
|
| 1457 |
+
'specific_humidity_dlat': 0.02819,
|
| 1458 |
+
'specific_humidity_dlon': 0.021,
|
| 1459 |
+
'surface_embedding': 1.0,
|
| 1460 |
+
'temperature_variation': 14.99,
|
| 1461 |
+
'temperature_variation_del2': 5793.0,
|
| 1462 |
+
'temperature_variation_dlat': 75.47,
|
| 1463 |
+
'temperature_variation_dlon': 64.98,
|
| 1464 |
+
'u': 0.01485,
|
| 1465 |
+
'u_del2': 13.13,
|
| 1466 |
+
'u_dlat': 1.223,
|
| 1467 |
+
'u_dlon': 0.1378,
|
| 1468 |
+
'v': 0.01017,
|
| 1469 |
+
'v_del2': 11.26,
|
| 1470 |
+
'v_dlat': 0.764,
|
| 1471 |
+
'v_dlon': 0.1781,
|
| 1472 |
+
'vorticity': 0.2579,
|
| 1473 |
+
'vorticity_del2': 1189.0,
|
| 1474 |
+
'vorticity_dlat': 11.67,
|
| 1475 |
+
'vorticity_dlon': 9.514}
|
| 1476 |
+
decoder_model/ShiftAndNormalize.shifts = \
|
| 1477 |
+
{'CNN1D_0': 0.0,
|
| 1478 |
+
'CNN1D_1': 0.0,
|
| 1479 |
+
'CNN1D_10': 0.0,
|
| 1480 |
+
'CNN1D_11': 0.0,
|
| 1481 |
+
'CNN1D_12': 0.0,
|
| 1482 |
+
'CNN1D_13': 0.0,
|
| 1483 |
+
'CNN1D_14': 0.0,
|
| 1484 |
+
'CNN1D_15': 0.0,
|
| 1485 |
+
'CNN1D_16': 0.0,
|
| 1486 |
+
'CNN1D_17': 0.0,
|
| 1487 |
+
'CNN1D_18': 0.0,
|
| 1488 |
+
'CNN1D_19': 0.0,
|
| 1489 |
+
'CNN1D_2': 0.0,
|
| 1490 |
+
'CNN1D_20': 0.0,
|
| 1491 |
+
'CNN1D_21': 0.0,
|
| 1492 |
+
'CNN1D_22': 0.0,
|
| 1493 |
+
'CNN1D_23': 0.0,
|
| 1494 |
+
'CNN1D_24': 0.0,
|
| 1495 |
+
'CNN1D_25': 0.0,
|
| 1496 |
+
'CNN1D_26': 0.0,
|
| 1497 |
+
'CNN1D_27': 0.0,
|
| 1498 |
+
'CNN1D_28': 0.0,
|
| 1499 |
+
'CNN1D_29': 0.0,
|
| 1500 |
+
'CNN1D_3': 0.0,
|
| 1501 |
+
'CNN1D_30': 0.0,
|
| 1502 |
+
'CNN1D_31': 0.0,
|
| 1503 |
+
'CNN1D_32': 0.0,
|
| 1504 |
+
'CNN1D_33': 0.0,
|
| 1505 |
+
'CNN1D_34': 0.0,
|
| 1506 |
+
'CNN1D_35': 0.0,
|
| 1507 |
+
'CNN1D_36': 0.0,
|
| 1508 |
+
'CNN1D_37': 0.0,
|
| 1509 |
+
'CNN1D_38': 0.0,
|
| 1510 |
+
'CNN1D_39': 0.0,
|
| 1511 |
+
'CNN1D_4': 0.0,
|
| 1512 |
+
'CNN1D_40': 0.0,
|
| 1513 |
+
'CNN1D_41': 0.0,
|
| 1514 |
+
'CNN1D_42': 0.0,
|
| 1515 |
+
'CNN1D_43': 0.0,
|
| 1516 |
+
'CNN1D_44': 0.0,
|
| 1517 |
+
'CNN1D_45': 0.0,
|
| 1518 |
+
'CNN1D_46': 0.0,
|
| 1519 |
+
'CNN1D_47': 0.0,
|
| 1520 |
+
'CNN1D_48': 0.0,
|
| 1521 |
+
'CNN1D_49': 0.0,
|
| 1522 |
+
'CNN1D_5': 0.0,
|
| 1523 |
+
'CNN1D_50': 0.0,
|
| 1524 |
+
'CNN1D_51': 0.0,
|
| 1525 |
+
'CNN1D_52': 0.0,
|
| 1526 |
+
'CNN1D_53': 0.0,
|
| 1527 |
+
'CNN1D_54': 0.0,
|
| 1528 |
+
'CNN1D_55': 0.0,
|
| 1529 |
+
'CNN1D_56': 0.0,
|
| 1530 |
+
'CNN1D_57': 0.0,
|
| 1531 |
+
'CNN1D_58': 0.0,
|
| 1532 |
+
'CNN1D_59': 0.0,
|
| 1533 |
+
'CNN1D_6': 0.0,
|
| 1534 |
+
'CNN1D_60': 0.0,
|
| 1535 |
+
'CNN1D_61': 0.0,
|
| 1536 |
+
'CNN1D_62': 0.0,
|
| 1537 |
+
'CNN1D_63': 0.0,
|
| 1538 |
+
'CNN1D_7': 0.0,
|
| 1539 |
+
'CNN1D_8': 0.0,
|
| 1540 |
+
'CNN1D_9': 0.0,
|
| 1541 |
+
'cos_latitude': 0.639,
|
| 1542 |
+
'divergence': -0.0,
|
| 1543 |
+
'divergence_del2': -0.009,
|
| 1544 |
+
'divergence_dlat': -0.001,
|
| 1545 |
+
'divergence_dlon': 0.0,
|
| 1546 |
+
'geopotential_at_surface': 0.004,
|
| 1547 |
+
'geopotential_at_surface_del2': -0.069,
|
| 1548 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1549 |
+
'geopotential_at_surface_dlon': 0.0,
|
| 1550 |
+
'land_sea_mask': 0.335,
|
| 1551 |
+
'learned_positional_features': 0.0,
|
| 1552 |
+
'log_surface_pressure': 1.716,
|
| 1553 |
+
'log_surface_pressure_del2': 0.824,
|
| 1554 |
+
'log_surface_pressure_dlat': 0.13,
|
| 1555 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1556 |
+
'memory_divergence': -0.0,
|
| 1557 |
+
'memory_log_surface_pressure': 1.716,
|
| 1558 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1559 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1560 |
+
'memory_specific_humidity': 0.003298,
|
| 1561 |
+
'memory_temperature_variation': -4.946,
|
| 1562 |
+
'memory_u': 0.007,
|
| 1563 |
+
'memory_v': -0.0,
|
| 1564 |
+
'memory_vorticity': -0.002,
|
| 1565 |
+
'pressure': 2.796,
|
| 1566 |
+
'radiation': 0.214,
|
| 1567 |
+
'sea_ice_cover': 0.24,
|
| 1568 |
+
'sea_surface_temperature': 285.14,
|
| 1569 |
+
'sin_latitude': -0.0,
|
| 1570 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1571 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1572 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1573 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1574 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1575 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1576 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1577 |
+
'specific_cloud_liquid_water_content_dlon': 0.0,
|
| 1578 |
+
'specific_humidity': 0.0,
|
| 1579 |
+
'specific_humidity_del2': 0.0,
|
| 1580 |
+
'specific_humidity_dlat': 0.0,
|
| 1581 |
+
'specific_humidity_dlon': 0.0,
|
| 1582 |
+
'surface_embedding': 0.0,
|
| 1583 |
+
'temperature_variation': -4.946,
|
| 1584 |
+
'temperature_variation_del2': 47.3,
|
| 1585 |
+
'temperature_variation_dlat': 4.913,
|
| 1586 |
+
'temperature_variation_dlon': -0.0,
|
| 1587 |
+
'u': 0.007,
|
| 1588 |
+
'u_del2': 0.187,
|
| 1589 |
+
'u_dlat': 0.002,
|
| 1590 |
+
'u_dlon': -0.0,
|
| 1591 |
+
'v': -0.0,
|
| 1592 |
+
'v_del2': -0.002,
|
| 1593 |
+
'v_dlat': -0.0,
|
| 1594 |
+
'v_dlon': 0.0,
|
| 1595 |
+
'vorticity': -0.002,
|
| 1596 |
+
'vorticity_del2': -0.37,
|
| 1597 |
+
'vorticity_dlat': 0.042,
|
| 1598 |
+
'vorticity_dlon': -0.0}
|
| 1599 |
+
|
| 1600 |
+
# Parameters for embedding_model/ShiftAndNormalize:
|
| 1601 |
+
# ==============================================================================
|
| 1602 |
+
embedding_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1603 |
+
embedding_model/ShiftAndNormalize.global_scale = None
|
| 1604 |
+
embedding_model/ShiftAndNormalize.name = None
|
| 1605 |
+
embedding_model/ShiftAndNormalize.scales = \
|
| 1606 |
+
{'CNN1D_0': 0.1,
|
| 1607 |
+
'CNN1D_1': 0.1,
|
| 1608 |
+
'CNN1D_10': 0.1,
|
| 1609 |
+
'CNN1D_11': 0.1,
|
| 1610 |
+
'CNN1D_12': 0.1,
|
| 1611 |
+
'CNN1D_13': 0.1,
|
| 1612 |
+
'CNN1D_14': 0.1,
|
| 1613 |
+
'CNN1D_15': 0.1,
|
| 1614 |
+
'CNN1D_16': 0.1,
|
| 1615 |
+
'CNN1D_17': 0.1,
|
| 1616 |
+
'CNN1D_18': 0.1,
|
| 1617 |
+
'CNN1D_19': 0.1,
|
| 1618 |
+
'CNN1D_2': 0.1,
|
| 1619 |
+
'CNN1D_20': 0.1,
|
| 1620 |
+
'CNN1D_21': 0.1,
|
| 1621 |
+
'CNN1D_22': 0.1,
|
| 1622 |
+
'CNN1D_23': 0.1,
|
| 1623 |
+
'CNN1D_24': 0.1,
|
| 1624 |
+
'CNN1D_25': 0.1,
|
| 1625 |
+
'CNN1D_26': 0.1,
|
| 1626 |
+
'CNN1D_27': 0.1,
|
| 1627 |
+
'CNN1D_28': 0.1,
|
| 1628 |
+
'CNN1D_29': 0.1,
|
| 1629 |
+
'CNN1D_3': 0.1,
|
| 1630 |
+
'CNN1D_30': 0.1,
|
| 1631 |
+
'CNN1D_31': 0.1,
|
| 1632 |
+
'CNN1D_32': 0.1,
|
| 1633 |
+
'CNN1D_33': 0.1,
|
| 1634 |
+
'CNN1D_34': 0.1,
|
| 1635 |
+
'CNN1D_35': 0.1,
|
| 1636 |
+
'CNN1D_36': 0.1,
|
| 1637 |
+
'CNN1D_37': 0.1,
|
| 1638 |
+
'CNN1D_38': 0.1,
|
| 1639 |
+
'CNN1D_39': 0.1,
|
| 1640 |
+
'CNN1D_4': 0.1,
|
| 1641 |
+
'CNN1D_40': 0.1,
|
| 1642 |
+
'CNN1D_41': 0.1,
|
| 1643 |
+
'CNN1D_42': 0.1,
|
| 1644 |
+
'CNN1D_43': 0.1,
|
| 1645 |
+
'CNN1D_44': 0.1,
|
| 1646 |
+
'CNN1D_45': 0.1,
|
| 1647 |
+
'CNN1D_46': 0.1,
|
| 1648 |
+
'CNN1D_47': 0.1,
|
| 1649 |
+
'CNN1D_48': 0.1,
|
| 1650 |
+
'CNN1D_49': 0.1,
|
| 1651 |
+
'CNN1D_5': 0.1,
|
| 1652 |
+
'CNN1D_50': 0.1,
|
| 1653 |
+
'CNN1D_51': 0.1,
|
| 1654 |
+
'CNN1D_52': 0.1,
|
| 1655 |
+
'CNN1D_53': 0.1,
|
| 1656 |
+
'CNN1D_54': 0.1,
|
| 1657 |
+
'CNN1D_55': 0.1,
|
| 1658 |
+
'CNN1D_56': 0.1,
|
| 1659 |
+
'CNN1D_57': 0.1,
|
| 1660 |
+
'CNN1D_58': 0.1,
|
| 1661 |
+
'CNN1D_59': 0.1,
|
| 1662 |
+
'CNN1D_6': 0.1,
|
| 1663 |
+
'CNN1D_60': 0.1,
|
| 1664 |
+
'CNN1D_61': 0.1,
|
| 1665 |
+
'CNN1D_62': 0.1,
|
| 1666 |
+
'CNN1D_63': 0.1,
|
| 1667 |
+
'CNN1D_7': 0.1,
|
| 1668 |
+
'CNN1D_8': 0.1,
|
| 1669 |
+
'CNN1D_9': 0.1,
|
| 1670 |
+
'cos_latitude': 0.3057,
|
| 1671 |
+
'divergence': 0.08243,
|
| 1672 |
+
'divergence_del2': 629.5,
|
| 1673 |
+
'divergence_dlat': 6.037,
|
| 1674 |
+
'divergence_dlon': 4.505,
|
| 1675 |
+
'geopotential_at_surface': 0.009482,
|
| 1676 |
+
'geopotential_at_surface_del2': 11.13,
|
| 1677 |
+
'geopotential_at_surface_dlat': 0.1337,
|
| 1678 |
+
'geopotential_at_surface_dlon': 0.1168,
|
| 1679 |
+
'land_sea_mask': 0.4503,
|
| 1680 |
+
'learned_positional_features': 1.0,
|
| 1681 |
+
'log_surface_pressure': 0.1123,
|
| 1682 |
+
'log_surface_pressure_del2': 120.6,
|
| 1683 |
+
'log_surface_pressure_dlat': 1.41,
|
| 1684 |
+
'log_surface_pressure_dlon': 1.285,
|
| 1685 |
+
'memory_divergence': 0.08243,
|
| 1686 |
+
'memory_log_surface_pressure': 0.1123,
|
| 1687 |
+
'memory_specific_cloud_ice_water_content': 9.111e-06,
|
| 1688 |
+
'memory_specific_cloud_liquid_water_content': 1.897e-05,
|
| 1689 |
+
'memory_specific_humidity': 0.003298,
|
| 1690 |
+
'memory_temperature_variation': 14.99,
|
| 1691 |
+
'memory_u': 0.01485,
|
| 1692 |
+
'memory_v': 0.01017,
|
| 1693 |
+
'memory_vorticity': 0.2579,
|
| 1694 |
+
'pressure': 1.644,
|
| 1695 |
+
'radiation': 0.2864,
|
| 1696 |
+
'sea_ice_cover': 0.387,
|
| 1697 |
+
'sea_surface_temperature': 11.93,
|
| 1698 |
+
'sin_latitude': 0.7057,
|
| 1699 |
+
'specific_cloud_ice_water_content': 9.111e-06,
|
| 1700 |
+
'specific_cloud_ice_water_content_del2': 0.03864,
|
| 1701 |
+
'specific_cloud_ice_water_content_dlat': 0.0003813,
|
| 1702 |
+
'specific_cloud_ice_water_content_dlon': 0.0003149,
|
| 1703 |
+
'specific_cloud_liquid_water_content': 1.897e-05,
|
| 1704 |
+
'specific_cloud_liquid_water_content_del2': 0.08082,
|
| 1705 |
+
'specific_cloud_liquid_water_content_dlat': 0.0008049,
|
| 1706 |
+
'specific_cloud_liquid_water_content_dlon': 0.0006388,
|
| 1707 |
+
'specific_humidity': 0.003298,
|
| 1708 |
+
'specific_humidity_del2': 2.389,
|
| 1709 |
+
'specific_humidity_dlat': 0.02819,
|
| 1710 |
+
'specific_humidity_dlon': 0.021,
|
| 1711 |
+
'surface_embedding': 1.0,
|
| 1712 |
+
'temperature_variation': 14.99,
|
| 1713 |
+
'temperature_variation_del2': 5793.0,
|
| 1714 |
+
'temperature_variation_dlat': 75.47,
|
| 1715 |
+
'temperature_variation_dlon': 64.98,
|
| 1716 |
+
'u': 0.01485,
|
| 1717 |
+
'u_del2': 13.13,
|
| 1718 |
+
'u_dlat': 1.223,
|
| 1719 |
+
'u_dlon': 0.1378,
|
| 1720 |
+
'v': 0.01017,
|
| 1721 |
+
'v_del2': 11.26,
|
| 1722 |
+
'v_dlat': 0.764,
|
| 1723 |
+
'v_dlon': 0.1781,
|
| 1724 |
+
'vorticity': 0.2579,
|
| 1725 |
+
'vorticity_del2': 1189.0,
|
| 1726 |
+
'vorticity_dlat': 11.67,
|
| 1727 |
+
'vorticity_dlon': 9.514}
|
| 1728 |
+
embedding_model/ShiftAndNormalize.shifts = \
|
| 1729 |
+
{'CNN1D_0': 0.0,
|
| 1730 |
+
'CNN1D_1': 0.0,
|
| 1731 |
+
'CNN1D_10': 0.0,
|
| 1732 |
+
'CNN1D_11': 0.0,
|
| 1733 |
+
'CNN1D_12': 0.0,
|
| 1734 |
+
'CNN1D_13': 0.0,
|
| 1735 |
+
'CNN1D_14': 0.0,
|
| 1736 |
+
'CNN1D_15': 0.0,
|
| 1737 |
+
'CNN1D_16': 0.0,
|
| 1738 |
+
'CNN1D_17': 0.0,
|
| 1739 |
+
'CNN1D_18': 0.0,
|
| 1740 |
+
'CNN1D_19': 0.0,
|
| 1741 |
+
'CNN1D_2': 0.0,
|
| 1742 |
+
'CNN1D_20': 0.0,
|
| 1743 |
+
'CNN1D_21': 0.0,
|
| 1744 |
+
'CNN1D_22': 0.0,
|
| 1745 |
+
'CNN1D_23': 0.0,
|
| 1746 |
+
'CNN1D_24': 0.0,
|
| 1747 |
+
'CNN1D_25': 0.0,
|
| 1748 |
+
'CNN1D_26': 0.0,
|
| 1749 |
+
'CNN1D_27': 0.0,
|
| 1750 |
+
'CNN1D_28': 0.0,
|
| 1751 |
+
'CNN1D_29': 0.0,
|
| 1752 |
+
'CNN1D_3': 0.0,
|
| 1753 |
+
'CNN1D_30': 0.0,
|
| 1754 |
+
'CNN1D_31': 0.0,
|
| 1755 |
+
'CNN1D_32': 0.0,
|
| 1756 |
+
'CNN1D_33': 0.0,
|
| 1757 |
+
'CNN1D_34': 0.0,
|
| 1758 |
+
'CNN1D_35': 0.0,
|
| 1759 |
+
'CNN1D_36': 0.0,
|
| 1760 |
+
'CNN1D_37': 0.0,
|
| 1761 |
+
'CNN1D_38': 0.0,
|
| 1762 |
+
'CNN1D_39': 0.0,
|
| 1763 |
+
'CNN1D_4': 0.0,
|
| 1764 |
+
'CNN1D_40': 0.0,
|
| 1765 |
+
'CNN1D_41': 0.0,
|
| 1766 |
+
'CNN1D_42': 0.0,
|
| 1767 |
+
'CNN1D_43': 0.0,
|
| 1768 |
+
'CNN1D_44': 0.0,
|
| 1769 |
+
'CNN1D_45': 0.0,
|
| 1770 |
+
'CNN1D_46': 0.0,
|
| 1771 |
+
'CNN1D_47': 0.0,
|
| 1772 |
+
'CNN1D_48': 0.0,
|
| 1773 |
+
'CNN1D_49': 0.0,
|
| 1774 |
+
'CNN1D_5': 0.0,
|
| 1775 |
+
'CNN1D_50': 0.0,
|
| 1776 |
+
'CNN1D_51': 0.0,
|
| 1777 |
+
'CNN1D_52': 0.0,
|
| 1778 |
+
'CNN1D_53': 0.0,
|
| 1779 |
+
'CNN1D_54': 0.0,
|
| 1780 |
+
'CNN1D_55': 0.0,
|
| 1781 |
+
'CNN1D_56': 0.0,
|
| 1782 |
+
'CNN1D_57': 0.0,
|
| 1783 |
+
'CNN1D_58': 0.0,
|
| 1784 |
+
'CNN1D_59': 0.0,
|
| 1785 |
+
'CNN1D_6': 0.0,
|
| 1786 |
+
'CNN1D_60': 0.0,
|
| 1787 |
+
'CNN1D_61': 0.0,
|
| 1788 |
+
'CNN1D_62': 0.0,
|
| 1789 |
+
'CNN1D_63': 0.0,
|
| 1790 |
+
'CNN1D_7': 0.0,
|
| 1791 |
+
'CNN1D_8': 0.0,
|
| 1792 |
+
'CNN1D_9': 0.0,
|
| 1793 |
+
'cos_latitude': 0.639,
|
| 1794 |
+
'divergence': -0.0,
|
| 1795 |
+
'divergence_del2': -0.009,
|
| 1796 |
+
'divergence_dlat': -0.001,
|
| 1797 |
+
'divergence_dlon': 0.0,
|
| 1798 |
+
'geopotential_at_surface': 0.004,
|
| 1799 |
+
'geopotential_at_surface_del2': -0.069,
|
| 1800 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1801 |
+
'geopotential_at_surface_dlon': 0.0,
|
| 1802 |
+
'land_sea_mask': 0.335,
|
| 1803 |
+
'learned_positional_features': 0.0,
|
| 1804 |
+
'log_surface_pressure': 1.716,
|
| 1805 |
+
'log_surface_pressure_del2': 0.824,
|
| 1806 |
+
'log_surface_pressure_dlat': 0.13,
|
| 1807 |
+
'log_surface_pressure_dlon': 0.0,
|
| 1808 |
+
'memory_divergence': -0.0,
|
| 1809 |
+
'memory_log_surface_pressure': 1.716,
|
| 1810 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 1811 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 1812 |
+
'memory_specific_humidity': 0.003298,
|
| 1813 |
+
'memory_temperature_variation': -4.946,
|
| 1814 |
+
'memory_u': 0.007,
|
| 1815 |
+
'memory_v': -0.0,
|
| 1816 |
+
'memory_vorticity': -0.002,
|
| 1817 |
+
'pressure': 2.796,
|
| 1818 |
+
'radiation': 0.214,
|
| 1819 |
+
'sea_ice_cover': 0.24,
|
| 1820 |
+
'sea_surface_temperature': 285.14,
|
| 1821 |
+
'sin_latitude': -0.0,
|
| 1822 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1823 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 1824 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1825 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1826 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1827 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1828 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1829 |
+
'specific_cloud_liquid_water_content_dlon': 0.0,
|
| 1830 |
+
'specific_humidity': 0.0,
|
| 1831 |
+
'specific_humidity_del2': 0.0,
|
| 1832 |
+
'specific_humidity_dlat': 0.0,
|
| 1833 |
+
'specific_humidity_dlon': 0.0,
|
| 1834 |
+
'surface_embedding': 0.0,
|
| 1835 |
+
'temperature_variation': -4.946,
|
| 1836 |
+
'temperature_variation_del2': 47.3,
|
| 1837 |
+
'temperature_variation_dlat': 4.913,
|
| 1838 |
+
'temperature_variation_dlon': -0.0,
|
| 1839 |
+
'u': 0.007,
|
| 1840 |
+
'u_del2': 0.187,
|
| 1841 |
+
'u_dlat': 0.002,
|
| 1842 |
+
'u_dlon': -0.0,
|
| 1843 |
+
'v': -0.0,
|
| 1844 |
+
'v_del2': -0.002,
|
| 1845 |
+
'v_dlat': -0.0,
|
| 1846 |
+
'v_dlon': 0.0,
|
| 1847 |
+
'vorticity': -0.002,
|
| 1848 |
+
'vorticity_del2': -0.37,
|
| 1849 |
+
'vorticity_dlat': 0.042,
|
| 1850 |
+
'vorticity_dlon': -0.0}
|
| 1851 |
+
|
| 1852 |
+
# Parameters for encoder_data/ShiftAndNormalize:
|
| 1853 |
+
# ==============================================================================
|
| 1854 |
+
encoder_data/ShiftAndNormalize.features_to_exclude = ()
|
| 1855 |
+
encoder_data/ShiftAndNormalize.global_scale = None
|
| 1856 |
+
encoder_data/ShiftAndNormalize.name = None
|
| 1857 |
+
encoder_data/ShiftAndNormalize.scales = \
|
| 1858 |
+
{'cos_latitude': 0.3057,
|
| 1859 |
+
'geopotential_at_surface': 0.009482,
|
| 1860 |
+
'geopotential_at_surface_del2': 11.13,
|
| 1861 |
+
'geopotential_at_surface_dlat': 0.1337,
|
| 1862 |
+
'geopotential_at_surface_dlon': 0.1168,
|
| 1863 |
+
'land_sea_mask': 0.4503,
|
| 1864 |
+
'learned_positional_features': 1.0,
|
| 1865 |
+
'radiation': 0.2864,
|
| 1866 |
+
'sin_latitude': 0.7057,
|
| 1867 |
+
'specific_cloud_ice_water_content': 7.538e-06,
|
| 1868 |
+
'specific_cloud_ice_water_content_del2': 0.03283,
|
| 1869 |
+
'specific_cloud_ice_water_content_dlat': 0.0003762,
|
| 1870 |
+
'specific_cloud_ice_water_content_dlon': 0.0002613,
|
| 1871 |
+
'specific_cloud_liquid_water_content': 1.979e-05,
|
| 1872 |
+
'specific_cloud_liquid_water_content_del2': 0.08809,
|
| 1873 |
+
'specific_cloud_liquid_water_content_dlat': 0.001038,
|
| 1874 |
+
'specific_cloud_liquid_water_content_dlon': 0.000684,
|
| 1875 |
+
'specific_humidity': 0.003487,
|
| 1876 |
+
'specific_humidity_del2': 2.381,
|
| 1877 |
+
'specific_humidity_dlat': 0.03179,
|
| 1878 |
+
'specific_humidity_dlon': 0.02059,
|
| 1879 |
+
't': 28.0,
|
| 1880 |
+
't_del2': 4397.0,
|
| 1881 |
+
't_dlat': 64.14,
|
| 1882 |
+
't_dlon': 50.07,
|
| 1883 |
+
'u': 0.01933,
|
| 1884 |
+
'u_del2': 12.27,
|
| 1885 |
+
'u_dlat': 0.1743,
|
| 1886 |
+
'u_dlon': 0.1296,
|
| 1887 |
+
'v': 0.01033,
|
| 1888 |
+
'v_del2': 12.08,
|
| 1889 |
+
'v_dlat': 0.1152,
|
| 1890 |
+
'v_dlon': 0.1617,
|
| 1891 |
+
'z': 0.1496,
|
| 1892 |
+
'z_del2': 0.332,
|
| 1893 |
+
'z_dlat': 0.01598,
|
| 1894 |
+
'z_dlon': 0.008823}
|
| 1895 |
+
encoder_data/ShiftAndNormalize.shifts = \
|
| 1896 |
+
{'cos_latitude': 0.639,
|
| 1897 |
+
'geopotential_at_surface': 0.004,
|
| 1898 |
+
'geopotential_at_surface_del2': -0.069,
|
| 1899 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 1900 |
+
'geopotential_at_surface_dlon': 0.0,
|
| 1901 |
+
'land_sea_mask': 0.335,
|
| 1902 |
+
'learned_positional_features': 0.0,
|
| 1903 |
+
'radiation': 0.214,
|
| 1904 |
+
'sin_latitude': -0.0,
|
| 1905 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 1906 |
+
'specific_cloud_ice_water_content_del2': -0.0,
|
| 1907 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 1908 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 1909 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 1910 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 1911 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 1912 |
+
'specific_cloud_liquid_water_content_dlon': -0.0,
|
| 1913 |
+
'specific_humidity': 0.0,
|
| 1914 |
+
'specific_humidity_del2': 0.0,
|
| 1915 |
+
'specific_humidity_dlat': 0.0,
|
| 1916 |
+
'specific_humidity_dlon': 0.0,
|
| 1917 |
+
't': 247.07,
|
| 1918 |
+
't_del2': 35.964,
|
| 1919 |
+
't_dlat': 1.763,
|
| 1920 |
+
't_dlon': -0.0,
|
| 1921 |
+
'u': 0.007,
|
| 1922 |
+
'u_del2': 0.045,
|
| 1923 |
+
'u_dlat': -0.0,
|
| 1924 |
+
'u_dlon': 0.0,
|
| 1925 |
+
'v': 0.0,
|
| 1926 |
+
'v_del2': 0.005,
|
| 1927 |
+
'v_dlat': 0.0,
|
| 1928 |
+
'v_dlon': 0.0,
|
| 1929 |
+
'z': 0.145,
|
| 1930 |
+
'z_del2': 0.011,
|
| 1931 |
+
'z_dlat': 0.001,
|
| 1932 |
+
'z_dlon': 0.0}
|
| 1933 |
+
|
| 1934 |
+
# Parameters for sea_model/ShiftAndNormalize:
|
| 1935 |
+
# ==============================================================================
|
| 1936 |
+
sea_model/ShiftAndNormalize.features_to_exclude = ()
|
| 1937 |
+
sea_model/ShiftAndNormalize.global_scale = None
|
| 1938 |
+
sea_model/ShiftAndNormalize.name = None
|
| 1939 |
+
sea_model/ShiftAndNormalize.scales = \
|
| 1940 |
+
{'CNN1D_0': 0.1,
|
| 1941 |
+
'CNN1D_1': 0.1,
|
| 1942 |
+
'CNN1D_10': 0.1,
|
| 1943 |
+
'CNN1D_11': 0.1,
|
| 1944 |
+
'CNN1D_12': 0.1,
|
| 1945 |
+
'CNN1D_13': 0.1,
|
| 1946 |
+
'CNN1D_14': 0.1,
|
| 1947 |
+
'CNN1D_15': 0.1,
|
| 1948 |
+
'CNN1D_16': 0.1,
|
| 1949 |
+
'CNN1D_17': 0.1,
|
| 1950 |
+
'CNN1D_18': 0.1,
|
| 1951 |
+
'CNN1D_19': 0.1,
|
| 1952 |
+
'CNN1D_2': 0.1,
|
| 1953 |
+
'CNN1D_20': 0.1,
|
| 1954 |
+
'CNN1D_21': 0.1,
|
| 1955 |
+
'CNN1D_22': 0.1,
|
| 1956 |
+
'CNN1D_23': 0.1,
|
| 1957 |
+
'CNN1D_24': 0.1,
|
| 1958 |
+
'CNN1D_25': 0.1,
|
| 1959 |
+
'CNN1D_26': 0.1,
|
| 1960 |
+
'CNN1D_27': 0.1,
|
| 1961 |
+
'CNN1D_28': 0.1,
|
| 1962 |
+
'CNN1D_29': 0.1,
|
| 1963 |
+
'CNN1D_3': 0.1,
|
| 1964 |
+
'CNN1D_30': 0.1,
|
| 1965 |
+
'CNN1D_31': 0.1,
|
| 1966 |
+
'CNN1D_32': 0.1,
|
| 1967 |
+
'CNN1D_33': 0.1,
|
| 1968 |
+
'CNN1D_34': 0.1,
|
| 1969 |
+
'CNN1D_35': 0.1,
|
| 1970 |
+
'CNN1D_36': 0.1,
|
| 1971 |
+
'CNN1D_37': 0.1,
|
| 1972 |
+
'CNN1D_38': 0.1,
|
| 1973 |
+
'CNN1D_39': 0.1,
|
| 1974 |
+
'CNN1D_4': 0.1,
|
| 1975 |
+
'CNN1D_40': 0.1,
|
| 1976 |
+
'CNN1D_41': 0.1,
|
| 1977 |
+
'CNN1D_42': 0.1,
|
| 1978 |
+
'CNN1D_43': 0.1,
|
| 1979 |
+
'CNN1D_44': 0.1,
|
| 1980 |
+
'CNN1D_45': 0.1,
|
| 1981 |
+
'CNN1D_46': 0.1,
|
| 1982 |
+
'CNN1D_47': 0.1,
|
| 1983 |
+
'CNN1D_48': 0.1,
|
| 1984 |
+
'CNN1D_49': 0.1,
|
| 1985 |
+
'CNN1D_5': 0.1,
|
| 1986 |
+
'CNN1D_50': 0.1,
|
| 1987 |
+
'CNN1D_51': 0.1,
|
| 1988 |
+
'CNN1D_52': 0.1,
|
| 1989 |
+
'CNN1D_53': 0.1,
|
| 1990 |
+
'CNN1D_54': 0.1,
|
| 1991 |
+
'CNN1D_55': 0.1,
|
| 1992 |
+
'CNN1D_56': 0.1,
|
| 1993 |
+
'CNN1D_57': 0.1,
|
| 1994 |
+
'CNN1D_58': 0.1,
|
| 1995 |
+
'CNN1D_59': 0.1,
|
| 1996 |
+
'CNN1D_6': 0.1,
|
| 1997 |
+
'CNN1D_60': 0.1,
|
| 1998 |
+
'CNN1D_61': 0.1,
|
| 1999 |
+
'CNN1D_62': 0.1,
|
| 2000 |
+
'CNN1D_63': 0.1,
|
| 2001 |
+
'CNN1D_7': 0.1,
|
| 2002 |
+
'CNN1D_8': 0.1,
|
| 2003 |
+
'CNN1D_9': 0.1,
|
| 2004 |
+
'cos_latitude': 0.3057,
|
| 2005 |
+
'divergence': 0.08243,
|
| 2006 |
+
'divergence_del2': 629.5,
|
| 2007 |
+
'divergence_dlat': 6.037,
|
| 2008 |
+
'divergence_dlon': 4.505,
|
| 2009 |
+
'geopotential_at_surface': 0.009482,
|
| 2010 |
+
'geopotential_at_surface_del2': 11.13,
|
| 2011 |
+
'geopotential_at_surface_dlat': 0.1337,
|
| 2012 |
+
'geopotential_at_surface_dlon': 0.1168,
|
| 2013 |
+
'land_sea_mask': 0.4503,
|
| 2014 |
+
'learned_positional_features': 1.0,
|
| 2015 |
+
'log_surface_pressure': 0.1123,
|
| 2016 |
+
'log_surface_pressure_del2': 120.6,
|
| 2017 |
+
'log_surface_pressure_dlat': 1.41,
|
| 2018 |
+
'log_surface_pressure_dlon': 1.285,
|
| 2019 |
+
'memory_divergence': 0.08243,
|
| 2020 |
+
'memory_log_surface_pressure': 0.1123,
|
| 2021 |
+
'memory_specific_cloud_ice_water_content': 9.111e-06,
|
| 2022 |
+
'memory_specific_cloud_liquid_water_content': 1.897e-05,
|
| 2023 |
+
'memory_specific_humidity': 0.003298,
|
| 2024 |
+
'memory_temperature_variation': 14.99,
|
| 2025 |
+
'memory_u': 0.01485,
|
| 2026 |
+
'memory_v': 0.01017,
|
| 2027 |
+
'memory_vorticity': 0.2579,
|
| 2028 |
+
'pressure': 1.644,
|
| 2029 |
+
'radiation': 0.2864,
|
| 2030 |
+
'sea_ice_cover': 0.387,
|
| 2031 |
+
'sea_surface_temperature': 11.93,
|
| 2032 |
+
'sin_latitude': 0.7057,
|
| 2033 |
+
'specific_cloud_ice_water_content': 9.111e-06,
|
| 2034 |
+
'specific_cloud_ice_water_content_del2': 0.03864,
|
| 2035 |
+
'specific_cloud_ice_water_content_dlat': 0.0003813,
|
| 2036 |
+
'specific_cloud_ice_water_content_dlon': 0.0003149,
|
| 2037 |
+
'specific_cloud_liquid_water_content': 1.897e-05,
|
| 2038 |
+
'specific_cloud_liquid_water_content_del2': 0.08082,
|
| 2039 |
+
'specific_cloud_liquid_water_content_dlat': 0.0008049,
|
| 2040 |
+
'specific_cloud_liquid_water_content_dlon': 0.0006388,
|
| 2041 |
+
'specific_humidity': 0.003298,
|
| 2042 |
+
'specific_humidity_del2': 2.389,
|
| 2043 |
+
'specific_humidity_dlat': 0.02819,
|
| 2044 |
+
'specific_humidity_dlon': 0.021,
|
| 2045 |
+
'surface_embedding': 1.0,
|
| 2046 |
+
'temperature_variation': 14.99,
|
| 2047 |
+
'temperature_variation_del2': 5793.0,
|
| 2048 |
+
'temperature_variation_dlat': 75.47,
|
| 2049 |
+
'temperature_variation_dlon': 64.98,
|
| 2050 |
+
'u': 0.01485,
|
| 2051 |
+
'u_del2': 13.13,
|
| 2052 |
+
'u_dlat': 1.223,
|
| 2053 |
+
'u_dlon': 0.1378,
|
| 2054 |
+
'v': 0.01017,
|
| 2055 |
+
'v_del2': 11.26,
|
| 2056 |
+
'v_dlat': 0.764,
|
| 2057 |
+
'v_dlon': 0.1781,
|
| 2058 |
+
'vorticity': 0.2579,
|
| 2059 |
+
'vorticity_del2': 1189.0,
|
| 2060 |
+
'vorticity_dlat': 11.67,
|
| 2061 |
+
'vorticity_dlon': 9.514}
|
| 2062 |
+
sea_model/ShiftAndNormalize.shifts = \
|
| 2063 |
+
{'CNN1D_0': 0.0,
|
| 2064 |
+
'CNN1D_1': 0.0,
|
| 2065 |
+
'CNN1D_10': 0.0,
|
| 2066 |
+
'CNN1D_11': 0.0,
|
| 2067 |
+
'CNN1D_12': 0.0,
|
| 2068 |
+
'CNN1D_13': 0.0,
|
| 2069 |
+
'CNN1D_14': 0.0,
|
| 2070 |
+
'CNN1D_15': 0.0,
|
| 2071 |
+
'CNN1D_16': 0.0,
|
| 2072 |
+
'CNN1D_17': 0.0,
|
| 2073 |
+
'CNN1D_18': 0.0,
|
| 2074 |
+
'CNN1D_19': 0.0,
|
| 2075 |
+
'CNN1D_2': 0.0,
|
| 2076 |
+
'CNN1D_20': 0.0,
|
| 2077 |
+
'CNN1D_21': 0.0,
|
| 2078 |
+
'CNN1D_22': 0.0,
|
| 2079 |
+
'CNN1D_23': 0.0,
|
| 2080 |
+
'CNN1D_24': 0.0,
|
| 2081 |
+
'CNN1D_25': 0.0,
|
| 2082 |
+
'CNN1D_26': 0.0,
|
| 2083 |
+
'CNN1D_27': 0.0,
|
| 2084 |
+
'CNN1D_28': 0.0,
|
| 2085 |
+
'CNN1D_29': 0.0,
|
| 2086 |
+
'CNN1D_3': 0.0,
|
| 2087 |
+
'CNN1D_30': 0.0,
|
| 2088 |
+
'CNN1D_31': 0.0,
|
| 2089 |
+
'CNN1D_32': 0.0,
|
| 2090 |
+
'CNN1D_33': 0.0,
|
| 2091 |
+
'CNN1D_34': 0.0,
|
| 2092 |
+
'CNN1D_35': 0.0,
|
| 2093 |
+
'CNN1D_36': 0.0,
|
| 2094 |
+
'CNN1D_37': 0.0,
|
| 2095 |
+
'CNN1D_38': 0.0,
|
| 2096 |
+
'CNN1D_39': 0.0,
|
| 2097 |
+
'CNN1D_4': 0.0,
|
| 2098 |
+
'CNN1D_40': 0.0,
|
| 2099 |
+
'CNN1D_41': 0.0,
|
| 2100 |
+
'CNN1D_42': 0.0,
|
| 2101 |
+
'CNN1D_43': 0.0,
|
| 2102 |
+
'CNN1D_44': 0.0,
|
| 2103 |
+
'CNN1D_45': 0.0,
|
| 2104 |
+
'CNN1D_46': 0.0,
|
| 2105 |
+
'CNN1D_47': 0.0,
|
| 2106 |
+
'CNN1D_48': 0.0,
|
| 2107 |
+
'CNN1D_49': 0.0,
|
| 2108 |
+
'CNN1D_5': 0.0,
|
| 2109 |
+
'CNN1D_50': 0.0,
|
| 2110 |
+
'CNN1D_51': 0.0,
|
| 2111 |
+
'CNN1D_52': 0.0,
|
| 2112 |
+
'CNN1D_53': 0.0,
|
| 2113 |
+
'CNN1D_54': 0.0,
|
| 2114 |
+
'CNN1D_55': 0.0,
|
| 2115 |
+
'CNN1D_56': 0.0,
|
| 2116 |
+
'CNN1D_57': 0.0,
|
| 2117 |
+
'CNN1D_58': 0.0,
|
| 2118 |
+
'CNN1D_59': 0.0,
|
| 2119 |
+
'CNN1D_6': 0.0,
|
| 2120 |
+
'CNN1D_60': 0.0,
|
| 2121 |
+
'CNN1D_61': 0.0,
|
| 2122 |
+
'CNN1D_62': 0.0,
|
| 2123 |
+
'CNN1D_63': 0.0,
|
| 2124 |
+
'CNN1D_7': 0.0,
|
| 2125 |
+
'CNN1D_8': 0.0,
|
| 2126 |
+
'CNN1D_9': 0.0,
|
| 2127 |
+
'cos_latitude': 0.639,
|
| 2128 |
+
'divergence': -0.0,
|
| 2129 |
+
'divergence_del2': -0.009,
|
| 2130 |
+
'divergence_dlat': -0.001,
|
| 2131 |
+
'divergence_dlon': 0.0,
|
| 2132 |
+
'geopotential_at_surface': 0.004,
|
| 2133 |
+
'geopotential_at_surface_del2': -0.069,
|
| 2134 |
+
'geopotential_at_surface_dlat': -0.01,
|
| 2135 |
+
'geopotential_at_surface_dlon': 0.0,
|
| 2136 |
+
'land_sea_mask': 0.335,
|
| 2137 |
+
'learned_positional_features': 0.0,
|
| 2138 |
+
'log_surface_pressure': 1.716,
|
| 2139 |
+
'log_surface_pressure_del2': 0.824,
|
| 2140 |
+
'log_surface_pressure_dlat': 0.13,
|
| 2141 |
+
'log_surface_pressure_dlon': 0.0,
|
| 2142 |
+
'memory_divergence': -0.0,
|
| 2143 |
+
'memory_log_surface_pressure': 1.716,
|
| 2144 |
+
'memory_specific_cloud_ice_water_content': 0.0,
|
| 2145 |
+
'memory_specific_cloud_liquid_water_content': 0.0,
|
| 2146 |
+
'memory_specific_humidity': 0.003298,
|
| 2147 |
+
'memory_temperature_variation': -4.946,
|
| 2148 |
+
'memory_u': 0.007,
|
| 2149 |
+
'memory_v': -0.0,
|
| 2150 |
+
'memory_vorticity': -0.002,
|
| 2151 |
+
'pressure': 2.796,
|
| 2152 |
+
'radiation': 0.214,
|
| 2153 |
+
'sea_ice_cover': 0.24,
|
| 2154 |
+
'sea_surface_temperature': 285.14,
|
| 2155 |
+
'sin_latitude': -0.0,
|
| 2156 |
+
'specific_cloud_ice_water_content': 0.0,
|
| 2157 |
+
'specific_cloud_ice_water_content_del2': 0.0,
|
| 2158 |
+
'specific_cloud_ice_water_content_dlat': 0.0,
|
| 2159 |
+
'specific_cloud_ice_water_content_dlon': -0.0,
|
| 2160 |
+
'specific_cloud_liquid_water_content': 0.0,
|
| 2161 |
+
'specific_cloud_liquid_water_content_del2': -0.0,
|
| 2162 |
+
'specific_cloud_liquid_water_content_dlat': 0.0,
|
| 2163 |
+
'specific_cloud_liquid_water_content_dlon': 0.0,
|
| 2164 |
+
'specific_humidity': 0.0,
|
| 2165 |
+
'specific_humidity_del2': 0.0,
|
| 2166 |
+
'specific_humidity_dlat': 0.0,
|
| 2167 |
+
'specific_humidity_dlon': 0.0,
|
| 2168 |
+
'surface_embedding': 0.0,
|
| 2169 |
+
'temperature_variation': -4.946,
|
| 2170 |
+
'temperature_variation_del2': 47.3,
|
| 2171 |
+
'temperature_variation_dlat': 4.913,
|
| 2172 |
+
'temperature_variation_dlon': -0.0,
|
| 2173 |
+
'u': 0.007,
|
| 2174 |
+
'u_del2': 0.187,
|
| 2175 |
+
'u_dlat': 0.002,
|
| 2176 |
+
'u_dlon': -0.0,
|
| 2177 |
+
'v': -0.0,
|
| 2178 |
+
'v_del2': -0.002,
|
| 2179 |
+
'v_dlat': -0.0,
|
| 2180 |
+
'v_dlon': 0.0,
|
| 2181 |
+
'vorticity': -0.002,
|
| 2182 |
+
'vorticity_del2': -0.37,
|
| 2183 |
+
'vorticity_dlat': 0.042,
|
| 2184 |
+
'vorticity_dlon': -0.0}
|
| 2185 |
+
|
| 2186 |
+
# Parameters for SigmaCoordinatesEquidistant:
|
| 2187 |
+
# ==============================================================================
|
| 2188 |
+
SigmaCoordinatesEquidistant.layers = %N_SIGMA_LAYERS
|
| 2189 |
+
|
| 2190 |
+
# Parameters for custom_corrds/SigmaCoordinatesEquidistant:
|
| 2191 |
+
# ==============================================================================
|
| 2192 |
+
custom_corrds/SigmaCoordinatesEquidistant.layers = %N_SIGMA_LAYERS
|
| 2193 |
+
|
| 2194 |
+
# Parameters for advance/SoftClip:
|
| 2195 |
+
# ==============================================================================
|
| 2196 |
+
advance/SoftClip.hinge_softness = 1.0
|
| 2197 |
+
advance/SoftClip.max_value = 16
|
| 2198 |
+
advance/SoftClip.name = None
|
| 2199 |
+
|
| 2200 |
+
# Parameters for StochasticModularStepModel:
|
| 2201 |
+
# ==============================================================================
|
| 2202 |
+
StochasticModularStepModel.advance_module = @StochasticPhysicsParameterizationStep
|
| 2203 |
+
StochasticModularStepModel.decoder_module = \
|
| 2204 |
+
@DimensionalLearnedPrimitiveToWeatherbenchDecoder
|
| 2205 |
+
StochasticModularStepModel.encoder_module = \
|
| 2206 |
+
@DimensionalLearnedWeatherbenchToPrimitiveWithMemoryEncoder
|
| 2207 |
+
StochasticModularStepModel.forcing_module = @DynamicDataForcing
|
| 2208 |
+
StochasticModularStepModel.name = None
|
| 2209 |
+
|
| 2210 |
+
# Parameters for StochasticPhysicsParameterizationStep:
|
| 2211 |
+
# ==============================================================================
|
| 2212 |
+
StochasticPhysicsParameterizationStep.checkpoint_substep = False
|
| 2213 |
+
StochasticPhysicsParameterizationStep.corrector_module = %CORRECTOR_MODULE
|
| 2214 |
+
StochasticPhysicsParameterizationStep.name = None
|
| 2215 |
+
StochasticPhysicsParameterizationStep.num_substeps = %NUM_SUBSTEPS
|
| 2216 |
+
StochasticPhysicsParameterizationStep.physics_parameterization_module = \
|
| 2217 |
+
@DivCurlNeuralParameterization
|
| 2218 |
+
StochasticPhysicsParameterizationStep.randomness_module = @ZerosRandomField
|
| 2219 |
+
|
| 2220 |
+
# Parameters for advance/ToModalDiffOperators:
|
| 2221 |
+
# ==============================================================================
|
| 2222 |
+
advance/ToModalDiffOperators.name = None
|
| 2223 |
+
|
| 2224 |
+
# Parameters for encoder_data/ToModalDiffOperators:
|
| 2225 |
+
# ==============================================================================
|
| 2226 |
+
encoder_data/ToModalDiffOperators.name = None
|
| 2227 |
+
|
| 2228 |
+
# Parameters for with_grads/ToModalDiffOperators:
|
| 2229 |
+
# ==============================================================================
|
| 2230 |
+
with_grads/ToModalDiffOperators.name = None
|
| 2231 |
+
|
| 2232 |
+
# Parameters for trajectory_from_step:
|
| 2233 |
+
# ==============================================================================
|
| 2234 |
+
trajectory_from_step.checkpoint_multistep = False
|
| 2235 |
+
trajectory_from_step.checkpoint_post_process = True
|
| 2236 |
+
trajectory_from_step.checkpoint_step = True
|
| 2237 |
+
|
| 2238 |
+
# Parameters for advance/TruncateSigmaLevels:
|
| 2239 |
+
# ==============================================================================
|
| 2240 |
+
advance/TruncateSigmaLevels.name = None
|
| 2241 |
+
advance/TruncateSigmaLevels.sigma_ranges = \
|
| 2242 |
+
{'divergence': (0.0, 1),
|
| 2243 |
+
'geopotential': (0.0, 1),
|
| 2244 |
+
'specific_cloud_ice_water_content': (0.0, 1),
|
| 2245 |
+
'specific_cloud_liquid_water_content': (0.0, 1),
|
| 2246 |
+
'specific_humidity': (0.0, 1),
|
| 2247 |
+
't': (0.0, 1),
|
| 2248 |
+
'temperature_variation': (0.0, 1),
|
| 2249 |
+
'u': (0.0, 1),
|
| 2250 |
+
'u_component_of_wind': (0.0, 1),
|
| 2251 |
+
'v': (0.0, 1),
|
| 2252 |
+
'v_component_of_wind': (0.0, 1),
|
| 2253 |
+
'vorticity': (0.0, 1),
|
| 2254 |
+
'z': (0.0, 1)}
|
| 2255 |
+
|
| 2256 |
+
# Parameters for decoder_model/TruncateSigmaLevels:
|
| 2257 |
+
# ==============================================================================
|
| 2258 |
+
decoder_model/TruncateSigmaLevels.name = None
|
| 2259 |
+
decoder_model/TruncateSigmaLevels.sigma_ranges = \
|
| 2260 |
+
{'divergence': (0.0, 1),
|
| 2261 |
+
'geopotential': (0.0, 1),
|
| 2262 |
+
'specific_cloud_ice_water_content': (0.0, 1),
|
| 2263 |
+
'specific_cloud_liquid_water_content': (0.0, 1),
|
| 2264 |
+
'specific_humidity': (0.0, 1),
|
| 2265 |
+
't': (0.0, 1),
|
| 2266 |
+
'temperature_variation': (0.0, 1),
|
| 2267 |
+
'u': (0.0, 1),
|
| 2268 |
+
'u_component_of_wind': (0.0, 1),
|
| 2269 |
+
'v': (0.0, 1),
|
| 2270 |
+
'v_component_of_wind': (0.0, 1),
|
| 2271 |
+
'vorticity': (0.0, 1),
|
| 2272 |
+
'z': (0.0, 1)}
|
| 2273 |
+
|
| 2274 |
+
# Parameters for advance/VelocityAndPrognostics:
|
| 2275 |
+
# ==============================================================================
|
| 2276 |
+
advance/VelocityAndPrognostics.compute_gradients_module = @ToModalDiffOperators
|
| 2277 |
+
advance/VelocityAndPrognostics.fields_to_include = \
|
| 2278 |
+
['divergence',
|
| 2279 |
+
'vorticity',
|
| 2280 |
+
'u',
|
| 2281 |
+
'v',
|
| 2282 |
+
'temperature_variation',
|
| 2283 |
+
'log_surface_pressure',
|
| 2284 |
+
'specific_humidity',
|
| 2285 |
+
'specific_cloud_liquid_water_content',
|
| 2286 |
+
'specific_cloud_ice_water_content']
|
| 2287 |
+
advance/VelocityAndPrognostics.name = None
|
| 2288 |
+
|
| 2289 |
+
# Parameters for embedding_model/VelocityAndPrognostics:
|
| 2290 |
+
# ==============================================================================
|
| 2291 |
+
embedding_model/VelocityAndPrognostics.fields_to_include = \
|
| 2292 |
+
['divergence',
|
| 2293 |
+
'vorticity',
|
| 2294 |
+
'u',
|
| 2295 |
+
'v',
|
| 2296 |
+
'temperature_variation',
|
| 2297 |
+
'specific_humidity',
|
| 2298 |
+
'specific_cloud_liquid_water_content',
|
| 2299 |
+
'specific_cloud_ice_water_content']
|
| 2300 |
+
embedding_model/VelocityAndPrognostics.name = None
|
| 2301 |
+
|
| 2302 |
+
# Parameters for encoder_data/VelocityAndPrognostics:
|
| 2303 |
+
# ==============================================================================
|
| 2304 |
+
encoder_data/VelocityAndPrognostics.compute_gradients_module = \
|
| 2305 |
+
@ToModalDiffOperators
|
| 2306 |
+
encoder_data/VelocityAndPrognostics.fields_to_include = \
|
| 2307 |
+
['u',
|
| 2308 |
+
'v',
|
| 2309 |
+
't',
|
| 2310 |
+
'z',
|
| 2311 |
+
'specific_humidity',
|
| 2312 |
+
'specific_cloud_liquid_water_content',
|
| 2313 |
+
'specific_cloud_ice_water_content']
|
| 2314 |
+
encoder_data/VelocityAndPrognostics.name = None
|
| 2315 |
+
|
| 2316 |
+
# Parameters for model/VelocityAndPrognostics:
|
| 2317 |
+
# ==============================================================================
|
| 2318 |
+
model/VelocityAndPrognostics.fields_to_include = None
|
| 2319 |
+
model/VelocityAndPrognostics.name = None
|
| 2320 |
+
|
| 2321 |
+
# Parameters for VerticalConvTower:
|
| 2322 |
+
# ==============================================================================
|
| 2323 |
+
VerticalConvTower.activate_final = False
|
| 2324 |
+
VerticalConvTower.activation = %ACTIVATION
|
| 2325 |
+
VerticalConvTower.channels = [64, 64, 64, 64]
|
| 2326 |
+
VerticalConvTower.checkpoint_tower = True
|
| 2327 |
+
VerticalConvTower.kernel_shape = 5
|
| 2328 |
+
VerticalConvTower.name = None
|
| 2329 |
+
VerticalConvTower.with_bias = True
|
| 2330 |
+
|
| 2331 |
+
# Parameters for WhirlModel:
|
| 2332 |
+
# ==============================================================================
|
| 2333 |
+
WhirlModel.from_xarray_fn = @xarray_to_state_and_dynamic_covariate_data
|
| 2334 |
+
WhirlModel.model_cls = @StochasticModularStepModel
|
| 2335 |
+
WhirlModel.to_xarray_fn = @data_to_xarray_with_renaming
|
| 2336 |
+
|
| 2337 |
+
# Parameters for xarray_to_data_with_renaming:
|
| 2338 |
+
# ==============================================================================
|
| 2339 |
+
xarray_to_data_with_renaming.renaming_dict = \
|
| 2340 |
+
{'geopotential': 'z',
|
| 2341 |
+
'latitude': 'lat',
|
| 2342 |
+
'longitude': 'lon',
|
| 2343 |
+
'temperature': 't',
|
| 2344 |
+
'u_component_of_wind': 'u',
|
| 2345 |
+
'v_component_of_wind': 'v'}
|
| 2346 |
+
xarray_to_data_with_renaming.xarray_to_data_fn = @xarray_to_weatherbench_data
|
| 2347 |
+
|
| 2348 |
+
# Parameters for xarray_to_dynamic_covariate_data:
|
| 2349 |
+
# ==============================================================================
|
| 2350 |
+
xarray_to_dynamic_covariate_data.covariates_to_include = \
|
| 2351 |
+
('sea_ice_cover', 'sea_surface_temperature')
|
| 2352 |
+
|
| 2353 |
+
# Parameters for xarray_to_state_and_dynamic_covariate_data:
|
| 2354 |
+
# ==============================================================================
|
| 2355 |
+
xarray_to_state_and_dynamic_covariate_data.values = 'values'
|
| 2356 |
+
xarray_to_state_and_dynamic_covariate_data.xarray_to_dynamic_covariate_data_fn = \
|
| 2357 |
+
@xarray_to_dynamic_covariate_data
|
| 2358 |
+
xarray_to_state_and_dynamic_covariate_data.xarray_to_state_data_fn = \
|
| 2359 |
+
@xarray_to_data_with_renaming
|
| 2360 |
+
|
| 2361 |
+
# Parameters for xarray_to_weatherbench_data:
|
| 2362 |
+
# ==============================================================================
|
| 2363 |
+
xarray_to_weatherbench_data.diagnostics_to_include = ()
|
| 2364 |
+
xarray_to_weatherbench_data.tracers_to_include = \
|
| 2365 |
+
('specific_humidity',
|
| 2366 |
+
'specific_cloud_liquid_water_content',
|
| 2367 |
+
'specific_cloud_ice_water_content')
|
| 2368 |
+
|
| 2369 |
+
# Parameters for ZerosRandomField:
|
| 2370 |
+
# ==============================================================================
|
| 2371 |
+
ZerosRandomField.prefer_nodal = True
|
model/reference_code/paper_configs/deterministic_2_8_deg.gin
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model/reference_code/paper_configs/stochastic_1_4_deg.gin
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model/reference_code/reader.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
# pylint: disable=line-too-long
|
| 15 |
+
# pyformat: disable
|
| 16 |
+
"""Xarray based readers for feeding time-series into tf.data."""
|
| 17 |
+
# pyformat: enable
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
from collections import abc
|
| 20 |
+
import concurrent.futures
|
| 21 |
+
import dataclasses
|
| 22 |
+
import logging
|
| 23 |
+
import math
|
| 24 |
+
import random
|
| 25 |
+
from typing import Callable, Optional, TypeVar
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
import tensorflow as tf
|
| 29 |
+
import xarray
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# pylint: disable=logging-fstring-interpolation
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _xarray_bytes_per_element(
|
| 36 |
+
source: xarray.Dataset, exclude_dims: set[str]
|
| 37 |
+
) -> int:
|
| 38 |
+
bytes_per_element = 0
|
| 39 |
+
for variable in source.values():
|
| 40 |
+
items_per_element = math.prod(
|
| 41 |
+
size for dim, size in variable.sizes.items() if dim not in exclude_dims
|
| 42 |
+
)
|
| 43 |
+
bytes_per_element += variable.dtype.itemsize * items_per_element
|
| 44 |
+
return bytes_per_element
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _calculate_block_size(
|
| 48 |
+
source: xarray.Dataset,
|
| 49 |
+
block_dims: list[str],
|
| 50 |
+
bytes_per_request: float,
|
| 51 |
+
min_elements_per_request: int = 1,
|
| 52 |
+
) -> int:
|
| 53 |
+
"""Calculate the size of blocks to read simultaneously from disk."""
|
| 54 |
+
bytes_per_element = _xarray_bytes_per_element(source, set(block_dims))
|
| 55 |
+
elements_per_request = round(bytes_per_request / bytes_per_element)
|
| 56 |
+
max_elements = math.prod(source.sizes[dim] for dim in block_dims)
|
| 57 |
+
elements_per_request = min(
|
| 58 |
+
max(elements_per_request, min_elements_per_request), max_elements
|
| 59 |
+
)
|
| 60 |
+
return elements_per_request
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _iterate_windowed_block_slices(
|
| 64 |
+
sample_size: int,
|
| 65 |
+
total_size: int,
|
| 66 |
+
block_size: int,
|
| 67 |
+
stride_between_samples: int = 1,
|
| 68 |
+
output_window_stride: int = 1,
|
| 69 |
+
first_sample_offset: int = 0,
|
| 70 |
+
) -> abc.Iterator[slice]:
|
| 71 |
+
"""Yields slices for every block needed to generate windowed samples.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
sample_size: size of each sample.
|
| 75 |
+
total_size: total size of the dimension being sampled along.
|
| 76 |
+
block_size: desired size of blocks to read from disk.
|
| 77 |
+
stride_between_samples: shift between starts of sampled windows.
|
| 78 |
+
output_window_stride: shift between samples within a window.
|
| 79 |
+
first_sample_offset: offset of the first sample.
|
| 80 |
+
|
| 81 |
+
Yields:
|
| 82 |
+
Slice objects with integer bounds for each block.
|
| 83 |
+
"""
|
| 84 |
+
assert stride_between_samples >= 1
|
| 85 |
+
assert output_window_stride >= 1
|
| 86 |
+
assert first_sample_offset >= 0
|
| 87 |
+
|
| 88 |
+
sample_input_size = (
|
| 89 |
+
range(0, sample_size * output_window_stride, output_window_stride)[-1] + 1
|
| 90 |
+
)
|
| 91 |
+
assert 0 < sample_input_size <= block_size <= total_size
|
| 92 |
+
|
| 93 |
+
sample_stop = 0 # unused
|
| 94 |
+
|
| 95 |
+
# first block
|
| 96 |
+
block_start = first_sample_offset
|
| 97 |
+
block_stop = first_sample_offset + block_size
|
| 98 |
+
|
| 99 |
+
# iterate through all slices, in order
|
| 100 |
+
for start in range(
|
| 101 |
+
first_sample_offset,
|
| 102 |
+
total_size - sample_input_size + 1,
|
| 103 |
+
stride_between_samples,
|
| 104 |
+
):
|
| 105 |
+
prev_sample_stop = sample_stop
|
| 106 |
+
sample_stop = start + sample_input_size
|
| 107 |
+
|
| 108 |
+
if sample_stop > block_stop:
|
| 109 |
+
# yield previous block
|
| 110 |
+
assert prev_sample_stop > 0
|
| 111 |
+
yield slice(block_start, prev_sample_stop)
|
| 112 |
+
|
| 113 |
+
# begin new block
|
| 114 |
+
block_start = start
|
| 115 |
+
block_stop = start + block_size
|
| 116 |
+
|
| 117 |
+
if sample_stop > block_start:
|
| 118 |
+
# yield the final block
|
| 119 |
+
yield slice(block_start, sample_stop)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _drop_static_vars(dataset: xarray.Dataset) -> xarray.Dataset:
|
| 123 |
+
"""Drop fields that are static and do not vary with time."""
|
| 124 |
+
vars_to_drop = [k for k, v in dataset.items() if 'time' not in v.dims[0]] # pytype: disable=unsupported-operands
|
| 125 |
+
return dataset.drop_vars(vars_to_drop)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
NestedTensors = TypeVar('NestedTensors', tf.Tensor, dict[str, tf.Tensor])
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@tf.function(jit_compile=True, autograph=False)
|
| 132 |
+
def rolling_window_tensors(
|
| 133 |
+
inputs: NestedTensors, /, size: int, shift: int = 1, stride: int = 1
|
| 134 |
+
) -> NestedTensors:
|
| 135 |
+
"""Calculate a tensor of rolling windows.
|
| 136 |
+
|
| 137 |
+
Example usage:
|
| 138 |
+
|
| 139 |
+
>>> rolling_window_tensors(tf.range(10), size=6, shift=2)
|
| 140 |
+
<tf.Tensor: shape=(3, 6), dtype=int32, numpy=
|
| 141 |
+
array([[0, 1, 2, 3, 4, 5],
|
| 142 |
+
[2, 3, 4, 5, 6, 7],
|
| 143 |
+
[4, 5, 6, 7, 8, 9]], dtype=int32)>
|
| 144 |
+
|
| 145 |
+
>>> rolling_window_tensors(tf.range(10), size=4, stride=2)
|
| 146 |
+
<tf.Tensor: shape=(4, 4), dtype=int32, numpy=
|
| 147 |
+
array([[0, 2, 4, 6],
|
| 148 |
+
[1, 3, 5, 7],
|
| 149 |
+
[2, 4, 6, 8],
|
| 150 |
+
[3, 5, 7, 9]], dtype=int32)>
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
inputs: nested data structure with tf.Tensor values of shape [T, ...].
|
| 154 |
+
size: size of the time dimension in rolling window samples.
|
| 155 |
+
shift: shift between subsequent window samples along time.
|
| 156 |
+
stride: shift within a window along time.
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
Nested tensors of shape [S, W, ...] sampled from inputs, where S is the
|
| 160 |
+
number of samples and W is the window size.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
def calculate_windows(tensor):
|
| 164 |
+
shifts = tf.range(0, tf.shape(tensor)[0] - stride * (size - 1), shift)
|
| 165 |
+
indices = tf.range(0, size * stride, stride)
|
| 166 |
+
samples = tf.vectorized_map(
|
| 167 |
+
lambda shift: tf.gather(tensor, shift + indices), shifts
|
| 168 |
+
)
|
| 169 |
+
samples = tf.ensure_shape(samples, [None, size] + tensor.shape[1:])
|
| 170 |
+
return samples
|
| 171 |
+
|
| 172 |
+
return tf.nest.map_structure(calculate_windows, inputs)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class Sampler:
|
| 176 |
+
"""Base class for sampling from blocks."""
|
| 177 |
+
|
| 178 |
+
def list_block_slices(self, block_size: int, total_size: int) -> list[slice]:
|
| 179 |
+
"""Returns a list of slices bounding blocks to sample from."""
|
| 180 |
+
raise NotImplementedError
|
| 181 |
+
|
| 182 |
+
def sample_block(self, data: NestedTensors) -> NestedTensors:
|
| 183 |
+
"""Returns sample tensors from block tensors."""
|
| 184 |
+
raise NotImplementedError
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def example_size(self) -> int:
|
| 188 |
+
"""Size of each example."""
|
| 189 |
+
raise NotImplementedError
|
| 190 |
+
|
| 191 |
+
def examples_per_block(self, block_size: int) -> int:
|
| 192 |
+
"""Number of examples per block."""
|
| 193 |
+
raise NotImplementedError
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@dataclasses.dataclass
|
| 197 |
+
class Splitter(Sampler):
|
| 198 |
+
"""Split samples along the first axis."""
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def list_block_slices(self, block_size: int, total_size: int) -> list[slice]:
|
| 202 |
+
return [
|
| 203 |
+
slice(start, min(start + block_size, total_size))
|
| 204 |
+
for start in range(0, total_size, block_size)
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
def sample_block(self, data: NestedTensors) -> NestedTensors:
|
| 208 |
+
# Insert a dummy dimension for time-series length, which is always one.
|
| 209 |
+
return tf.nest.map_structure(lambda x: x[:, tf.newaxis, ...], data)
|
| 210 |
+
|
| 211 |
+
@property
|
| 212 |
+
def example_size(self) -> int:
|
| 213 |
+
return 1
|
| 214 |
+
|
| 215 |
+
def examples_per_block(self, block_size: int) -> int:
|
| 216 |
+
return block_size
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@dataclasses.dataclass
|
| 220 |
+
class Windower(Sampler):
|
| 221 |
+
"""Sample rolling windows along the first axis.
|
| 222 |
+
|
| 223 |
+
Attributes:
|
| 224 |
+
window_size: size of output windows.
|
| 225 |
+
stride_between_windows: offset between starting sequential windows.
|
| 226 |
+
output_window_stride: separation between between observations within a
|
| 227 |
+
window.
|
| 228 |
+
first_window_offset: offset of starting the first window.
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
window_size: int
|
| 232 |
+
stride_between_windows: int
|
| 233 |
+
output_window_stride: int = 1
|
| 234 |
+
first_window_offset: int = 0
|
| 235 |
+
|
| 236 |
+
def list_block_slices(self, block_size: int, total_size: int) -> list[slice]:
|
| 237 |
+
return list(
|
| 238 |
+
_iterate_windowed_block_slices(
|
| 239 |
+
sample_size=self.window_size,
|
| 240 |
+
block_size=block_size,
|
| 241 |
+
total_size=total_size,
|
| 242 |
+
stride_between_samples=self.stride_between_windows,
|
| 243 |
+
output_window_stride=self.output_window_stride,
|
| 244 |
+
first_sample_offset=self.first_window_offset,
|
| 245 |
+
)
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
def sample_block(self, data: NestedTensors) -> NestedTensors:
|
| 249 |
+
# NOTE(shoyer): It is tempting to try to use tf.data.Dataset.window instead
|
| 250 |
+
# for sampling windows, but that method does something different: it
|
| 251 |
+
# calculates windows over Dataset elements, rather than calculating windows
|
| 252 |
+
# within each Dataset element.
|
| 253 |
+
return rolling_window_tensors(
|
| 254 |
+
data,
|
| 255 |
+
size=self.window_size,
|
| 256 |
+
shift=self.stride_between_windows,
|
| 257 |
+
stride=self.output_window_stride,
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def example_size(self) -> int:
|
| 262 |
+
return self.window_size
|
| 263 |
+
|
| 264 |
+
def examples_per_block(self, block_size: int) -> int:
|
| 265 |
+
stop = block_size - self.output_window_stride * (self.window_size - 1)
|
| 266 |
+
return len(range(0, stop, self.stride_between_windows))
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
@dataclasses.dataclass
|
| 270 |
+
class WindowerAtOffsets(Sampler):
|
| 271 |
+
"""Sample rolling windows along the first axis at specified offsets."""
|
| 272 |
+
|
| 273 |
+
window_size: int
|
| 274 |
+
window_offsets: list[int]
|
| 275 |
+
output_window_stride: int = 1
|
| 276 |
+
|
| 277 |
+
def list_block_slices(self, block_size: int, total_size: int) -> list[slice]:
|
| 278 |
+
stride = self.output_window_stride
|
| 279 |
+
# This suffices for now because generally we cache evaluation data.
|
| 280 |
+
sample_input_size = range(0, self.window_size * stride, stride)[-1] + 1
|
| 281 |
+
assert 0 < sample_input_size <= block_size <= total_size
|
| 282 |
+
slices = []
|
| 283 |
+
for start in self.window_offsets:
|
| 284 |
+
stop = start + sample_input_size
|
| 285 |
+
if stop > total_size:
|
| 286 |
+
raise ValueError(
|
| 287 |
+
f'offset at {start} needs data through {stop=}, which is beyond'
|
| 288 |
+
f' {total_size=}'
|
| 289 |
+
)
|
| 290 |
+
slices.append(slice(start, stop))
|
| 291 |
+
return slices
|
| 292 |
+
|
| 293 |
+
def sample_block(self, data: NestedTensors) -> NestedTensors:
|
| 294 |
+
def strided_sample(tensor):
|
| 295 |
+
# Insert a dummy batch/sample dimension.
|
| 296 |
+
return tf.ensure_shape(
|
| 297 |
+
tensor[tf.newaxis, :: self.output_window_stride],
|
| 298 |
+
[1, self.window_size] + tensor.shape[1:],
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
return tf.nest.map_structure(strided_sample, data)
|
| 302 |
+
|
| 303 |
+
@property
|
| 304 |
+
def example_size(self) -> int:
|
| 305 |
+
return self.window_size
|
| 306 |
+
|
| 307 |
+
def examples_per_block(self, block_size: int) -> int:
|
| 308 |
+
del block_size # unused
|
| 309 |
+
return 1
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class Selector:
|
| 313 |
+
"""Base class for block selection."""
|
| 314 |
+
|
| 315 |
+
def select(self, blocks: list[slice]) -> list[slice]:
|
| 316 |
+
"""Select a subset of blocks for sampling."""
|
| 317 |
+
raise NotImplementedError
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class CompleteSelector(Selector):
|
| 321 |
+
|
| 322 |
+
def select(self, blocks: list[slice]) -> list[slice]:
|
| 323 |
+
return blocks
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
@dataclasses.dataclass
|
| 327 |
+
class ShardSelector(Selector):
|
| 328 |
+
shard_index: int
|
| 329 |
+
shard_count: int
|
| 330 |
+
|
| 331 |
+
def select(self, blocks: list[slice]) -> list[slice]:
|
| 332 |
+
return [
|
| 333 |
+
block
|
| 334 |
+
for i, block in enumerate(blocks)
|
| 335 |
+
if i % self.shard_count == self.shard_index
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@dataclasses.dataclass
|
| 340 |
+
class ShuffleSelector(Selector):
|
| 341 |
+
seed: int = 0
|
| 342 |
+
reshuffle_each_iteration: bool = True
|
| 343 |
+
|
| 344 |
+
def select(self, blocks: list[slice]) -> list[slice]:
|
| 345 |
+
rng = random.Random(self.seed)
|
| 346 |
+
if self.reshuffle_each_iteration:
|
| 347 |
+
self.seed = rng.randrange(2**63)
|
| 348 |
+
return rng.sample(blocks, k=len(blocks))
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
@dataclasses.dataclass
|
| 352 |
+
class ComposedSelector(Selector):
|
| 353 |
+
components: list[Selector]
|
| 354 |
+
|
| 355 |
+
def select(self, blocks: list[slice]) -> list[slice]:
|
| 356 |
+
for component in self.components:
|
| 357 |
+
blocks = component.select(blocks)
|
| 358 |
+
return blocks
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@dataclasses.dataclass
|
| 362 |
+
class CustomSelector(Selector):
|
| 363 |
+
select: Callable[[list[slice]], list[slice]]
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _thread_pool_loader(max_workers: int = 100):
|
| 367 |
+
"""Dataset loader using a large thread pool for concurrency."""
|
| 368 |
+
# We use a separate thread for reading each data variable in each block.
|
| 369 |
+
executor = concurrent.futures.ThreadPoolExecutor(max_workers)
|
| 370 |
+
|
| 371 |
+
def load(dataset: xarray.Dataset) -> xarray.Dataset:
|
| 372 |
+
arrays = executor.map(lambda var: var.values, dataset.values())
|
| 373 |
+
return dataset.copy(data={k: v for k, v in zip(dataset, arrays)})
|
| 374 |
+
|
| 375 |
+
return load
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
class _Reader:
|
| 379 |
+
"""Class for reading an xarray.Dataset."""
|
| 380 |
+
|
| 381 |
+
def __init__(
|
| 382 |
+
self,
|
| 383 |
+
source: xarray.Dataset,
|
| 384 |
+
sampler: Sampler,
|
| 385 |
+
block_selector: Selector = CompleteSelector(),
|
| 386 |
+
*,
|
| 387 |
+
sample_dim: str = 'time',
|
| 388 |
+
block_size_in_bytes: float = 1e8,
|
| 389 |
+
parallel_block_reads: int = tf.data.AUTOTUNE,
|
| 390 |
+
parallel_samples: int = tf.data.AUTOTUNE,
|
| 391 |
+
dataset_loader: Optional[
|
| 392 |
+
Callable[[xarray.Dataset], xarray.Dataset]
|
| 393 |
+
] = None,
|
| 394 |
+
):
|
| 395 |
+
if dataset_loader is None:
|
| 396 |
+
# In principle, it could make sense to support passing alternative
|
| 397 |
+
# loaders, such as xarray_tensorstore.read() or a dask loader that calls
|
| 398 |
+
# .compute(). We don't yet have any use cases where this seems to make a
|
| 399 |
+
# difference, though. (The thread pool loader works as well as
|
| 400 |
+
# xarray_tensorstore.read.)
|
| 401 |
+
dataset_loader = _thread_pool_loader()
|
| 402 |
+
|
| 403 |
+
if sample_dim not in source.dims:
|
| 404 |
+
raise ValueError(
|
| 405 |
+
'source does not include variables with a'
|
| 406 |
+
f' {sample_dim!r} dimension:\n{source}'
|
| 407 |
+
)
|
| 408 |
+
source = _drop_static_vars(source)
|
| 409 |
+
source = source.transpose(sample_dim, ...)
|
| 410 |
+
|
| 411 |
+
block_size = _calculate_block_size(
|
| 412 |
+
source,
|
| 413 |
+
block_dims=[sample_dim],
|
| 414 |
+
bytes_per_request=block_size_in_bytes,
|
| 415 |
+
min_elements_per_request=sampler.example_size,
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
block_slices = sampler.list_block_slices(
|
| 419 |
+
block_size, source.sizes[sample_dim]
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
bytes_per_element = _xarray_bytes_per_element(source, {sample_dim})
|
| 423 |
+
bytes_per_example = sampler.example_size * bytes_per_element
|
| 424 |
+
examples_per_block = sampler.examples_per_block(block_size)
|
| 425 |
+
sample_bytes_per_block = bytes_per_example * examples_per_block
|
| 426 |
+
expansion = sample_bytes_per_block / block_size_in_bytes
|
| 427 |
+
logging.info(
|
| 428 |
+
f'picked block_size={block_size}, corresponding to {len(block_slices)} '
|
| 429 |
+
f'blocks with examples_per_block={examples_per_block}, based on '
|
| 430 |
+
f'sampler={sampler} and {block_size_in_bytes=:g}. '
|
| 431 |
+
f'{sample_bytes_per_block=:g} is a {expansion:1.2f}x expansion.'
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
self.source = source
|
| 435 |
+
self.sampler = sampler
|
| 436 |
+
self.block_selector = block_selector
|
| 437 |
+
self.parallel_block_reads = parallel_block_reads
|
| 438 |
+
self.parallel_samples = parallel_samples
|
| 439 |
+
self.dataset_loader = dataset_loader
|
| 440 |
+
|
| 441 |
+
self.block_size = block_size
|
| 442 |
+
self.block_slices = block_slices
|
| 443 |
+
self.bytes_per_example = bytes_per_example
|
| 444 |
+
self.examples_per_block = examples_per_block
|
| 445 |
+
|
| 446 |
+
def read(self) -> tf.data.Dataset:
|
| 447 |
+
"""Read this dataset into a tf.data.Dataset."""
|
| 448 |
+
|
| 449 |
+
def generate_blocks():
|
| 450 |
+
for block in self.block_selector.select(self.block_slices):
|
| 451 |
+
yield (block.start, block.stop)
|
| 452 |
+
|
| 453 |
+
def np_read_block(start: np.ndarray, stop: np.ndarray) -> list[np.ndarray]:
|
| 454 |
+
selection = self.source.isel(time=slice(start, stop))
|
| 455 |
+
loaded = self.dataset_loader(selection)
|
| 456 |
+
arrays = [x.values for x in loaded.values()]
|
| 457 |
+
return arrays
|
| 458 |
+
|
| 459 |
+
def tf_read_block(start: tf.Tensor, stop: tf.Tensor):
|
| 460 |
+
dtypes = [v.dtype for v in self.source.values()]
|
| 461 |
+
shapes = [(None,) + v.shape[1:] for v in self.source.values()]
|
| 462 |
+
tensors = tf.numpy_function(np_read_block, [start, stop], dtypes)
|
| 463 |
+
for tensor, shape in zip(tensors, shapes):
|
| 464 |
+
tensor.set_shape(shape)
|
| 465 |
+
return dict(zip(self.source.keys(), tensors))
|
| 466 |
+
|
| 467 |
+
data = tf.data.Dataset.from_generator(
|
| 468 |
+
generate_blocks, output_signature=2 * (tf.TensorSpec((), tf.int64),)
|
| 469 |
+
)
|
| 470 |
+
data = data.map(tf_read_block, num_parallel_calls=self.parallel_block_reads)
|
| 471 |
+
data = data.map(
|
| 472 |
+
self.sampler.sample_block, num_parallel_calls=self.parallel_samples
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
data = data.unbatch()
|
| 476 |
+
|
| 477 |
+
return data
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def read_timeseries(
|
| 481 |
+
source: xarray.Dataset,
|
| 482 |
+
sampler: Sampler,
|
| 483 |
+
block_selector: Selector = CompleteSelector(),
|
| 484 |
+
*,
|
| 485 |
+
sample_dim: str = 'time',
|
| 486 |
+
block_size_in_bytes: float = 1e8,
|
| 487 |
+
parallel_block_reads: int = tf.data.AUTOTUNE,
|
| 488 |
+
parallel_samples: int = tf.data.AUTOTUNE,
|
| 489 |
+
) -> tf.data.Dataset:
|
| 490 |
+
"""Read a time-series xarray.Dataset into a tf.data.Dataset of windows.
|
| 491 |
+
|
| 492 |
+
See go/whirl-zarr-reader for a detailed description of the design.
|
| 493 |
+
|
| 494 |
+
Args:
|
| 495 |
+
source: lazy xarray.Dataset, e.g., opened from a Zarr file with
|
| 496 |
+
`open_zarr(..., chunks=None)`. All data variables with a 'time' dimension
|
| 497 |
+
will be sampled. Note: setting `chunks=None` to avoid using Dask is
|
| 498 |
+
preferred for optimal performance.
|
| 499 |
+
sampler: specification of what time-series samples of this dataset should
|
| 500 |
+
look like. Currently the only supported sampler is Windower.
|
| 501 |
+
block_selector: selector called at each pass through the source dataset,
|
| 502 |
+
indicating the blocks to read in order. The returned blocks should be a
|
| 503 |
+
subset of passed in blocks.
|
| 504 |
+
sample_dim: name of the dimension to sample along.
|
| 505 |
+
block_size_in_bytes: number of bytes to use for each reading a "block" of
|
| 506 |
+
data from the source data. Larger block sizes are more efficient.
|
| 507 |
+
parallel_block_reads: number of blocks to read in parallel.
|
| 508 |
+
parallel_samples: number of threads to use for generating samples from
|
| 509 |
+
blocks.
|
| 510 |
+
|
| 511 |
+
Returns:
|
| 512 |
+
tf.data.Dataset where each element is a dict of arrays.
|
| 513 |
+
"""
|
| 514 |
+
return _Reader(
|
| 515 |
+
source=source,
|
| 516 |
+
sampler=sampler,
|
| 517 |
+
block_selector=block_selector,
|
| 518 |
+
sample_dim=sample_dim,
|
| 519 |
+
block_size_in_bytes=block_size_in_bytes,
|
| 520 |
+
parallel_block_reads=parallel_block_reads,
|
| 521 |
+
parallel_samples=parallel_samples,
|
| 522 |
+
).read()
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def read_shuffled_shard(
|
| 526 |
+
source: xarray.Dataset,
|
| 527 |
+
sampler: Sampler,
|
| 528 |
+
*,
|
| 529 |
+
sample_dim: str = 'time',
|
| 530 |
+
block_size_in_bytes: float = 1e8,
|
| 531 |
+
buffer_size_in_bytes: float = 1e10,
|
| 532 |
+
min_buffer_blocks: float = 10,
|
| 533 |
+
parallel_block_reads: int = tf.data.AUTOTUNE,
|
| 534 |
+
parallel_samples: int = tf.data.AUTOTUNE,
|
| 535 |
+
shard_index: Optional[int] = None,
|
| 536 |
+
shard_count: Optional[int] = None,
|
| 537 |
+
seed: int = 0,
|
| 538 |
+
reshuffle_each_iteration: bool = True,
|
| 539 |
+
) -> tf.data.Dataset:
|
| 540 |
+
"""Read a time-series with samples in randomly shuffled order.
|
| 541 |
+
|
| 542 |
+
Args:
|
| 543 |
+
source: lazy xarray.Dataset, e.g., opened from a Zarr file with
|
| 544 |
+
`open_zarr(..., chunks=None)`. All data variables with a 'time' dimension
|
| 545 |
+
will be sampled. Note: setting `chunks=None` to avoid using Dask is
|
| 546 |
+
preferred for optimal performance.
|
| 547 |
+
sampler: specification of what time-series samples of this dataset should
|
| 548 |
+
look like.
|
| 549 |
+
sample_dim: name of the dimension to sample along.
|
| 550 |
+
block_size_in_bytes: number of bytes to use for each reading a "block" of
|
| 551 |
+
data from the source data. Larger block sizes are more efficient.
|
| 552 |
+
buffer_size_in_bytes: number of bytes to use in the shuffle buffer.
|
| 553 |
+
min_buffer_blocks: minimum number of blocks that must be represented in the
|
| 554 |
+
shuffle buffer, if more than one sample is taken from each block.
|
| 555 |
+
Typically this should be at least as large as the batch size.
|
| 556 |
+
parallel_block_reads: number of blocks to read in parallel.
|
| 557 |
+
parallel_samples: number of threads to use for generating samples from
|
| 558 |
+
blocks.
|
| 559 |
+
shard_index: integer index for this shard of the data, in the range `[0,
|
| 560 |
+
shard_count)`. In a multi-host JAX training setup, this should equal
|
| 561 |
+
`jax.process_index()`.
|
| 562 |
+
shard_count: total number of data shards. In a multi-host JAX training
|
| 563 |
+
setup, this should equal `jax.process_count()`.
|
| 564 |
+
seed: seed to use for random number generation.
|
| 565 |
+
reshuffle_each_iteration: whether to use a new shuffle order for elements
|
| 566 |
+
after each iteration through `source` or not.
|
| 567 |
+
|
| 568 |
+
Returns:
|
| 569 |
+
tf.data.Dataset where each element is a dict of arrays.
|
| 570 |
+
"""
|
| 571 |
+
if shard_index is None and shard_count is None:
|
| 572 |
+
shard_index = 0
|
| 573 |
+
shard_count = 1
|
| 574 |
+
|
| 575 |
+
if shard_index is None or shard_count is None:
|
| 576 |
+
raise ValueError('must set both or neither of shard_index and shard_count')
|
| 577 |
+
|
| 578 |
+
selector = ComposedSelector([
|
| 579 |
+
ShardSelector(shard_index, shard_count),
|
| 580 |
+
ShuffleSelector(seed, reshuffle_each_iteration),
|
| 581 |
+
])
|
| 582 |
+
|
| 583 |
+
def _make_reader(block_size_in_bytes):
|
| 584 |
+
reader = _Reader(
|
| 585 |
+
source=source,
|
| 586 |
+
sampler=sampler,
|
| 587 |
+
sample_dim=sample_dim,
|
| 588 |
+
block_selector=selector,
|
| 589 |
+
block_size_in_bytes=block_size_in_bytes,
|
| 590 |
+
parallel_block_reads=parallel_block_reads,
|
| 591 |
+
parallel_samples=parallel_samples,
|
| 592 |
+
)
|
| 593 |
+
buffer_size = int(buffer_size_in_bytes / reader.bytes_per_example)
|
| 594 |
+
logging.info(
|
| 595 |
+
f'picked shuffle buffer size of {buffer_size} based on '
|
| 596 |
+
f'{buffer_size_in_bytes=:g}'
|
| 597 |
+
)
|
| 598 |
+
return reader, buffer_size
|
| 599 |
+
|
| 600 |
+
reader, buffer_size = _make_reader(block_size_in_bytes)
|
| 601 |
+
|
| 602 |
+
if buffer_size:
|
| 603 |
+
examples_per_block = reader.examples_per_block
|
| 604 |
+
buffer_blocks = buffer_size / examples_per_block
|
| 605 |
+
if examples_per_block > 1 and buffer_blocks < min_buffer_blocks:
|
| 606 |
+
block_size_in_bytes = reader.bytes_per_example
|
| 607 |
+
logging.warning(
|
| 608 |
+
'insufficient diversity in proposed shuffle buffer: '
|
| 609 |
+
f'{examples_per_block=} and {buffer_size=} means that on average '
|
| 610 |
+
f'only {buffer_blocks:g} blocks will be represented in the shuffle '
|
| 611 |
+
f'buffer, which is less than {min_buffer_blocks=}. Falling back to '
|
| 612 |
+
f'one example per block ({block_size_in_bytes=:g}).'
|
| 613 |
+
)
|
| 614 |
+
reader, buffer_size = _make_reader(block_size_in_bytes)
|
| 615 |
+
assert reader.examples_per_block == 1
|
| 616 |
+
|
| 617 |
+
data = reader.read()
|
| 618 |
+
|
| 619 |
+
if buffer_size:
|
| 620 |
+
# for testing, disable the shuffle buffer if it has size zero (the shuffle
|
| 621 |
+
# method does not support size zero buffers)
|
| 622 |
+
data = data.shuffle(buffer_size, seed, reshuffle_each_iteration)
|
| 623 |
+
|
| 624 |
+
return data
|
model/reference_code/stochastic_losses.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Stochastic losses for NeuralGCM."""
|
| 15 |
+
import abc
|
| 16 |
+
from typing import Callable, Optional, Sequence
|
| 17 |
+
from dinosaur import typing
|
| 18 |
+
import gin
|
| 19 |
+
import jax
|
| 20 |
+
import jax.numpy as jnp
|
| 21 |
+
import model.reference_code.linear_transforms as linear_transforms
|
| 22 |
+
import model.reference_code.metrics_base as metrics_base
|
| 23 |
+
import model.reference_code.metrics_util as metrics_util
|
| 24 |
+
from model.legacy import model_utils
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
Pytree = typing.Pytree
|
| 29 |
+
TrajectoryRepresentations = typing.TrajectoryRepresentations
|
| 30 |
+
|
| 31 |
+
AggregationTransformConstructor = metrics_util.AggregationTransformConstructor
|
| 32 |
+
|
| 33 |
+
tree_leaves = jax.tree_util.tree_leaves
|
| 34 |
+
tree_map = jax.tree_util.tree_map
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def replicate(
|
| 38 |
+
x: Pytree,
|
| 39 |
+
axis_name: str = 'batch',
|
| 40 |
+
times: Optional[int] = None,
|
| 41 |
+
) -> Pytree:
|
| 42 |
+
"""Replicated a pytree across devices."""
|
| 43 |
+
if times is None:
|
| 44 |
+
times = jax.local_device_count()
|
| 45 |
+
|
| 46 |
+
def _replicate(_):
|
| 47 |
+
return x
|
| 48 |
+
|
| 49 |
+
return jax.pmap(_replicate, axis_name)(np.ones(times))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class EnergyLikeLoss(metrics_base.Loss, abc.ABC):
|
| 53 |
+
"""Energy-score like loss function.
|
| 54 |
+
|
| 55 |
+
Both CRPS and EnergyScore take the form (with E expectation)
|
| 56 |
+
E‖X - Y‖^β - ensemble_term_weight * E‖X - X'‖^β
|
| 57 |
+
where for CRPS ‖⋅‖ is the L1 norm, and for EnergyScore it is the L2 norm.
|
| 58 |
+
|
| 59 |
+
To create a general implementation, we decompose the norm as
|
| 60 |
+
‖Z‖ := _norm_reduction_fn(_norm_inner_fn(Z))
|
| 61 |
+
|
| 62 |
+
For more see (21) and (22) in [1]; http://shortn/_Lyu0etEy1F
|
| 63 |
+
|
| 64 |
+
References:
|
| 65 |
+
[1]: Gneiting, T., & Raftery, A. E. (2007). Strictly proper scoring rules,
|
| 66 |
+
prediction, and estimation. Journal of the American statistical
|
| 67 |
+
Association, 102(477), 359-378.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
trajectory_spec: metrics_util.TrajectorySpec,
|
| 73 |
+
components: Sequence[linear_transforms.LinearTransformConstructor],
|
| 74 |
+
time_step: Optional[int | slice] = None,
|
| 75 |
+
level: Optional[int] = None,
|
| 76 |
+
getter: Callable[[Pytree], Pytree] = (
|
| 77 |
+
metrics_util.filter_sim_time_and_diagnostics
|
| 78 |
+
),
|
| 79 |
+
beta: float = 1.0,
|
| 80 |
+
ensemble_term_weight: float = 0.5,
|
| 81 |
+
is_nodal: bool = True,
|
| 82 |
+
is_encoded: bool = False,
|
| 83 |
+
coarsen_aggregation: AggregationTransformConstructor = (
|
| 84 |
+
metrics_util.AggregateIdentity
|
| 85 |
+
),
|
| 86 |
+
vector_norm_squared_aggregation: AggregationTransformConstructor = (
|
| 87 |
+
metrics_util.AggregateIdentity
|
| 88 |
+
),
|
| 89 |
+
):
|
| 90 |
+
"""Constructs an instance of EnergyLikeLoss.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
trajectory_spec: Specification of spatial and temporal trajectory sizes.
|
| 94 |
+
components: Sequence of linear transformations to be applied to errors.
|
| 95 |
+
time_step: Step or slice at which to compute loss, or None for all steps.
|
| 96 |
+
level: Level to compute loss at, or None to use mean over all levels.
|
| 97 |
+
getter: Function for extracting a sub-pytree on which errors are computed.
|
| 98 |
+
beta: Power parameter of the loss. For energy score to be strictly proper
|
| 99 |
+
beta must be belong to `(0, 2)`.
|
| 100 |
+
ensemble_term_weight: Coefficient that specifcies how much weight is put
|
| 101 |
+
on the terms that captures the spread of the 2-ensemble. For standard
|
| 102 |
+
energy score this value should be set to `0.5`. It can be used to
|
| 103 |
+
interpolate to other scoring rules that are not strictly proper. For
|
| 104 |
+
example setting this value to `0.0` and setting `beta = 2.0` will result
|
| 105 |
+
in a squared error loss.
|
| 106 |
+
is_nodal: Indicator whether loss is computed in nodal space.
|
| 107 |
+
is_encoded: Indicator whether loss is computed in encoded(model) space.
|
| 108 |
+
coarsen_aggregation: Transform class that is used to aggregate errors
|
| 109 |
+
before computing the loss elements. This enables defining losses on
|
| 110 |
+
coarser representations that accentuate larger scale structure.
|
| 111 |
+
Currently this argument should be used only by PatchEnergyLoss. Example
|
| 112 |
+
coarsening operators include `RegriddingAggregation`, `TimeWindowSum`.
|
| 113 |
+
vector_norm_squared_aggregation: Transform class that is used to aggregate
|
| 114 |
+
components of the squared errors to form the distance for computing the
|
| 115 |
+
energy score. Currently this argument should be used only by
|
| 116 |
+
PatchEnergyLoss. Suitable aggregation methods include
|
| 117 |
+
`RegriddingAggregation`, `TimeWindowSum`, `SumVariables`, which would
|
| 118 |
+
correspond to vectors of (1) single level, time, variable, horizontal
|
| 119 |
+
neighbors; (2) single level, variable, lon-lat, sequence of time values;
|
| 120 |
+
(3) all variables at a single level, time, lon-lat.
|
| 121 |
+
"""
|
| 122 |
+
self.coarsen_fn = coarsen_aggregation(
|
| 123 |
+
trajectory_spec, is_nodal=is_nodal, is_encoded=is_encoded
|
| 124 |
+
)
|
| 125 |
+
self.vector_norm_squared_fn = vector_norm_squared_aggregation(
|
| 126 |
+
self.coarsen_fn.out_trajectory_spec,
|
| 127 |
+
is_nodal=is_nodal,
|
| 128 |
+
is_encoded=is_encoded,
|
| 129 |
+
)
|
| 130 |
+
# parent class reductions are done on the final out_trajectory_spec.
|
| 131 |
+
super().__init__(
|
| 132 |
+
self.vector_norm_squared_fn.out_trajectory_spec,
|
| 133 |
+
is_nodal=is_nodal,
|
| 134 |
+
is_encoded=is_encoded,
|
| 135 |
+
)
|
| 136 |
+
self.components = components
|
| 137 |
+
self.time_step = time_step
|
| 138 |
+
self.level = level
|
| 139 |
+
self.getter = getter
|
| 140 |
+
# transform is applied to raw inputs which are aligned with trajectory_spec.
|
| 141 |
+
self.transform = linear_transforms.ComposedTransformForLoss(
|
| 142 |
+
trajectory_spec, self.components
|
| 143 |
+
)
|
| 144 |
+
self._beta = beta
|
| 145 |
+
self._ensemble_term_weight = ensemble_term_weight
|
| 146 |
+
|
| 147 |
+
def a_minus_cb(self, a: Pytree, c: float, b: Pytree) -> Pytree:
|
| 148 |
+
"""A - c * B."""
|
| 149 |
+
return tree_map(lambda a_i, b_i: a_i - c * b_i, a, b)
|
| 150 |
+
|
| 151 |
+
def ca_minus_b(self, c: float, a: Pytree, b: Pytree) -> Pytree:
|
| 152 |
+
"""c * A - B."""
|
| 153 |
+
return tree_map(lambda a_i, b_i: c * a_i - b_i, a, b)
|
| 154 |
+
|
| 155 |
+
def component_mean(self, tree: Pytree) -> jax.Array:
|
| 156 |
+
"""Mean over variable, time, pressure, lat, lon."""
|
| 157 |
+
leaf_means = tree_leaves(self.mean_per_variable(tree))
|
| 158 |
+
return sum(leaf_means) / len(leaf_means)
|
| 159 |
+
|
| 160 |
+
def ensemble_mean(self, tree: Pytree) -> Pytree:
|
| 161 |
+
return jax.lax.pmean(tree, 'ensemble')
|
| 162 |
+
|
| 163 |
+
def _prepare(self, trajectory: TrajectoryRepresentations) -> Pytree:
|
| 164 |
+
"""Prepares target or predictions."""
|
| 165 |
+
# Cannot consolidate with RMSE.prepare since this one
|
| 166 |
+
# * does not take ensemble mean of trajectory.
|
| 167 |
+
trajectory = metrics_util.extract_variable(
|
| 168 |
+
trajectory,
|
| 169 |
+
self.trajectory_spec,
|
| 170 |
+
self.time_step,
|
| 171 |
+
self.level,
|
| 172 |
+
self.getter,
|
| 173 |
+
self.is_nodal,
|
| 174 |
+
self.is_encoded,
|
| 175 |
+
)
|
| 176 |
+
return trajectory
|
| 177 |
+
|
| 178 |
+
def evaluate(
|
| 179 |
+
self,
|
| 180 |
+
prediction: TrajectoryRepresentations,
|
| 181 |
+
target: TrajectoryRepresentations,
|
| 182 |
+
) -> Pytree:
|
| 183 |
+
"""Evaluates giving values of interest."""
|
| 184 |
+
pv2ss = self._per_variable_spread_skill_errors(prediction, target)
|
| 185 |
+
return self._spread_skill_and_loss(
|
| 186 |
+
x_minus_y=pv2ss['x_minus_y'],
|
| 187 |
+
x_minus_xprime=pv2ss['x_minus_xprime'],
|
| 188 |
+
)['loss']
|
| 189 |
+
|
| 190 |
+
def debug_loss_terms_instance(self) -> metrics_base.EvaluateFunctionWrapper:
|
| 191 |
+
"""Returns class that evaluates rel loss per variable and spread/skill."""
|
| 192 |
+
|
| 193 |
+
def evaluate_fn(
|
| 194 |
+
prediction: TrajectoryRepresentations,
|
| 195 |
+
target: TrajectoryRepresentations,
|
| 196 |
+
) -> Pytree:
|
| 197 |
+
# self.loss.evaluate takes ensemble mean (to evaluate on ensemble mean) if
|
| 198 |
+
# needed.
|
| 199 |
+
pv2ss = self._per_variable_spread_skill_errors(prediction, target)
|
| 200 |
+
overall_spread_skill_loss = self._spread_skill_and_loss(
|
| 201 |
+
x_minus_y=pv2ss['x_minus_y'],
|
| 202 |
+
x_minus_xprime=pv2ss['x_minus_xprime'],
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
all_vars = pv2ss['x_minus_y'].keys()
|
| 206 |
+
|
| 207 |
+
per_variable_terms = {
|
| 208 |
+
var: self._spread_skill_and_loss(
|
| 209 |
+
x_minus_y=pv2ss['x_minus_y'][var],
|
| 210 |
+
x_minus_xprime=pv2ss['x_minus_xprime'][var],
|
| 211 |
+
)
|
| 212 |
+
for var in all_vars
|
| 213 |
+
}
|
| 214 |
+
# here we reduce terms by summation to expose relative contributions,
|
| 215 |
+
# even though the actual total_loss might be different.
|
| 216 |
+
per_variable_losses = {
|
| 217 |
+
var: per_variable_terms[var]['loss'] for var in all_vars
|
| 218 |
+
}
|
| 219 |
+
sum_of_losses = sum(per_variable_losses.values())
|
| 220 |
+
per_variable_relative_losses = tree_map(
|
| 221 |
+
lambda x: x / sum_of_losses, per_variable_losses
|
| 222 |
+
)
|
| 223 |
+
return {
|
| 224 |
+
'relative_loss': per_variable_relative_losses,
|
| 225 |
+
'overall': overall_spread_skill_loss,
|
| 226 |
+
'per_variable_spread': {
|
| 227 |
+
var: per_variable_terms[var]['spread'] for var in all_vars
|
| 228 |
+
},
|
| 229 |
+
'per_variable_skill': {
|
| 230 |
+
var: per_variable_terms[var]['skill'] for var in all_vars
|
| 231 |
+
},
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
return metrics_base.EvaluateFunctionWrapper(evaluate_fn)
|
| 235 |
+
|
| 236 |
+
def _per_variable_spread_skill_errors(
|
| 237 |
+
self,
|
| 238 |
+
prediction: TrajectoryRepresentations,
|
| 239 |
+
target: TrajectoryRepresentations,
|
| 240 |
+
) -> Pytree:
|
| 241 |
+
"""Computes non-reduced loss terms (skill and spread) for each variable.
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
prediction: predicted 2-ensemble of trajectories with each component
|
| 245 |
+
having shape [2, time_steps, vertical, lat_axis, lon_axis], with leading
|
| 246 |
+
axis corresponding to different ensemble members and last two axes being
|
| 247 |
+
either spherical harmonics numbers or lat, lon values.
|
| 248 |
+
target: target trajectory replicated along the ensemble axis. The shape is
|
| 249 |
+
expected to be exactly the same as `trajectory`.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
A dictionary with keys containing transformed variables.
|
| 253 |
+
`x_minus_y` = prediction - target
|
| 254 |
+
`x_minus_xprime` = difference of ensemble predictions
|
| 255 |
+
`prediction` = prediction
|
| 256 |
+
"""
|
| 257 |
+
ensemble_size = jax.lax.psum(1, 'ensemble')
|
| 258 |
+
if ensemble_size != 2:
|
| 259 |
+
raise ValueError(f'{ensemble_size=} is not 2')
|
| 260 |
+
|
| 261 |
+
prediction = self.transform(self._prepare(prediction), target)
|
| 262 |
+
target = self.transform(self._prepare(target), target)
|
| 263 |
+
|
| 264 |
+
x_minus_y = tree_map(jnp.subtract, prediction, target) # X_i - Y
|
| 265 |
+
|
| 266 |
+
xprime = jax.lax.pshuffle(prediction, 'ensemble', (1, 0))
|
| 267 |
+
x_minus_xprime = tree_map(jnp.subtract, prediction, xprime) # X_i - X_j≠i
|
| 268 |
+
|
| 269 |
+
return {
|
| 270 |
+
'x_minus_y': x_minus_y,
|
| 271 |
+
'x_minus_xprime': x_minus_xprime,
|
| 272 |
+
'prediction': prediction,
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
@abc.abstractmethod
|
| 276 |
+
def _spread_skill_and_loss(
|
| 277 |
+
self,
|
| 278 |
+
x_minus_y: Pytree,
|
| 279 |
+
x_minus_xprime: Pytree,
|
| 280 |
+
) -> dict[str, jax.Array]:
|
| 281 |
+
"""Gets dictionary with 'spread', 'skill', and 'loss' entries."""
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
@gin.register(
|
| 285 |
+
denylist=['coarsen_aggregation', 'vector_norm_squared_aggregation']
|
| 286 |
+
)
|
| 287 |
+
class CRPSLoss(EnergyLikeLoss):
|
| 288 |
+
"""CRPS loss on linearly transformed errors.
|
| 289 |
+
|
| 290 |
+
CRPS takes the form (with E expectation)
|
| 291 |
+
E‖X - Y‖^β - ensemble_term_weight * E‖X - X'‖^β
|
| 292 |
+
where ‖⋅‖ is the L1 norm. It can be thought of as the sum of component-wise
|
| 293 |
+
energy score losses.
|
| 294 |
+
|
| 295 |
+
Based on formula 21 in [1]; http://shortn/_Lyu0etEy1F
|
| 296 |
+
|
| 297 |
+
References:
|
| 298 |
+
[1]: Gneiting, T., & Raftery, A. E. (2007). Strictly proper scoring rules,
|
| 299 |
+
prediction, and estimation. Journal of the American statistical
|
| 300 |
+
Association, 102(477), 359-378.
|
| 301 |
+
"""
|
| 302 |
+
|
| 303 |
+
def _spread_skill_and_loss(
|
| 304 |
+
self,
|
| 305 |
+
x_minus_y: Pytree,
|
| 306 |
+
x_minus_xprime: Pytree,
|
| 307 |
+
) -> dict[str, jax.Array]:
|
| 308 |
+
"""Gets dictionary with 'spread', 'skill', and 'loss' entries."""
|
| 309 |
+
a_minus_cb = self.a_minus_cb
|
| 310 |
+
ensemble_mean = self.ensemble_mean
|
| 311 |
+
component_mean = self.component_mean
|
| 312 |
+
|
| 313 |
+
def abs_beta(tree: Pytree) -> Pytree:
|
| 314 |
+
return tree_map(lambda x: jnp.abs(x) ** self._beta, tree)
|
| 315 |
+
|
| 316 |
+
# With X, X' two i.i.d. predictions,
|
| 317 |
+
# Skill = (1/2)[ (1/N)Σₙ|Xₙ-Yₙ| + (1/N)Σₙ|Xₙ'-Yₙ| ]
|
| 318 |
+
# Spread = (1/N) Σₙ|Xₙ-Xₙ'|
|
| 319 |
+
|
| 320 |
+
# Recall x_minus_y = X-Y on one device and X'-Y on another. So the ensemble
|
| 321 |
+
# mean of this (which is all-reduced) is exactly Skill above.
|
| 322 |
+
skill = component_mean(ensemble_mean(abs_beta(x_minus_y)))
|
| 323 |
+
|
| 324 |
+
# One device has X-X' and the other has X'-X, so the ensemble mean is the
|
| 325 |
+
# same on both devices.
|
| 326 |
+
spread = component_mean(ensemble_mean(abs_beta(x_minus_xprime)))
|
| 327 |
+
|
| 328 |
+
# Then CRPS = Skill - (1/2) Spread
|
| 329 |
+
# However, this is unstable if Spread = 2Skill + ε, where |ε| << |Spread|.
|
| 330 |
+
# In particular, up to numerical precision, CRPS will equal 0!
|
| 331 |
+
# This can happen if Prob[Xₙ = 1] = p << 1, and Prob[Xₙ = 0] = 1 - p.
|
| 332 |
+
# a stable estimate of CRPS is
|
| 333 |
+
# CRPS = C + C' (an ensemble mean)
|
| 334 |
+
# where
|
| 335 |
+
# C = (1/N) Σₙ[ |Xₙ-Yₙ| - (1/2) |Xₙ-Xₙ'| ]
|
| 336 |
+
# C'= (1/N) Σₙ[ |Xₙ'-Yₙ| - (1/2) |Xₙ'-Xₙ| ]
|
| 337 |
+
# which should be re-written as
|
| 338 |
+
# CRPS = (1/(2N)) Σₙ[ |Xₙ-Yₙ| + |Xₙ'-Yₙ| - |Xₙ-Xₙ'| ]
|
| 339 |
+
# The triangle inequality ensures the summands are non-negative.
|
| 340 |
+
crps = component_mean(
|
| 341 |
+
ensemble_mean(
|
| 342 |
+
a_minus_cb( # |Xₙ-Yₙ| - (1/2) |Xₙ-Xₙ'|
|
| 343 |
+
abs_beta(x_minus_y),
|
| 344 |
+
self._ensemble_term_weight,
|
| 345 |
+
abs_beta(x_minus_xprime),
|
| 346 |
+
)
|
| 347 |
+
)
|
| 348 |
+
)
|
| 349 |
+
return {'spread': spread, 'skill': skill, 'loss': crps}
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@gin.register(
|
| 353 |
+
denylist=['coarsen_aggregation', 'vector_norm_squared_aggregation']
|
| 354 |
+
)
|
| 355 |
+
class EnergyScoreLoss(EnergyLikeLoss):
|
| 356 |
+
"""Energy score loss on linearly transformed errors.
|
| 357 |
+
|
| 358 |
+
EnergyScoreLoss takes the form (with E expectation)
|
| 359 |
+
E‖X - Y‖^β - ensemble_term_weight * E‖X - X'‖^β
|
| 360 |
+
where ‖⋅‖ is the L2 norm. It is a generalization of CRPS to
|
| 361 |
+
multiple-dimensions.
|
| 362 |
+
|
| 363 |
+
Based on formula 22 in [1]; http://shortn/_Lyu0etEy1F
|
| 364 |
+
|
| 365 |
+
References:
|
| 366 |
+
[1]: Gneiting, T., & Raftery, A. E. (2007). Strictly proper scoring rules,
|
| 367 |
+
prediction, and estimation. Journal of the American Statistical
|
| 368 |
+
Association, 102(477), 359-378.
|
| 369 |
+
"""
|
| 370 |
+
|
| 371 |
+
def _spread_skill_and_loss(
|
| 372 |
+
self,
|
| 373 |
+
x_minus_y: Pytree,
|
| 374 |
+
x_minus_xprime: Pytree,
|
| 375 |
+
) -> dict[str, jax.Array]:
|
| 376 |
+
"""Gets dictionary with 'spread', 'skill', and 'loss' entries."""
|
| 377 |
+
a_minus_cb = self.a_minus_cb
|
| 378 |
+
ensemble_mean = self.ensemble_mean
|
| 379 |
+
component_mean = self.component_mean
|
| 380 |
+
|
| 381 |
+
def sqrt_beta(x: jax.Array) -> jax.Array:
|
| 382 |
+
return model_utils.safe_sqrt(x) ** self._beta
|
| 383 |
+
|
| 384 |
+
def square(tree: Pytree) -> Pytree:
|
| 385 |
+
return tree_map(jnp.square, tree)
|
| 386 |
+
|
| 387 |
+
# With X, X' two i.i.d. predictions,
|
| 388 |
+
# Skill = (1/2)[ ‖X-Y‖ + ‖X'-Y‖ ]
|
| 389 |
+
# Spread = ‖Xₙ-Xₙ'‖
|
| 390 |
+
|
| 391 |
+
# Recall x_minus_y = X-Y on one device and X'-Y on another. So the ensemble
|
| 392 |
+
# mean of this (which is all-reduced) is exactly Skill above.
|
| 393 |
+
skill = ensemble_mean(sqrt_beta(component_mean(square(x_minus_y))))
|
| 394 |
+
|
| 395 |
+
# One device has X-X' and the other has X'-X, so the ensemble mean is the
|
| 396 |
+
# same on both devices. The call to ensemble_mean simply removes the
|
| 397 |
+
# ensemble dim.
|
| 398 |
+
spread = ensemble_mean(sqrt_beta(component_mean(square(x_minus_xprime))))
|
| 399 |
+
|
| 400 |
+
# The straightforward implementation will lose resolution when the relative
|
| 401 |
+
# difference between
|
| 402 |
+
# ‖X - X'‖ AND ‖X - Y‖ + ‖X' - Y‖,
|
| 403 |
+
# is less than 1e-6. This is so unlikely that we do will not handle it.
|
| 404 |
+
es_straightforward = a_minus_cb(skill, self._ensemble_term_weight, spread)
|
| 405 |
+
es = es_straightforward
|
| 406 |
+
|
| 407 |
+
# Unused demonstration of how to handle this co-linear case with lots of
|
| 408 |
+
# extra complex operations.
|
| 409 |
+
# if float(self._beta) != 1:
|
| 410 |
+
# es = es_straightforward
|
| 411 |
+
# else:
|
| 412 |
+
# # If beta == 1, there is a high resolution fix.
|
| 413 |
+
# # See http://screen/BkvX57d9B9eqMrB
|
| 414 |
+
# #
|
| 415 |
+
# # alpha = ‖X - Y‖²
|
| 416 |
+
# alpha = component_mean(square(x_minus_y))
|
| 417 |
+
# # And if ensemble_term_weight == 1/2,
|
| 418 |
+
# # gamma_minus_alpha = ‖X - X'‖²/4 - ‖X - Y‖²
|
| 419 |
+
# # = (1/N) Σₙ[ (Xₙ-Xₙ')²/4 - (Xₙ-Yₙ)² ]
|
| 420 |
+
# gamma_minus_alpha = component_mean(
|
| 421 |
+
# self.ca_minus_b(
|
| 422 |
+
# self._ensemble_term_weight**2,
|
| 423 |
+
# square(x_minus_xprime),
|
| 424 |
+
# square(x_minus_y),
|
| 425 |
+
# )
|
| 426 |
+
# )
|
| 427 |
+
# # If gamma = ‖X - X'‖²/4 = 0 (e.g. at step=0), then (γ-α)/α = -1,
|
| 428 |
+
# # and then grad(sqrt1pm1) is NaN. However, in this case we can use the
|
| 429 |
+
# # straightforward version with no issues.
|
| 430 |
+
# gamma_minus_alpha_div_alpha = gamma_minus_alpha / alpha
|
| 431 |
+
|
| 432 |
+
# # Construct a "safe" input to use in the go/tf-where-nan trick.
|
| 433 |
+
# cutoff = -0.1
|
| 434 |
+
# safe_gamma_minus_alpha_div_alpha = jnp.maximum(
|
| 435 |
+
# gamma_minus_alpha / alpha, cutoff
|
| 436 |
+
# )
|
| 437 |
+
|
| 438 |
+
# # For γ ≈ α, safe_gamma_minus_alpha_div_alpha =
|
| 439 |
+
# # gamma_minus_alpha_div_alpha, and this code block will be used.
|
| 440 |
+
# es_for_small_diffs = ensemble_mean(
|
| 441 |
+
# # sqrt1pm1(z) = sqrt(z + 1) - 1, so
|
| 442 |
+
# # sqrt(α) * -1 * sqrt1pm1((γ-α)/α)
|
| 443 |
+
# # = sqrt(α) * (1 - sqrt((γ-α)/α) + 1)
|
| 444 |
+
# # = sqrt(α) * (1 - sqrt(γ/α))
|
| 445 |
+
# # = sqrt(α) - sqrt(γ)
|
| 446 |
+
# # = sqrt(‖X - Y‖²) - sqrt(‖X - X'‖²/4)
|
| 447 |
+
# jnp.sqrt(alpha)
|
| 448 |
+
# * -1
|
| 449 |
+
# * tfp.math.sqrt1pm1(safe_gamma_minus_alpha_div_alpha)
|
| 450 |
+
# )
|
| 451 |
+
# es = jnp.where(
|
| 452 |
+
# # Reminder that the triangle-inequality shows γ <= α always. So
|
| 453 |
+
# # (γ - α) / α < 0.1 is a "large diff" (despite being negative).
|
| 454 |
+
# gamma_minus_alpha_div_alpha < cutoff,
|
| 455 |
+
# es_straightforward,
|
| 456 |
+
# es_for_small_diffs,
|
| 457 |
+
# )
|
| 458 |
+
return {'spread': spread, 'skill': skill, 'loss': es}
|
model/reference_code/train_utils.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Training utility functions for NeuralGCM."""
|
| 15 |
+
|
| 16 |
+
import collections
|
| 17 |
+
from collections import abc
|
| 18 |
+
import functools
|
| 19 |
+
import logging
|
| 20 |
+
import math
|
| 21 |
+
from typing import (
|
| 22 |
+
Any,
|
| 23 |
+
Callable,
|
| 24 |
+
Iterable,
|
| 25 |
+
Iterator,
|
| 26 |
+
Mapping,
|
| 27 |
+
Optional,
|
| 28 |
+
Tuple,
|
| 29 |
+
TypeVar,
|
| 30 |
+
Union,
|
| 31 |
+
)
|
| 32 |
+
from dinosaur import pytree_utils
|
| 33 |
+
from dinosaur import typing
|
| 34 |
+
import einops
|
| 35 |
+
import gin
|
| 36 |
+
import haiku as hk
|
| 37 |
+
import jax
|
| 38 |
+
from jax.experimental import mesh_utils
|
| 39 |
+
import jax.numpy as jnp
|
| 40 |
+
from model.legacy import optimization
|
| 41 |
+
import numpy as np
|
| 42 |
+
import optax
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# pylint: disable=logging-fstring-interpolation
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
PRNGKeyArray = typing.PRNGKeyArray
|
| 49 |
+
Array = Union[np.ndarray, jnp.ndarray]
|
| 50 |
+
PyTree = Any
|
| 51 |
+
Forcing = typing.Forcing
|
| 52 |
+
|
| 53 |
+
IntOrArray = Union[int, Array]
|
| 54 |
+
OptState = optimization.OptState
|
| 55 |
+
ModelParams = Any
|
| 56 |
+
ModelGradients = ModelParams
|
| 57 |
+
EMAParams = ModelParams
|
| 58 |
+
StepAndOptState = Tuple[IntOrArray, OptState]
|
| 59 |
+
StepOptAndEMAState = Tuple[IntOrArray, OptState, ModelParams]
|
| 60 |
+
LossValue = Array
|
| 61 |
+
LossFunction = Callable[[PyTree, PyTree], LossValue]
|
| 62 |
+
LossAndGradFunction = Callable[
|
| 63 |
+
[ModelParams, PRNGKeyArray, PyTree, Forcing],
|
| 64 |
+
Tuple[LossValue, ModelGradients],
|
| 65 |
+
]
|
| 66 |
+
MetricFunction = Callable[[PyTree, PyTree], Union[Array, Mapping[str, Array]]]
|
| 67 |
+
TrainStepFunction = Callable[
|
| 68 |
+
[PRNGKeyArray, StepAndOptState, PyTree, Forcing],
|
| 69 |
+
Tuple[StepAndOptState, LossValue],
|
| 70 |
+
]
|
| 71 |
+
EvalStepFunction = Callable[
|
| 72 |
+
[ModelParams, PRNGKeyArray, PyTree, Forcing], Mapping[str, Array]
|
| 73 |
+
]
|
| 74 |
+
TrajectoryFunction = Callable[
|
| 75 |
+
[ModelParams, PRNGKeyArray, PyTree, Forcing], Tuple[PyTree, PyTree]
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def flatten_dict(
|
| 80 |
+
inputs: Mapping[str, Any],
|
| 81 |
+
parent_key: str = '',
|
| 82 |
+
sep: str = ' ',
|
| 83 |
+
) -> Mapping[str, Array]:
|
| 84 |
+
"""Returns a flattened version of `inputs` dictionary."""
|
| 85 |
+
items = []
|
| 86 |
+
for k, v in inputs.items():
|
| 87 |
+
new_key = parent_key + sep + k if parent_key else k
|
| 88 |
+
if isinstance(v, Mapping):
|
| 89 |
+
items.extend(flatten_dict(v, new_key, sep=sep).items())
|
| 90 |
+
else:
|
| 91 |
+
items.append((new_key, v))
|
| 92 |
+
keys, counts = np.unique(np.array([x[0] for x in items]), return_counts=True)
|
| 93 |
+
if (counts > 1).any():
|
| 94 |
+
raise ValueError(f'got duplicate keys {keys[counts > 1]}')
|
| 95 |
+
return dict(items)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
#
|
| 99 |
+
# Note that all functions below deal with *batched* inputs.
|
| 100 |
+
#
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def loss_and_gradient(
|
| 104 |
+
trajectory_fn: TrajectoryFunction,
|
| 105 |
+
loss_fn: LossFunction,
|
| 106 |
+
) -> LossAndGradFunction:
|
| 107 |
+
"""Returns a function that computes loss and the gradient of the loss.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
trajectory_fn: a function that accepts `params` and `initial_velocity` and
|
| 111 |
+
returns a trajectory of velocities.
|
| 112 |
+
loss_fn: a function that accepts a predicted trajectory and a ground truth
|
| 113 |
+
trajectory, returning a scalar loss value.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
A function that accepts `params, initial_velocity, target_trajectory` and
|
| 117 |
+
returns the loss and the gradient of the loss.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
def _loss(
|
| 121 |
+
params: ModelParams,
|
| 122 |
+
rng: PRNGKeyArray,
|
| 123 |
+
target_trajectory: PyTree,
|
| 124 |
+
forcing_data: typing.ForcingData,
|
| 125 |
+
) -> LossValue:
|
| 126 |
+
"""Returns loss value and gradient with respect to model parameters."""
|
| 127 |
+
_, predicted_trajectory = trajectory_fn(
|
| 128 |
+
params, rng, target_trajectory, forcing_data
|
| 129 |
+
)
|
| 130 |
+
loss = loss_fn(predicted_trajectory, target_trajectory) # type: ignore
|
| 131 |
+
return loss
|
| 132 |
+
|
| 133 |
+
return jax.value_and_grad(_loss)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def train_step(
|
| 137 |
+
loss_and_grad_fn: LossAndGradFunction,
|
| 138 |
+
optimizer: optax.GradientTransformation,
|
| 139 |
+
) -> TrainStepFunction:
|
| 140 |
+
"""Returns a function that performs a single training step.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
loss_and_grad_fn: a function that accepts `params, initial_velocity,
|
| 144 |
+
target_trajectory` and returns the loss and the gradient of the loss.
|
| 145 |
+
optimizer: Optax optimizer to update params and internal state.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
A function that performs a single training step.
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
def _train_step(
|
| 152 |
+
rng: PRNGKeyArray,
|
| 153 |
+
step_and_state: StepAndOptState,
|
| 154 |
+
target_trajectory: PyTree,
|
| 155 |
+
forcing_data: typing.ForcingData,
|
| 156 |
+
) -> Tuple[StepAndOptState, LossValue]:
|
| 157 |
+
"""A function that performs a single training step."""
|
| 158 |
+
step, opt_state = step_and_state
|
| 159 |
+
loss, grad = loss_and_grad_fn(
|
| 160 |
+
opt_state.params, rng, target_trajectory, forcing_data
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
updates, new_state = optimizer.update(
|
| 164 |
+
grad, opt_state.state, opt_state.params
|
| 165 |
+
)
|
| 166 |
+
new_params = optax.apply_updates(opt_state.params, updates)
|
| 167 |
+
new_opt_state = OptState(state=new_state, params=new_params)
|
| 168 |
+
|
| 169 |
+
return (step + 1, new_opt_state), loss
|
| 170 |
+
|
| 171 |
+
return _train_step
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def eval_batch(
|
| 175 |
+
trajectory_fn: TrajectoryFunction,
|
| 176 |
+
metric_funcs: Mapping[str, MetricFunction],
|
| 177 |
+
) -> EvalStepFunction:
|
| 178 |
+
"""Returns a function that performs a single evaluation step.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
trajectory_fn: a function that accepts `params` and `initial_velocity` and
|
| 182 |
+
returns a trajectory of velocities.
|
| 183 |
+
metric_funcs: a dictionary mapping strings to metric funcutils, each
|
| 184 |
+
returning either a metric scalar or a dictionary of such.
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
A function that performs a single evaluation step.
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
def _eval_batch(
|
| 191 |
+
params: ModelParams,
|
| 192 |
+
rng: PRNGKeyArray,
|
| 193 |
+
target_trajectory: PyTree,
|
| 194 |
+
forcing_data: typing.ForcingData,
|
| 195 |
+
) -> Mapping[str, Array]:
|
| 196 |
+
"""A function that performs a single evaluation step."""
|
| 197 |
+
_, predicted_trajectory = trajectory_fn(
|
| 198 |
+
params, rng, target_trajectory, forcing_data
|
| 199 |
+
)
|
| 200 |
+
metric_values = {
|
| 201 |
+
k: metric(predicted_trajectory, target_trajectory)
|
| 202 |
+
for k, metric in metric_funcs.items()
|
| 203 |
+
}
|
| 204 |
+
results = flatten_dict(metric_values)
|
| 205 |
+
return results
|
| 206 |
+
|
| 207 |
+
return _eval_batch
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def streaming_mean(
|
| 211 |
+
rngs: Iterable[PRNGKeyArray],
|
| 212 |
+
batch_and_forcing: Iterable[Tuple[PyTree, Forcing]],
|
| 213 |
+
eval_fn: Callable[[PRNGKeyArray, PyTree, Forcing], Mapping[str, Array]],
|
| 214 |
+
data_preprocess_fn: Callable[..., PyTree] = lambda x: x,
|
| 215 |
+
) -> Mapping[str, Array]:
|
| 216 |
+
"""Runs evaluation on `eval_data`.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
rngs: an iterable of random number keys to be used for evaluation.
|
| 220 |
+
batch_and_forcing: an iterable of batched velocity trajectories and forcing.
|
| 221 |
+
eval_fn: a function that performs a single evaluation step.
|
| 222 |
+
data_preprocess_fn: a preprocessing function be applied to each batch.
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
A dict mapping strings to metric values.
|
| 226 |
+
|
| 227 |
+
Raises:
|
| 228 |
+
RuntimeError: if there are no batches to iterate over.
|
| 229 |
+
"""
|
| 230 |
+
eval_metrics = collections.defaultdict(float)
|
| 231 |
+
count = 0
|
| 232 |
+
for rng, (batch, forcing) in zip(rngs, batch_and_forcing):
|
| 233 |
+
batch = data_preprocess_fn(batch)
|
| 234 |
+
batch_metrics = eval_fn(rng, batch, forcing)
|
| 235 |
+
for k, v in batch_metrics.items():
|
| 236 |
+
eval_metrics[k] += v
|
| 237 |
+
count += 1
|
| 238 |
+
if not count:
|
| 239 |
+
raise RuntimeError('no batches to iterate over')
|
| 240 |
+
return {k: v / count for k, v in eval_metrics.items()}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@gin.register
|
| 244 |
+
def identity(batch: Tuple[Array, ...], rng: Array = None) -> Tuple[Array, ...]: # pytype: disable=annotation-type-mismatch # jax-ndarray
|
| 245 |
+
"""Identity preprocessing function that does not modify the `batch`."""
|
| 246 |
+
del rng # unused.
|
| 247 |
+
return batch
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@gin.configurable
|
| 251 |
+
def add_noise_to_input_frame(
|
| 252 |
+
batch: Tuple[Array, ...], rng: Array, scale: float = 1e-2, **kwargs
|
| 253 |
+
) -> Tuple[Array, ...]:
|
| 254 |
+
"""Adds noise to the 0th time frame in the `batch`.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
batch: original batch to which the noise will be added.
|
| 258 |
+
rng: random number key to be used to generate noise.
|
| 259 |
+
scale: scale of the normal noise to be added.
|
| 260 |
+
**kwargs: other keyword arguments. Not used.
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
batch with noise added along the 0th time slice.
|
| 264 |
+
"""
|
| 265 |
+
del kwargs # unused.
|
| 266 |
+
time_zero_slice = pytree_utils.slice_along_axis(batch, 1, 0)
|
| 267 |
+
shapes = jax.tree.map(np.shape, time_zero_slice)
|
| 268 |
+
rngs = jax.random.split(rng, len(jax.tree.leaves(time_zero_slice)))
|
| 269 |
+
rngs = jax.tree.unflatten(jax.tree.structure(time_zero_slice), rngs)
|
| 270 |
+
|
| 271 |
+
def noise_fn(key, s):
|
| 272 |
+
return scale * jax.random.truncated_normal(key, -2.0, 2.0, s)
|
| 273 |
+
|
| 274 |
+
noise = jax.tree.map(noise_fn, rngs, shapes)
|
| 275 |
+
add_noise_fn = lambda x, n: x.at[:, 0, ...].add(n)
|
| 276 |
+
return jax.tree.map(add_noise_fn, batch, noise)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def preprocess(
|
| 280 |
+
data_iterator: Iterator[Tuple[Array, ...]],
|
| 281 |
+
rng_stream: Iterator[Array],
|
| 282 |
+
preprocess_fn: Callable[..., Tuple[Array, ...]],
|
| 283 |
+
):
|
| 284 |
+
"""Generator that applies `preprocess_fn` to entries of the `data_iterator`.
|
| 285 |
+
|
| 286 |
+
Args:
|
| 287 |
+
data_iterator: numpy iterator holding the data.
|
| 288 |
+
rng_stream: stream of random numbers to be used by `preprocess_fn`.
|
| 289 |
+
preprocess_fn: preprocessing function to be applied to each batch of data.
|
| 290 |
+
|
| 291 |
+
Yields:
|
| 292 |
+
Batch of data from `data_iterator` preprocessed with `preprocess_fn`.
|
| 293 |
+
"""
|
| 294 |
+
preprocess_fn = jax.jit(preprocess_fn)
|
| 295 |
+
while True:
|
| 296 |
+
rng = next(rng_stream)
|
| 297 |
+
yield preprocess_fn(next(data_iterator), rng)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def split_rngs(rngs: PRNGKeyArray, num: int) -> PRNGKeyArray:
|
| 301 |
+
"""Splits `rngs` into `num` along the last batch axis."""
|
| 302 |
+
ndim = rngs.ndim
|
| 303 |
+
split_fn = jax.random.split
|
| 304 |
+
for _ in range(ndim - 1):
|
| 305 |
+
split_fn = jax.vmap(split_fn, (0, None), 1)
|
| 306 |
+
return split_fn(rngs, num)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@functools.partial(jax.jit, static_argnames=['batch_shape'])
|
| 310 |
+
def _split_rmgs_by_batch_shape(
|
| 311 |
+
rngs: PRNGKeyArray,
|
| 312 |
+
batch_shape: tuple[int, ...],
|
| 313 |
+
) -> PRNGKeyArray:
|
| 314 |
+
for batch_size in batch_shape[::-1]:
|
| 315 |
+
rngs = split_rngs(rngs, batch_size)
|
| 316 |
+
return rngs
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
class BatchedPRNGSequence(Iterator):
|
| 320 |
+
"""Iterator of JAX different random keys split by `batch_shape`."""
|
| 321 |
+
|
| 322 |
+
def __init__(
|
| 323 |
+
self,
|
| 324 |
+
key_or_seed: Union[int, PRNGKeyArray],
|
| 325 |
+
batch_shape: Optional[Tuple[int, ...]] = None,
|
| 326 |
+
):
|
| 327 |
+
"""Creates an instance a class.
|
| 328 |
+
|
| 329 |
+
Args:
|
| 330 |
+
key_or_seed: Key or seed to initialize the random sequence.
|
| 331 |
+
batch_shape: Batch shape of the sequence.
|
| 332 |
+
"""
|
| 333 |
+
self._key = hk.PRNGSequence(key_or_seed)
|
| 334 |
+
self.batch_shape = batch_shape
|
| 335 |
+
|
| 336 |
+
def reserve(self, num: int):
|
| 337 |
+
"""Splits an additional ``num`` keys for later use."""
|
| 338 |
+
self._key = self._key.reserve(num)
|
| 339 |
+
|
| 340 |
+
def __next__(self):
|
| 341 |
+
rngs = next(self._key)
|
| 342 |
+
return _split_rmgs_by_batch_shape(rngs, self.batch_shape)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@jax.jit
|
| 346 |
+
def _combine_rng_seeds(seeds: jax.Array) -> jax.Array:
|
| 347 |
+
key = jax.random.PRNGKey(seeds[0])
|
| 348 |
+
for seed in seeds[1:]:
|
| 349 |
+
key = jax.random.fold_in(key, seed)
|
| 350 |
+
return jax.random.bits(key, shape=(), dtype=jnp.uint32)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def combine_rng_seeds(*seeds: int) -> int:
|
| 354 |
+
"""Combine uint32 seeds into a single Python integer RNG seed."""
|
| 355 |
+
# Put the seeds on the first CPU device so that JAX runs the entire
|
| 356 |
+
# computation on the CPU.
|
| 357 |
+
seeds = jax.device_put(
|
| 358 |
+
np.array(seeds), device=jax.local_devices(backend='cpu')[0]
|
| 359 |
+
)
|
| 360 |
+
return int(_combine_rng_seeds(seeds))
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def ensure_sharded_rng_key(
|
| 364 |
+
rng_key: jax.Array, *, mesh: jax.sharding.Mesh
|
| 365 |
+
) -> jax.Array:
|
| 366 |
+
"""Ensure that a batched PRNG key is sharded across all devices."""
|
| 367 |
+
spec = P('batch', 'ensemble', None)
|
| 368 |
+
sharding = jax.sharding.NamedSharding(mesh, spec)
|
| 369 |
+
return jax.lax.with_sharding_constraint(rng_key, sharding)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def get_tpu_physical_mesh_shape() -> tuple[int, int, int] | None:
|
| 373 |
+
"""Get the shape of the TPU connectivity torus for v4 or v5 chips."""
|
| 374 |
+
jax_devices = jax.devices()
|
| 375 |
+
try:
|
| 376 |
+
device_coords = [d.coords for d in jax_devices]
|
| 377 |
+
except AttributeError:
|
| 378 |
+
return None # no "coords" attribute (e.g., using CPU devices)
|
| 379 |
+
dims = tuple(d + 1 for d in max(device_coords))
|
| 380 |
+
if len(dims) != 3 or math.prod(dims) != len(jax_devices):
|
| 381 |
+
return None
|
| 382 |
+
return dims
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# dict of dicts of indicating how to rearrange from physical TPU mesh layouts
|
| 386 |
+
# (X, Y, Z) into logical mesh layouts (batch, ensemble, z, x, y) with
|
| 387 |
+
# einops.rearrange for model training.
|
| 388 |
+
# {tpu_topology: {(ensemble_shards, z_shard, x_shards, y_shards): ...}}
|
| 389 |
+
_TPU_LAYOUT_REARRANGEMENTS = {
|
| 390 |
+
'2x2x2': {
|
| 391 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 392 |
+
(1, 2, 1, 1): 'z b0 b1 -> (b0 b1) () z () ()',
|
| 393 |
+
(2, 1, 1, 1): 'e b0 b1 -> (b0 b1) e () () ()',
|
| 394 |
+
},
|
| 395 |
+
'2x2x4': {
|
| 396 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 397 |
+
(1, 2, 1, 1): 'z b0 b1 -> (b0 b1) () z () ()',
|
| 398 |
+
(1, 4, 1, 1): 'b0 b1 z -> (b0 b1) () z () ()',
|
| 399 |
+
(2, 1, 1, 1): 'e b0 b1 -> (b0 b1) e () () ()',
|
| 400 |
+
},
|
| 401 |
+
'2x4x4': {
|
| 402 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 403 |
+
(1, 2, 1, 1): 'z b0 b1 -> (b0 b1) () z () ()',
|
| 404 |
+
(1, 4, 1, 1): 'b0 b1 z -> (b0 b1) () z () ()',
|
| 405 |
+
(2, 1, 1, 1): 'e b0 b1 -> (b0 b1) e () () ()',
|
| 406 |
+
(2, 2, 1, 1): 'z (b0 e) b1 -> (b0 b1) e z () ()',
|
| 407 |
+
},
|
| 408 |
+
'4x4x4': {
|
| 409 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 410 |
+
(1, 2, 1, 1): '(b0 z) b1 b2 -> (b0 b1 b2) () z () ()',
|
| 411 |
+
(1, 4, 1, 1): 'b0 b1 z -> (b0 b1) () z () ()',
|
| 412 |
+
(1, 2, 2, 1): '(b0 z) (b1 x) b2 -> (b0 b1 b2) () z x ()',
|
| 413 |
+
(2, 1, 1, 1): '(b0 e) b1 b2 -> (b0 b1 b2) e () () ()',
|
| 414 |
+
(2, 2, 1, 1): '(b0 e) (b1 z) b2 -> (b0 b1 b2) e z () ()',
|
| 415 |
+
(2, 2, 2, 1): '(b0 e) (b1 z) (b2 x) -> (b0 b1 b2) e z x ()',
|
| 416 |
+
},
|
| 417 |
+
'4x4x8': {
|
| 418 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 419 |
+
(1, 4, 2, 1): 'z (b0 x) b1 -> (b0 b1) () z x ()',
|
| 420 |
+
(1, 4, 2, 2): 'z (b0 x) (b1 y) -> (b0 b1) () z x y',
|
| 421 |
+
(2, 4, 2, 1): 'z (b0 x) (b1 e) -> (b0 b1) e z x ()',
|
| 422 |
+
},
|
| 423 |
+
'4x8x8': {
|
| 424 |
+
(1, 1, 1, 1): 'b0 b1 b2 -> (b0 b1 b2) () () () ()',
|
| 425 |
+
(1, 4, 2, 1): 'z (b0 x) b1 -> (b0 b1) () z x ()',
|
| 426 |
+
(1, 4, 2, 2): 'z (b0 x) (b1 y) -> (b0 b1) () z x y',
|
| 427 |
+
(2, 4, 2, 1): 'z (b0 e) (b1 x) -> (b0 b1) e z x ()',
|
| 428 |
+
(2, 4, 2, 2): 'z (b0 e x) (b1 y) -> (b0 b1) e z x y',
|
| 429 |
+
},
|
| 430 |
+
'2x2x1': {
|
| 431 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 432 |
+
(1, 2, 1, 1): 'z b0 () -> b0 () z () ()',
|
| 433 |
+
(2, 1, 1, 1): 'e b0 () -> b0 e () () ()',
|
| 434 |
+
(2, 2, 1, 1): 'e z () -> () e z () ()',
|
| 435 |
+
},
|
| 436 |
+
'2x4x1': {
|
| 437 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 438 |
+
(1, 2, 1, 1): 'z b0 () -> b0 () z () ()',
|
| 439 |
+
(2, 1, 1, 1): 'e b0 () -> b0 e () () ()',
|
| 440 |
+
(2, 2, 1, 1): 'z (b0 e) -> b0 e z () ()',
|
| 441 |
+
},
|
| 442 |
+
'4x4x1': {
|
| 443 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 444 |
+
(1, 2, 1, 1): '(b0 z) b1 () -> (b0 b1) () z () ()',
|
| 445 |
+
(2, 1, 1, 1): '(b0 e) b1 () -> (b0 b1) e () () ()',
|
| 446 |
+
(2, 2, 1, 1): '(b0 e) (b1 z) () -> (b0 b1) e z () ()',
|
| 447 |
+
},
|
| 448 |
+
'4x8x1': {
|
| 449 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 450 |
+
(1, 2, 1, 1): '(b0 z) b1 () -> (b0 b1) () z () ()',
|
| 451 |
+
(1, 2, 2, 1): '(b0 z) (b1 x) () -> (b0 b1) () z x ()',
|
| 452 |
+
(2, 1, 1, 1): '(b0 e) b1 () -> (b0 b1) e () () ()',
|
| 453 |
+
(2, 2, 1, 1): '(b0 e) (b1 z) () -> (b0 b1) e z () ()',
|
| 454 |
+
},
|
| 455 |
+
'8x8x1': {
|
| 456 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 457 |
+
(1, 2, 1, 1): '(b0 z) b1 () -> (b0 b1) () z () ()',
|
| 458 |
+
(1, 2, 2, 1): '(b0 z) (b1 x) () -> (b0 b1) () z x ()',
|
| 459 |
+
(2, 1, 1, 1): '(b0 e) b1 () -> (b0 b1) e () () ()',
|
| 460 |
+
(2, 2, 1, 1): '(b0 e) (b1 z) () -> (b0 b1) e z () ()',
|
| 461 |
+
(2, 2, 2, 1): '(b0 e z) (b1 x) () -> (b0 b1) e z x ()',
|
| 462 |
+
},
|
| 463 |
+
'8x16x1': {
|
| 464 |
+
(1, 1, 1, 1): 'b0 b1 () -> (b0 b1) () () () ()',
|
| 465 |
+
(1, 2, 1, 1): '(b0 z) b1 () -> (b0 b1) () z () ()',
|
| 466 |
+
(1, 2, 2, 1): '(b0 z) (b1 x) () -> (b0 b1) () z x ()',
|
| 467 |
+
(2, 1, 1, 1): '(b0 e) b1 () -> (b0 b1) e () () ()',
|
| 468 |
+
(2, 2, 1, 1): '(b0 e) (b1 z) () -> (b0 b1) e z () ()',
|
| 469 |
+
(2, 2, 2, 1): '(b0 e z) (b1 x) () -> (b0 b1) e z x ()',
|
| 470 |
+
},
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def create_spmd_mesh(sizes: dict[str, int]) -> jax.sharding.Mesh:
|
| 475 |
+
"""Create an SPMD mesh suitable for data & model parallelism.
|
| 476 |
+
|
| 477 |
+
Args:
|
| 478 |
+
sizes: dictionary mapping from dimension names (batch, z, x, and y) to the
|
| 479 |
+
number of devices desired along that axis in the parallel mesh.
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
Mesh with axis names ['batch', 'ensemble', 'x', 'y', 'z'] and the desired
|
| 483 |
+
axis sizes.
|
| 484 |
+
"""
|
| 485 |
+
axis_names = ['batch', 'ensemble', 'z', 'x', 'y']
|
| 486 |
+
for name in sizes:
|
| 487 |
+
if name not in axis_names:
|
| 488 |
+
raise ValueError(f'unrecognized {name!r} not in {axis_names}')
|
| 489 |
+
|
| 490 |
+
logical_mesh_shape = tuple(
|
| 491 |
+
sizes.get(axis_name, 1) for axis_name in axis_names
|
| 492 |
+
)
|
| 493 |
+
if math.prod(logical_mesh_shape) != jax.device_count():
|
| 494 |
+
raise ValueError(
|
| 495 |
+
f'{logical_mesh_shape=} is incompatible with {jax.device_count()=}'
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
physical_mesh_shape = get_tpu_physical_mesh_shape()
|
| 499 |
+
if physical_mesh_shape is None:
|
| 500 |
+
try:
|
| 501 |
+
# only succeeds if the logical mesh shape perfectly matches the physical
|
| 502 |
+
# mesh, e.g., in the case of pure data parallelism
|
| 503 |
+
mesh_devices = mesh_utils.create_device_mesh(logical_mesh_shape)
|
| 504 |
+
except (AssertionError, NotImplementedError):
|
| 505 |
+
mesh_devices = np.reshape(jax.devices(), logical_mesh_shape)
|
| 506 |
+
else:
|
| 507 |
+
devices = np.empty(physical_mesh_shape, dtype=object)
|
| 508 |
+
for device in jax.devices():
|
| 509 |
+
devices[tuple(device.coords)] = device
|
| 510 |
+
|
| 511 |
+
topology = 'x'.join(map(str, physical_mesh_shape))
|
| 512 |
+
logical_mesh_shape = tuple(
|
| 513 |
+
sizes[dim] for dim in ['ensemble', 'z', 'x', 'y']
|
| 514 |
+
)
|
| 515 |
+
rearrangement = _TPU_LAYOUT_REARRANGEMENTS[topology][logical_mesh_shape]
|
| 516 |
+
|
| 517 |
+
abbreviated_sizes = {
|
| 518 |
+
'e': sizes['ensemble'],
|
| 519 |
+
'z': sizes['z'],
|
| 520 |
+
'x': sizes['x'],
|
| 521 |
+
'y': sizes['y'],
|
| 522 |
+
}
|
| 523 |
+
abbreviated_sizes = {k: v for k, v in abbreviated_sizes.items() if v != 1}
|
| 524 |
+
mesh_devices = einops.rearrange(devices, rearrangement, **abbreviated_sizes)
|
| 525 |
+
|
| 526 |
+
return jax.sharding.Mesh(mesh_devices, axis_names)
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
P = jax.sharding.PartitionSpec
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def make_distributed_array_from_local_arrays(
|
| 533 |
+
pytree: PyTree,
|
| 534 |
+
mesh: jax.sharding.Mesh,
|
| 535 |
+
spatial_partitions: jax.sharding.PartitionSpec,
|
| 536 |
+
global_batch_size: int,
|
| 537 |
+
) -> PyTree:
|
| 538 |
+
"""Creates a pytree of global jax arrays for data/model parallelsm.
|
| 539 |
+
|
| 540 |
+
This function exists for loading spatially partitioned data, which is assumed
|
| 541 |
+
to be replicated across the ensemble dimension.
|
| 542 |
+
|
| 543 |
+
Args:
|
| 544 |
+
pytree: PyTree of NumPy arrays to convert into distributed JAX arrays. The
|
| 545 |
+
leading "batch" dimension is divided between different local devices.
|
| 546 |
+
mesh: SPDM sharding mesh.
|
| 547 |
+
spatial_partitions: JAX partition spec (of length 3) to use for partitioning
|
| 548 |
+
spatial dimensions (z, x, y).
|
| 549 |
+
global_batch_size: number distinct examples in a single batch across all
|
| 550 |
+
devices. Does not include the ensemble.
|
| 551 |
+
|
| 552 |
+
Returns:
|
| 553 |
+
Pytree with the same structure as the inputs, but with arrays replaced by
|
| 554 |
+
distributed JAX arrays.
|
| 555 |
+
"""
|
| 556 |
+
if len(spatial_partitions) != 3:
|
| 557 |
+
raise ValueError(f'invalid {spatial_partitions=}')
|
| 558 |
+
|
| 559 |
+
def get_shard_count(spec_part: None | str | tuple[str, ...]) -> int:
|
| 560 |
+
# calculate the number of shards corresponding to an element in a
|
| 561 |
+
# PartitionSpec
|
| 562 |
+
if spec_part is None:
|
| 563 |
+
return 1
|
| 564 |
+
elif isinstance(spec_part, str):
|
| 565 |
+
return mesh.shape[spec_part]
|
| 566 |
+
else:
|
| 567 |
+
return math.prod(mesh.shape[x] for x in spec_part)
|
| 568 |
+
|
| 569 |
+
def shard_array(x: np.ndarray) -> jax.Array:
|
| 570 |
+
if x.ndim <= 3:
|
| 571 |
+
# handle sim_time [batch]
|
| 572 |
+
global_shape = (global_batch_size,) + x.shape[1:]
|
| 573 |
+
partition_spec = P('batch', *([None] * (x.ndim - 1)))
|
| 574 |
+
elif x.ndim == 4:
|
| 575 |
+
# This is currently needed to handle surface data that has shape:
|
| 576 |
+
# [batch, time, x, y].
|
| 577 |
+
_, x_shards, y_shards = map(get_shard_count, spatial_partitions)
|
| 578 |
+
global_shape = (
|
| 579 |
+
global_batch_size,
|
| 580 |
+
x.shape[1],
|
| 581 |
+
x.shape[2] * x_shards,
|
| 582 |
+
x.shape[3] * y_shards,
|
| 583 |
+
)
|
| 584 |
+
partition_spec = P('batch', None, *spatial_partitions[1:])
|
| 585 |
+
else:
|
| 586 |
+
# everything else has dimensions [batch, time, z, x, y]
|
| 587 |
+
assert x.ndim == 5, x.shape
|
| 588 |
+
z_shards, x_shards, y_shards = map(get_shard_count, spatial_partitions)
|
| 589 |
+
if x.shape[2] == 1:
|
| 590 |
+
z_shards = 1
|
| 591 |
+
global_shape = (
|
| 592 |
+
global_batch_size,
|
| 593 |
+
x.shape[1],
|
| 594 |
+
x.shape[2] * z_shards,
|
| 595 |
+
x.shape[3] * x_shards,
|
| 596 |
+
x.shape[4] * y_shards,
|
| 597 |
+
)
|
| 598 |
+
partition_spec = P('batch', None, *spatial_partitions)
|
| 599 |
+
|
| 600 |
+
sharding = jax.sharding.NamedSharding(mesh, partition_spec)
|
| 601 |
+
single_device_arrays = put_to_devices(x, jax.local_devices(), axis=0)
|
| 602 |
+
return jax.make_array_from_single_device_arrays(
|
| 603 |
+
global_shape, sharding, single_device_arrays
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
try:
|
| 607 |
+
return jax.tree_util.tree_map(shard_array, pytree)
|
| 608 |
+
except Exception as e:
|
| 609 |
+
shape_tree = jax.tree_util.tree_map(jnp.shape, pytree)
|
| 610 |
+
raise RuntimeError(
|
| 611 |
+
f'failed to shard arrays with shapes {shape_tree!r}'
|
| 612 |
+
) from e
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
def put_to_devices(
|
| 616 |
+
host_array: np.ndarray, local_devices: abc.Sequence[Any], axis: int
|
| 617 |
+
) -> list[Any]:
|
| 618 |
+
"""Transfers a host array to local devices, split on the first dimension."""
|
| 619 |
+
local_device_count = len(local_devices)
|
| 620 |
+
try:
|
| 621 |
+
per_device_arrays = np.split(host_array, local_device_count, axis=axis)
|
| 622 |
+
except ValueError as array_split_error:
|
| 623 |
+
raise ValueError(
|
| 624 |
+
f'Unable to put to devices shape {host_array.shape} with '
|
| 625 |
+
f'local device count {local_device_count}'
|
| 626 |
+
) from array_split_error
|
| 627 |
+
device_buffers = [
|
| 628 |
+
jax.device_put(arr, d) for arr, d in zip(per_device_arrays, local_devices)
|
| 629 |
+
]
|
| 630 |
+
return device_buffers
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def ensure_replicated(pytree: PyTree, *, mesh: jax.sharding.Mesh) -> PyTree:
|
| 634 |
+
"""Ensure that a pytree is replicated across all devices."""
|
| 635 |
+
|
| 636 |
+
def replicate(x):
|
| 637 |
+
x = jnp.asarray(x)
|
| 638 |
+
spec = jax.sharding.PartitionSpec(*([None] * x.ndim))
|
| 639 |
+
sharding = jax.sharding.NamedSharding(mesh, spec)
|
| 640 |
+
return jax.lax.with_sharding_constraint(x, sharding)
|
| 641 |
+
|
| 642 |
+
return jax.tree_util.tree_map(replicate, pytree)
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
T = TypeVar('T')
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def jit_once(f: T, **jit_kwargs) -> T:
|
| 649 |
+
"""Like jax.jit, but raises an error instead of compiling multiple times."""
|
| 650 |
+
compiled = None
|
| 651 |
+
|
| 652 |
+
def g(*args, **kwargs):
|
| 653 |
+
nonlocal compiled
|
| 654 |
+
if compiled is None:
|
| 655 |
+
logging.info(f'lowering {f}')
|
| 656 |
+
lowered = jax.jit(f, **jit_kwargs).lower(*args, **kwargs)
|
| 657 |
+
logging.info(f'compiling {f}')
|
| 658 |
+
compiled = lowered.compile()
|
| 659 |
+
logging.info(f'finishing compiling {f}')
|
| 660 |
+
return compiled(*args, **kwargs)
|
| 661 |
+
|
| 662 |
+
return g
|
scripts/checkpoint_info.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Print parameter and serialization sizes for NeuralGCM checkpoints."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import pickle
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from common import PROJECT_ROOT, resolve_path
|
| 11 |
+
except ModuleNotFoundError: # supports ``python -m scripts.checkpoint_info``
|
| 12 |
+
from scripts.common import PROJECT_ROOT, resolve_path
|
| 13 |
+
|
| 14 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 16 |
+
|
| 17 |
+
from model.NeuralGCM import checkpoint_mode, format_parameter_summary
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main() -> None:
|
| 21 |
+
parser = argparse.ArgumentParser()
|
| 22 |
+
parser.add_argument("checkpoints", nargs="+")
|
| 23 |
+
args = parser.parse_args()
|
| 24 |
+
for value in args.checkpoints:
|
| 25 |
+
path = resolve_path(value)
|
| 26 |
+
with path.open("rb") as handle:
|
| 27 |
+
payload = pickle.load(handle)
|
| 28 |
+
if not isinstance(payload, dict) or "params" not in payload:
|
| 29 |
+
raise ValueError(f"{path} does not contain an official params tree")
|
| 30 |
+
mode = payload.get("mode") or checkpoint_mode(payload) or "unknown"
|
| 31 |
+
training_state = payload.get("training_state")
|
| 32 |
+
resume_text = (
|
| 33 |
+
f"resumable=true step={training_state.get('step')}"
|
| 34 |
+
if isinstance(training_state, dict)
|
| 35 |
+
else "resumable=false"
|
| 36 |
+
)
|
| 37 |
+
print(
|
| 38 |
+
f"checkpoint={path.name} mode={mode} "
|
| 39 |
+
f"file.bytes={path.stat().st_size:,} "
|
| 40 |
+
f"{resume_text} {format_parameter_summary(payload['params'])}"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
main()
|
scripts/common.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared config, channel and OneScience ERA5Dataset helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import yaml
|
| 12 |
+
|
| 13 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
SYNTHETIC_GENERATOR_VERSION = "neuralgcm-hydrostatic-v2"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_config(path: str | Path | None = None) -> dict[str, Any]:
|
| 18 |
+
path = Path(path or PROJECT_ROOT / "conf/config.yaml")
|
| 19 |
+
with path.open(encoding="utf-8") as handle:
|
| 20 |
+
return yaml.safe_load(handle)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def resolve_path(value: str | Path, config_path: str | Path | None = None) -> Path:
|
| 24 |
+
path = Path(value).expanduser()
|
| 25 |
+
if path.is_absolute():
|
| 26 |
+
return path
|
| 27 |
+
base = Path(config_path or PROJECT_ROOT / "conf/config.yaml").resolve().parent.parent
|
| 28 |
+
return base / path
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def channel_order(config: dict[str, Any]) -> list[str]:
|
| 32 |
+
return list(config["data"]["channel_order"])
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def pressure_levels(config: dict[str, Any]) -> list[int]:
|
| 36 |
+
return list(config["model"]["pressure_levels_hpa"])
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def as_time_major_frames(value: Any, *, name: str = "frames"):
|
| 40 |
+
"""Normalize OneScience ERA5Dataset output to ``(T, C, H, W)``.
|
| 41 |
+
|
| 42 |
+
ERA5Dataset squeezes the leading time dimension when ``output_steps=1``;
|
| 43 |
+
callers must restore it before indexing forecast frames. Input frames are
|
| 44 |
+
allowed to remain ``(C, H, W)`` and should not use this helper.
|
| 45 |
+
"""
|
| 46 |
+
import numpy as np
|
| 47 |
+
|
| 48 |
+
if hasattr(value, "detach"):
|
| 49 |
+
value = value.detach().cpu().numpy()
|
| 50 |
+
value = np.asarray(value)
|
| 51 |
+
if value.ndim == 3:
|
| 52 |
+
value = value[None, ...]
|
| 53 |
+
if value.ndim != 4:
|
| 54 |
+
raise ValueError(
|
| 55 |
+
f"{name} must have shape (T,C,H,W) or (C,H,W), got {value.shape}"
|
| 56 |
+
)
|
| 57 |
+
return value
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_era5_dataset(config: dict[str, Any], years: list[int], *, input_steps: int | None = None, output_steps: int | None = None):
|
| 61 |
+
"""Construct the required OneScience ERA5Dataset, without replacing it."""
|
| 62 |
+
try:
|
| 63 |
+
from onescience.datapipes.climate import ERA5Dataset
|
| 64 |
+
except Exception as exc:
|
| 65 |
+
# Source-tree fallback mirrors the earth examples and keeps this
|
| 66 |
+
# project usable before OneScience is installed as a wheel.
|
| 67 |
+
local_src = Path("/public/home/yangzt01/onescience/src")
|
| 68 |
+
if local_src.exists() and str(local_src) not in sys.path:
|
| 69 |
+
sys.path.insert(0, str(local_src))
|
| 70 |
+
try:
|
| 71 |
+
from onescience.datapipes.climate import ERA5Dataset
|
| 72 |
+
except Exception as fallback_exc:
|
| 73 |
+
raise RuntimeError(
|
| 74 |
+
"OneScience ERA5Dataset import failed; load OneScience and its "
|
| 75 |
+
f"runtime modules first: {type(fallback_exc).__name__}: {fallback_exc}"
|
| 76 |
+
) from fallback_exc
|
| 77 |
+
data_dir = resolve_path(config["data"]["data_dir"])
|
| 78 |
+
return ERA5Dataset(
|
| 79 |
+
dataset_dir=str(data_dir),
|
| 80 |
+
used_years=years,
|
| 81 |
+
used_variables=channel_order(config),
|
| 82 |
+
input_steps=input_steps or int(config["data"]["input_steps"]),
|
| 83 |
+
output_steps=output_steps or int(config["data"]["output_steps"]),
|
| 84 |
+
normalize=bool(config["data"].get("normalize", False)),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def era5_data_is_synthetic(config: dict[str, Any], years: list[int]) -> bool:
|
| 89 |
+
"""Return true only when every requested HDF5 file declares synthetic data."""
|
| 90 |
+
import h5py
|
| 91 |
+
|
| 92 |
+
data_dir = resolve_path(config["data"]["data_dir"]) / "data"
|
| 93 |
+
paths = [data_dir / f"{year}.h5" for year in years]
|
| 94 |
+
if not paths or any(not path.exists() for path in paths):
|
| 95 |
+
return False
|
| 96 |
+
try:
|
| 97 |
+
for path in paths:
|
| 98 |
+
with h5py.File(path, "r") as handle:
|
| 99 |
+
fields = handle[config["data"].get("field_key", "fields")]
|
| 100 |
+
if not bool(fields.attrs.get("synthetic", False)):
|
| 101 |
+
return False
|
| 102 |
+
except (KeyError, OSError):
|
| 103 |
+
return False
|
| 104 |
+
return True
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def validate_synthetic_era5_version(
|
| 108 |
+
config: dict[str, Any], years: list[int]
|
| 109 |
+
) -> None:
|
| 110 |
+
"""Reject obsolete virtual fields that are known to destabilize the model."""
|
| 111 |
+
import h5py
|
| 112 |
+
|
| 113 |
+
data_dir = resolve_path(config["data"]["data_dir"]) / "data"
|
| 114 |
+
for year in years:
|
| 115 |
+
path = data_dir / f"{year}.h5"
|
| 116 |
+
with h5py.File(path, "r") as handle:
|
| 117 |
+
fields = handle[config["data"].get("field_key", "fields")]
|
| 118 |
+
if not bool(fields.attrs.get("synthetic", False)):
|
| 119 |
+
continue
|
| 120 |
+
version = fields.attrs.get("generator_version")
|
| 121 |
+
if isinstance(version, bytes):
|
| 122 |
+
version = version.decode()
|
| 123 |
+
if version != SYNTHETIC_GENERATOR_VERSION:
|
| 124 |
+
raise RuntimeError(
|
| 125 |
+
f"Synthetic ERA5 file {path} uses obsolete generator_version="
|
| 126 |
+
f"{version!r}; expected {SYNTHETIC_GENERATOR_VERSION!r}. "
|
| 127 |
+
"Regenerate it with scripts/fake_data.py before running a "
|
| 128 |
+
"NeuralGCM rollout."
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
| 133 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 134 |
+
path.write_text(json.dumps(payload, indent=2, default=str) + "\n", encoding="utf-8")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def era5_sample_to_xarray(sample: Any, config: dict[str, Any], *, timestamp: Any):
|
| 138 |
+
"""Convert one ERA5Dataset frame to the official NeuralGCM xarray contract.
|
| 139 |
+
|
| 140 |
+
The HDF5 loader returns flattened channels in ``[C, latitude, longitude]``;
|
| 141 |
+
official NeuralGCM expects named variables with pressure ``level`` and
|
| 142 |
+
explicit latitude/longitude coordinates. Spatial interpolation to the
|
| 143 |
+
configured native grid is performed before the model API sees the data.
|
| 144 |
+
"""
|
| 145 |
+
import numpy as np
|
| 146 |
+
import xarray as xr
|
| 147 |
+
|
| 148 |
+
invar = sample[0]
|
| 149 |
+
if hasattr(invar, "detach"):
|
| 150 |
+
invar = invar.detach().cpu().numpy()
|
| 151 |
+
channels = channel_order(config)
|
| 152 |
+
levels = pressure_levels(config)
|
| 153 |
+
height, width = invar.shape[-2:]
|
| 154 |
+
lat = np.linspace(90.0, -90.0, height, dtype=np.float32)
|
| 155 |
+
lon = np.linspace(0.0, 360.0, width, endpoint=False, dtype=np.float32)
|
| 156 |
+
# ERA5Dataset stores (latitude, longitude), while NeuralGCM's xarray API
|
| 157 |
+
# expects (longitude, latitude) for horizontal fields.
|
| 158 |
+
dataset = xr.Dataset(coords={"latitude": lat, "longitude": lon, "time": [np.datetime64(timestamp)]})
|
| 159 |
+
grouped: dict[str, list[tuple[int, Any]]] = {}
|
| 160 |
+
for index, name in enumerate(channels):
|
| 161 |
+
if name in {"sea_ice_cover", "sea_surface_temperature"}:
|
| 162 |
+
values = xr.DataArray(invar[index].T, dims=("longitude", "latitude"), coords={"latitude": lat, "longitude": lon})
|
| 163 |
+
else:
|
| 164 |
+
base, _, suffix = name.rpartition("_")
|
| 165 |
+
if not suffix.isdigit() or base not in config["model"]["input_variables"] + config["model"].get("optional_input_variables", []):
|
| 166 |
+
continue
|
| 167 |
+
values = xr.DataArray(invar[index].T, dims=("longitude", "latitude"), coords={"latitude": lat, "longitude": lon}).expand_dims(level=[int(suffix)])
|
| 168 |
+
values = values.expand_dims(time=[np.datetime64(timestamp)])
|
| 169 |
+
grouped.setdefault(base, []).append((int(suffix), values))
|
| 170 |
+
continue
|
| 171 |
+
values = values.expand_dims(time=[np.datetime64(timestamp)])
|
| 172 |
+
dataset[name] = values
|
| 173 |
+
for base, entries in grouped.items():
|
| 174 |
+
entries.sort(key=lambda item: levels.index(item[0]) if item[0] in levels else item[0])
|
| 175 |
+
merged = xr.concat([value for _, value in entries], dim="level")
|
| 176 |
+
dataset[base] = merged.transpose("time", "level", "longitude", "latitude") if "time" in merged.dims else merged.transpose("level", "longitude", "latitude")
|
| 177 |
+
return dataset
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def era5_frames_to_xarray(
|
| 181 |
+
frames: Any,
|
| 182 |
+
config: dict[str, Any],
|
| 183 |
+
*,
|
| 184 |
+
start_time: Any,
|
| 185 |
+
):
|
| 186 |
+
"""Vectorized ERA5 ``(T,C,H,W)`` to NeuralGCM xarray conversion.
|
| 187 |
+
|
| 188 |
+
This is equivalent to concatenating ``era5_sample_to_xarray`` outputs, but
|
| 189 |
+
constructs every multi-level variable in one operation. It avoids hundreds
|
| 190 |
+
of small DataArray allocations per training window.
|
| 191 |
+
"""
|
| 192 |
+
import numpy as np
|
| 193 |
+
import xarray as xr
|
| 194 |
+
|
| 195 |
+
frames = as_time_major_frames(frames, name="ERA5 trajectory")
|
| 196 |
+
channels = channel_order(config)
|
| 197 |
+
if frames.shape[1] != len(channels):
|
| 198 |
+
raise ValueError(
|
| 199 |
+
f"ERA5 trajectory has {frames.shape[1]} channels, expected "
|
| 200 |
+
f"{len(channels)}"
|
| 201 |
+
)
|
| 202 |
+
n_time, _, height, width = frames.shape
|
| 203 |
+
lat = np.linspace(90.0, -90.0, height, dtype=np.float32)
|
| 204 |
+
lon = np.linspace(0.0, 360.0, width, endpoint=False, dtype=np.float32)
|
| 205 |
+
step_hours = int(config["data"].get("time_step_hours", 6))
|
| 206 |
+
times = np.datetime64(start_time) + np.arange(n_time) * np.timedelta64(step_hours, "h")
|
| 207 |
+
coords = {"time": times, "latitude": lat, "longitude": lon}
|
| 208 |
+
dataset = xr.Dataset(coords=coords)
|
| 209 |
+
|
| 210 |
+
level_indices: dict[str, list[tuple[int, int]]] = {}
|
| 211 |
+
allowed = set(config["model"]["input_variables"])
|
| 212 |
+
allowed.update(config["model"].get("optional_input_variables", []))
|
| 213 |
+
for channel_index, name in enumerate(channels):
|
| 214 |
+
if name in {"sea_ice_cover", "sea_surface_temperature"}:
|
| 215 |
+
dataset[name] = (
|
| 216 |
+
("time", "longitude", "latitude"),
|
| 217 |
+
np.asarray(frames[:, channel_index]).transpose(0, 2, 1),
|
| 218 |
+
)
|
| 219 |
+
continue
|
| 220 |
+
base, _, suffix = name.rpartition("_")
|
| 221 |
+
if suffix.isdigit() and base in allowed:
|
| 222 |
+
level_indices.setdefault(base, []).append((int(suffix), channel_index))
|
| 223 |
+
|
| 224 |
+
configured_levels = pressure_levels(config)
|
| 225 |
+
for base, entries in level_indices.items():
|
| 226 |
+
entries.sort(
|
| 227 |
+
key=lambda item: configured_levels.index(item[0])
|
| 228 |
+
if item[0] in configured_levels
|
| 229 |
+
else item[0]
|
| 230 |
+
)
|
| 231 |
+
indices = [index for _, index in entries]
|
| 232 |
+
levels = [level for level, _ in entries]
|
| 233 |
+
values = np.asarray(frames[:, indices]).transpose(0, 1, 3, 2)
|
| 234 |
+
dataset[base] = (
|
| 235 |
+
("time", "level", "longitude", "latitude"),
|
| 236 |
+
values,
|
| 237 |
+
)
|
| 238 |
+
dataset = dataset.assign_coords(level=np.asarray(levels))
|
| 239 |
+
return dataset
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _target_grid(mode: str):
|
| 243 |
+
from dinosaur import spherical_harmonic
|
| 244 |
+
|
| 245 |
+
targets = {
|
| 246 |
+
"weather_forecast": spherical_harmonic.Grid.TL255,
|
| 247 |
+
"climate_scale": spherical_harmonic.Grid.TL127,
|
| 248 |
+
"forecast_2_8_deg": spherical_harmonic.Grid.TL63,
|
| 249 |
+
"stochastic_1_4_deg": spherical_harmonic.Grid.TL127,
|
| 250 |
+
}
|
| 251 |
+
try:
|
| 252 |
+
return targets[mode]()
|
| 253 |
+
except KeyError as exc:
|
| 254 |
+
raise ValueError(f"Unknown model mode {mode!r}") from exc
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@lru_cache(maxsize=16)
|
| 258 |
+
def _profile_regridder(
|
| 259 |
+
height: int,
|
| 260 |
+
width: int,
|
| 261 |
+
mode: str,
|
| 262 |
+
latitude_spacing: str,
|
| 263 |
+
longitude_offset: float,
|
| 264 |
+
):
|
| 265 |
+
"""Construct and cache the profile's conservative regridder."""
|
| 266 |
+
from dinosaur import horizontal_interpolation, spherical_harmonic
|
| 267 |
+
|
| 268 |
+
source_grid = spherical_harmonic.Grid(
|
| 269 |
+
latitude_nodes=height,
|
| 270 |
+
longitude_nodes=width,
|
| 271 |
+
latitude_spacing=latitude_spacing,
|
| 272 |
+
longitude_offset=longitude_offset,
|
| 273 |
+
)
|
| 274 |
+
return horizontal_interpolation.ConservativeRegridder(
|
| 275 |
+
source_grid, _target_grid(mode), skipna=True
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def regrid_for_neuralgcm(dataset: Any, official_model: Any):
|
| 280 |
+
"""Conservatively regrid ERA5 fields to the checkpoint's Gaussian grid."""
|
| 281 |
+
from dinosaur import horizontal_interpolation
|
| 282 |
+
from dinosaur import spherical_harmonic
|
| 283 |
+
from dinosaur import xarray_utils
|
| 284 |
+
|
| 285 |
+
source_grid = spherical_harmonic.Grid(
|
| 286 |
+
latitude_nodes=dataset.sizes["latitude"],
|
| 287 |
+
longitude_nodes=dataset.sizes["longitude"],
|
| 288 |
+
latitude_spacing=xarray_utils.infer_latitude_spacing(dataset.latitude),
|
| 289 |
+
longitude_offset=xarray_utils.infer_longitude_offset(dataset.longitude),
|
| 290 |
+
)
|
| 291 |
+
regridder = horizontal_interpolation.ConservativeRegridder(
|
| 292 |
+
source_grid, official_model.data_coords.horizontal, skipna=True
|
| 293 |
+
)
|
| 294 |
+
regridded = xarray_utils.regrid(dataset, regridder)
|
| 295 |
+
return xarray_utils.fill_nan_with_nearest(regridded)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def regrid_for_profile(dataset: Any, mode: str):
|
| 299 |
+
"""Regrid to the Gaussian data grid selected by an official Gin profile."""
|
| 300 |
+
from dinosaur import xarray_utils
|
| 301 |
+
|
| 302 |
+
regridder = _profile_regridder(
|
| 303 |
+
dataset.sizes["latitude"],
|
| 304 |
+
dataset.sizes["longitude"],
|
| 305 |
+
mode,
|
| 306 |
+
xarray_utils.infer_latitude_spacing(dataset.latitude),
|
| 307 |
+
float(xarray_utils.infer_longitude_offset(dataset.longitude)),
|
| 308 |
+
)
|
| 309 |
+
return xarray_utils.fill_nan_with_nearest(xarray_utils.regrid(dataset, regridder))
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
@lru_cache(maxsize=16)
|
| 313 |
+
def _load_static_features(
|
| 314 |
+
path_text: str,
|
| 315 |
+
mode: str | None,
|
| 316 |
+
target_height: int,
|
| 317 |
+
target_width: int,
|
| 318 |
+
):
|
| 319 |
+
"""Load and, only when necessary, regrid a reusable static dataset."""
|
| 320 |
+
import xarray as xr
|
| 321 |
+
|
| 322 |
+
with xr.open_dataset(path_text) as source:
|
| 323 |
+
static = source[["geopotential_at_surface", "land_sea_mask"]].load()
|
| 324 |
+
source_shape = (
|
| 325 |
+
static.sizes.get("latitude"),
|
| 326 |
+
static.sizes.get("longitude"),
|
| 327 |
+
)
|
| 328 |
+
if source_shape != (target_height, target_width):
|
| 329 |
+
if mode is None:
|
| 330 |
+
return None
|
| 331 |
+
static = regrid_for_profile(static, mode)
|
| 332 |
+
if (
|
| 333 |
+
static.sizes.get("latitude"),
|
| 334 |
+
static.sizes.get("longitude"),
|
| 335 |
+
) != (target_height, target_width):
|
| 336 |
+
return None
|
| 337 |
+
return static
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def add_static_features(
|
| 341 |
+
dataset: Any,
|
| 342 |
+
config: dict[str, Any] | None = None,
|
| 343 |
+
*,
|
| 344 |
+
mode: str | None = None,
|
| 345 |
+
prefer_profile: bool = True,
|
| 346 |
+
):
|
| 347 |
+
"""Attach official profile static fields, with a synthetic fallback.
|
| 348 |
+
|
| 349 |
+
Callers attach fields after regridding the dynamic ERA5 trajectory. This
|
| 350 |
+
preserves the exact Gaussian-grid topography and land/sea mask bundled in
|
| 351 |
+
the official checkpoints. ``data.static_file`` remains a source-grid
|
| 352 |
+
fallback for installations that do not carry the released checkpoints.
|
| 353 |
+
"""
|
| 354 |
+
import numpy as np
|
| 355 |
+
|
| 356 |
+
required = ("geopotential_at_surface", "land_sea_mask")
|
| 357 |
+
if config is not None and not set(required).issubset(dataset):
|
| 358 |
+
data_cfg = config.get("data", {})
|
| 359 |
+
profile_path = (
|
| 360 |
+
data_cfg.get("static_files", {}).get(mode) if mode else None
|
| 361 |
+
)
|
| 362 |
+
fallback_path = data_cfg.get("static_file")
|
| 363 |
+
candidates = []
|
| 364 |
+
if not prefer_profile and fallback_path:
|
| 365 |
+
candidates.append(fallback_path)
|
| 366 |
+
if mode:
|
| 367 |
+
if profile_path:
|
| 368 |
+
candidates.append(profile_path)
|
| 369 |
+
if prefer_profile and fallback_path:
|
| 370 |
+
candidates.append(fallback_path)
|
| 371 |
+
for value in candidates:
|
| 372 |
+
static_path = resolve_path(value)
|
| 373 |
+
if not static_path.exists():
|
| 374 |
+
continue
|
| 375 |
+
static = _load_static_features(
|
| 376 |
+
str(static_path.resolve()),
|
| 377 |
+
mode,
|
| 378 |
+
int(dataset.sizes["latitude"]),
|
| 379 |
+
int(dataset.sizes["longitude"]),
|
| 380 |
+
)
|
| 381 |
+
if static is None:
|
| 382 |
+
continue
|
| 383 |
+
for name in required:
|
| 384 |
+
if name not in dataset:
|
| 385 |
+
# Both arrays are on the same profile Gaussian grid. Assign
|
| 386 |
+
# by position rather than xarray label alignment: checkpoint
|
| 387 |
+
# coordinates are float64 while regridded ERA5 coordinates
|
| 388 |
+
# can be float32, and exact-label alignment would inject NaN.
|
| 389 |
+
values = static[name].transpose("longitude", "latitude")
|
| 390 |
+
dataset[name] = (
|
| 391 |
+
("longitude", "latitude"),
|
| 392 |
+
np.asarray(values.values),
|
| 393 |
+
)
|
| 394 |
+
dataset[name].attrs.update(values.attrs)
|
| 395 |
+
dataset.attrs["static_features_source"] = str(static_path)
|
| 396 |
+
break
|
| 397 |
+
if "geopotential_at_surface" not in dataset:
|
| 398 |
+
dataset["geopotential_at_surface"] = (("longitude", "latitude"), np.zeros((dataset.sizes["longitude"], dataset.sizes["latitude"]), np.float32))
|
| 399 |
+
if "land_sea_mask" not in dataset:
|
| 400 |
+
dataset["land_sea_mask"] = (("longitude", "latitude"), np.zeros((dataset.sizes["longitude"], dataset.sizes["latitude"]), np.float32))
|
| 401 |
+
# Gin FloatDataFeatures parses units from these static fields exactly as in
|
| 402 |
+
# the official ERA5 pipeline.
|
| 403 |
+
dataset["geopotential_at_surface"].attrs.setdefault("units", "m**2 s**-2")
|
| 404 |
+
dataset["land_sea_mask"].attrs.setdefault("units", "dimensionless")
|
| 405 |
+
return dataset
|