yzt15806542928 commited on
Commit
f4a39ee
·
verified ·
1 Parent(s): 582758d

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -18
  2. README.md +204 -0
  3. conf/config.yaml +527 -0
  4. config.json +147 -0
  5. configuration.json +12 -0
  6. model/NeuralGCM.py +230 -0
  7. model/NeuralGCM_demo.py +60 -0
  8. model/data/era5_tl31_19590102T00.nc +3 -0
  9. model/data/tl63_stochastic_mini.pkl +3 -0
  10. model/legacy/api.py +601 -0
  11. model/legacy/correctors.py +177 -0
  12. model/legacy/decoders.py +749 -0
  13. model/legacy/diagnostics.py +420 -0
  14. model/legacy/embeddings.py +380 -0
  15. model/legacy/encoders.py +874 -0
  16. model/legacy/equations.py +402 -0
  17. model/legacy/features.py +867 -0
  18. model/legacy/filters.py +457 -0
  19. model/legacy/forcings.py +292 -0
  20. model/legacy/gin_utils.py +81 -0
  21. model/legacy/initializers.py +124 -0
  22. model/legacy/integrators.py +36 -0
  23. model/legacy/layers.py +443 -0
  24. model/legacy/mappings.py +206 -0
  25. model/legacy/model_builder.py +744 -0
  26. model/legacy/model_utils.py +527 -0
  27. model/legacy/optimization.py +179 -0
  28. model/legacy/orographies.py +131 -0
  29. model/legacy/parameterizations.py +171 -0
  30. model/legacy/perturbations.py +230 -0
  31. model/legacy/physics_specifications.py +115 -0
  32. model/legacy/steps.py +332 -0
  33. model/legacy/stochastic.py +1225 -0
  34. model/legacy/towers.py +206 -0
  35. model/legacy/transforms.py +738 -0
  36. model/reference_code/datasets.py +70 -0
  37. model/reference_code/experiment.py +1400 -0
  38. model/reference_code/linear_transforms.py +342 -0
  39. model/reference_code/metrics.py +696 -0
  40. model/reference_code/metrics_base.py +150 -0
  41. model/reference_code/metrics_util.py +453 -0
  42. model/reference_code/paper_configs/deterministic_0_7_deg.gin +2368 -0
  43. model/reference_code/paper_configs/deterministic_1_4_deg.gin +2371 -0
  44. model/reference_code/paper_configs/deterministic_2_8_deg.gin +0 -0
  45. model/reference_code/paper_configs/stochastic_1_4_deg.gin +0 -0
  46. model/reference_code/reader.py +624 -0
  47. model/reference_code/stochastic_losses.py +458 -0
  48. model/reference_code/train_utils.py +662 -0
  49. scripts/checkpoint_info.py +45 -0
  50. 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
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
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