yzt15806542928 commited on
Commit
bd3493c
·
verified ·
1 Parent(s): 7b38a1a

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 -0
  2. .gitignore +8 -0
  3. README.md +204 -0
  4. conf/config.yaml +14 -0
  5. configuration.json +1 -0
  6. model/__init__.py +1 -0
  7. model/aardvark_adapter.py +153 -0
  8. model/sample_dataset.py +76 -0
  9. official-src/LICENSE +121 -0
  10. official-src/README.md +71 -0
  11. official-src/aardvark/architectures.py +37 -0
  12. official-src/aardvark/data_shapes.py +31 -0
  13. official-src/aardvark/e2e_model.py +241 -0
  14. official-src/aardvark/e2e_train.py +242 -0
  15. official-src/aardvark/finetune.py +431 -0
  16. official-src/aardvark/generate_initial_condition_single.py +148 -0
  17. official-src/aardvark/loader.py +1825 -0
  18. official-src/aardvark/loader_utils_new.py +88 -0
  19. official-src/aardvark/loss_functions.py +166 -0
  20. official-src/aardvark/misc_downscaling_functionality.py +273 -0
  21. official-src/aardvark/models.py +390 -0
  22. official-src/aardvark/set_convs.py +101 -0
  23. official-src/aardvark/train_module.py +315 -0
  24. official-src/aardvark/trainer.py +602 -0
  25. official-src/aardvark/unet_wrap_padding.py +310 -0
  26. official-src/aardvark/utils.py +23 -0
  27. official-src/aardvark/vit.py +298 -0
  28. official-src/data/grid_lon_lat/era5_x_1.npy +3 -0
  29. official-src/data/grid_lon_lat/era5_y_1.npy +3 -0
  30. official-src/data/norm_factors/mean_4u_1.npy +3 -0
  31. official-src/data/norm_factors/mean_diff_4u_1.npy +3 -0
  32. official-src/data/norm_factors/mean_hadisd_tas.npy +3 -0
  33. official-src/data/norm_factors/mean_hadisd_ws.npy +3 -0
  34. official-src/data/norm_factors/std_4u_1.npy +3 -0
  35. official-src/data/norm_factors/std_diff_4u_1.npy +3 -0
  36. official-src/data/norm_factors/std_hadisd_tas.npy +3 -0
  37. official-src/data/norm_factors/std_hadisd_ws.npy +3 -0
  38. official-src/data/sample_data_final.pkl +3 -0
  39. official-src/environment.yml +328 -0
  40. official-src/notebooks/data_demo.ipynb +386 -0
  41. official-src/notebooks/e2e_finetune_demo.ipynb +0 -0
  42. official-src/notebooks/forecast_demo.ipynb +0 -0
  43. official-src/training/downscaling_run_pipeline.sh +23 -0
  44. official-src/training/finetune.sh +9 -0
  45. official-src/training/test/config.pkl +3 -0
  46. official-src/training/train_decoder.sh +43 -0
  47. official-src/training/train_e2e.sh +21 -0
  48. official-src/training/train_encoder.sh +16 -0
  49. official-src/training/train_processor.sh +17 -0
  50. scripts/inference.py +57 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* 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
 
 
 
 
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
36
+ weights/trained_model/decoder/tas/lt_1/epoch_18 filter=lfs diff=lfs merge=lfs -text
37
+ weights/trained_model/encoder/epoch_96 filter=lfs diff=lfs merge=lfs -text
38
+ weights/trained_model/processor/forecast_1/epoch_0 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pdf
4
+ result/
5
+ weight/*
6
+ !weight/.gitkeep
7
+ .pytest_cache/
8
+ .ipynb_checkpoints/
README.md ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ language:
4
+ - en
5
+ - zh
6
+ tags:
7
+ - OneScience
8
+ - Earth Science
9
+ - Weather Forecasting
10
+ - Medium-Range Weather Forecasting
11
+ frameworks: PyTorch
12
+ datasets:
13
+ - OneScience/Aardvark-Weather
14
+ ---
15
+
16
+ <p align="center">
17
+ <strong><span style="font-size: 30px;">Aardvark Weather</span></strong>
18
+ </p>
19
+
20
+ # Model Overview
21
+
22
+ Aardvark Weather is an end-to-end multimodal weather forecasting model that generates global gridded forecasts and station-level predictions through an observation encoder, a global forecast processor, and a station decoder.
23
+
24
+ Paper: *End-to-end data-driven weather prediction*
25
+
26
+ https://www.nature.com/articles/s41586-025-08897-0
27
+
28
+ # Model Description
29
+
30
+ This model package reuses the official code and weights to provide the following run pipeline:
31
+
32
+ ```text
33
+ Official Multimodal Sample
34
+ -> Encoder
35
+ -> Day-1 Processor
36
+ -> TAS Decoder
37
+ -> 1-day global forecast and station 2-meter temperature
38
+ ```
39
+
40
+ # Use Cases
41
+
42
+ | Scenario | Description |
43
+ | :---: | :--- |
44
+ | Official Model Verification | Inspect official samples, configuration, and checkpoints. |
45
+ | Global Weather Forecasting | Output a global 1.5° gridded state of 24 variables. |
46
+ | Station Temperature Forecasting | Output 2-meter temperature at 8,719 stations. |
47
+
48
+ # Usage
49
+
50
+ ## 1. OneCode
51
+
52
+ [Click to experience intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
53
+
54
+ ## 2. Manual Installation & Usage
55
+
56
+ **Hardware Requirements**
57
+
58
+ - Inference with the official weights requires an NVIDIA GPU.
59
+ - CPU can be used for resource and checkpoint inspection; running full inference on CPU is not recommended.
60
+
61
+ ### Download the Model Package
62
+
63
+ ```bash
64
+ hf download --model OneScience-Group/Aardvark-Weather --local-dir ./Aardvark-Weather
65
+ cd Aardvark-Weather
66
+ ```
67
+
68
+ ### Set Up the Runtime Environment
69
+
70
+ **DCU Environment**
71
+
72
+ ```bash
73
+ conda create -n onescience311 python=3.11 -y
74
+ conda activate onescience311
75
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
76
+ ```
77
+
78
+ **GPU Environment**
79
+
80
+ ```bash
81
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
82
+ conda activate onescience311
83
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
84
+ ```
85
+
86
+ If the environment lacks Aardvark dependencies, refer to `official-src/environment.yml` for additional installation. The current adaptation is compatible with the `Block` parameter of the newer `timm` package.
87
+
88
+ ### Data & Weights
89
+
90
+ The model package already includes the resources required for 1-day temperature inference:
91
+
92
+ ```text
93
+ weights/sample_data/sample_data_final.pkl
94
+ weights/trained_model/encoder/epoch_96
95
+ weights/trained_model/processor/forecast_1/epoch_0
96
+ weights/trained_model/decoder/tas/lt_1/epoch_18
97
+ official-src/data/grid_lon_lat/
98
+ official-src/data/norm_factors/
99
+ ```
100
+
101
+ To re-download from source:
102
+
103
+ ```text
104
+ Official Code: https://github.com/anna-allen/aardvark-weather-public
105
+ Official Weights: https://huggingface.co/datasets/av555/aardvark-weather
106
+ ```
107
+
108
+ ### Training
109
+
110
+ The training entry point provides a complete pipeline with epochs, validation, early stopping, learning rate scheduling, best/latest checkpointing, and resumption. By default, the Encoder and Day-1 Processor are frozen and only the TAS Decoder is trained:
111
+
112
+ ```bash
113
+ python scripts/train.py
114
+ ```
115
+
116
+ Paper-style end-to-end joint fine-tuning:
117
+
118
+ ```bash
119
+ python scripts/train.py --train-modules all --epochs 10 --train-steps 100
120
+ ```
121
+
122
+ Resuming training:
123
+
124
+ ```bash
125
+ python scripts/train.py --resume weight/training/last.pth
126
+ ```
127
+
128
+ The default configuration resides in `conf/config.yaml`. `--data` can point to an official-schema pickle file or a directory containing multiple `.pkl` files; multiple files are partitioned into train and validation sets in a deterministic manner. `--batch-size` concatenates multiple tasks along the existing batch dimension of the official task. When only the single official sample is included, training and validation reuse the same task — this allows end-to-end validation of the training software pipeline but does not constitute an independent validation set, nor can it provide the data diversity required to reproduce paper-level accuracy.
129
+
130
+ Training artifacts:
131
+
132
+ ```text
133
+ weight/training/best.pth
134
+ weight/training/last.pth
135
+ weight/training/history.json
136
+ weight/training/train.json
137
+ ```
138
+
139
+ ### Inference
140
+
141
+ ```bash
142
+ python scripts/inference.py
143
+ ```
144
+
145
+ Using weights obtained from training:
146
+
147
+ ```bash
148
+ python scripts/inference.py --checkpoint weight/training/best.pth
149
+ ```
150
+
151
+ Inference loads the official sample and the Day-1 `tas` weights by default. Results are saved to:
152
+
153
+ ```text
154
+ result/inference_one_day.json
155
+ result/prediction.pt
156
+ result/target.pt
157
+ ```
158
+
159
+ ### Result Inspection
160
+
161
+ ```bash
162
+ python scripts/result.py
163
+ ```
164
+
165
+ Verified output shapes:
166
+
167
+ ```text
168
+ initial_state: [1, 121, 240, 24]
169
+ global_forecast: [1, 121, 240, 24]
170
+ station_tas: [1, 8719]
171
+ ```
172
+
173
+ Current results constitute a connectivity/end-to-end verification and do not reproduce the paper's RMSE/MAE metrics.
174
+
175
+ The result script additionally produces `result/metrics.json` and `result/comparison.png`. The `normalized_mae` and `normalized_rmse` are computed in the normalized space of the official sample and should not be directly compared to the paper's physical-unit metrics.
176
+
177
+ ### Paper vs. Current Implementation I/O
178
+
179
+ | Item | Paper | Current Package |
180
+ | --- | --- | --- |
181
+ | Input | Multimodal satellite, station, ship, and radiosonde observations | Bundled official sample pickle with a field structure consistent with the official Encoder |
182
+ | Global State | `24 × 121 × 240`, 1.5° | Day-1 supported; output `[1,121,240,24]` |
183
+ | Station Output | 2 m temperature and 10 m wind, up to Day-10 | Day-1 TAS only; `[1,8719]` |
184
+ | Training | Staged pre-training followed by ~25,000 steps of end-to-end fine-tuning | Configurable full training loop; supports Decoder-only or full-model joint fine-tuning |
185
+ | Evaluation | Grid-point weighted RMSE and station MAE in physical units | MAE/RMSE in normalized sample space |
186
+
187
+ All commands should be run from the project root; `scripts/inference.py --root` converts to an absolute path. The official model internally depends on CUDA, so CPU is currently unavailable for inference. Training data must adhere to the official multimodal task dictionary schema; the current directory does not synthesize satellite or station observations — the bundled official sample serves as the default training-pipeline validator. The model package retains only official resources under `weights/` and does not carry local training artifacts under `weight/` or generated outputs under `result/`. Paper-level training still requires preparing observation data spanning the full date range and converting it into the same `.pkl` task contract.
188
+
189
+ ### Real Data
190
+
191
+ Using real dates requires preparing ASCAT, AMSU-A/B, HIRS, IASI, GridSat, HadISD, ICOADS, IGRA, ERA5, topography, climatology, and their corresponding normalization statistics.
192
+
193
+ # OneScience Official Information
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 & License
201
+
202
+ - Official Code: https://github.com/anna-allen/aardvark-weather-public
203
+ - This directory is an independent adaptation of the official Aardvark Weather model.
204
+ - Code, weights, and data are subject to their respective official licenses and data terms.
conf/config.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ training:
2
+ data: weights/sample_data/sample_data_final.pkl
3
+ output_dir: weight/training
4
+ epochs: 10
5
+ train_steps: 8
6
+ validation_steps: 2
7
+ validation_fraction: 0.2
8
+ batch_size: 1
9
+ learning_rate: 1.0e-5
10
+ weight_decay: 1.0e-6
11
+ gradient_clip: 1.0
12
+ patience: 3
13
+ seed: 2026
14
+ train_modules: decoder
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"weather-forecasting","implementation":"official-one-day-tas-adapter"}
model/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Aardvark-local adapters around the pinned official implementation."""
model/aardvark_adapter.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Thin, model-local wrapper for the official Aardvark Weather model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import importlib
7
+ import os
8
+ import pickle
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+
16
+
17
+ TOP_LEVEL_KEYS = {"assimilation", "forecast", "downscaling", "y_target"}
18
+ REQUIRED_ASSIMILATION_KEYS = {
19
+ "x_context_hadisd_current", "y_context_hadisd_current", "climatology_current",
20
+ "sat_x_current", "sat_current", "icoads_x_current", "icoads_current",
21
+ "igra_x_current", "igra_current", "amsua_current", "amsua_x_current",
22
+ "amsub_current", "amsub_x_current", "iasi_current", "iasi_x_current",
23
+ "ascat_current", "ascat_x_current", "hirs_current", "hirs_x_current",
24
+ "y_target_current", "era5_x_current",
25
+ "era5_elev_current", "era5_lonlat_current", "aux_time_current", "lt",
26
+ "y_target",
27
+ }
28
+ REQUIRED_FORECAST_KEYS = {"y_context", "y_target", "lt"}
29
+ REQUIRED_DOWNSCALING_KEYS = {
30
+ "x_target", "alt_target", "y_target", "y_context", "x_context", "aux_time", "lt",
31
+ }
32
+
33
+
34
+ def sha256(path: Path) -> str:
35
+ digest = hashlib.sha256()
36
+ with path.open("rb") as handle:
37
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
38
+ digest.update(chunk)
39
+ return digest.hexdigest()
40
+
41
+
42
+ def validate_sample(sample_path: Path) -> dict[str, Any]:
43
+ with sample_path.open("rb") as handle:
44
+ sample = pickle.load(handle)
45
+ if not isinstance(sample, dict) or set(sample) != TOP_LEVEL_KEYS:
46
+ raise ValueError(f"sample top-level keys mismatch: {list(sample) if isinstance(sample, dict) else type(sample)}")
47
+ expected = {
48
+ "assimilation": REQUIRED_ASSIMILATION_KEYS,
49
+ "forecast": REQUIRED_FORECAST_KEYS,
50
+ "downscaling": REQUIRED_DOWNSCALING_KEYS,
51
+ }
52
+ for name, keys in expected.items():
53
+ if not isinstance(sample[name], dict) or set(sample[name]) != keys:
54
+ raise ValueError(f"sample {name} keys mismatch: {list(sample[name])}")
55
+ if not isinstance(sample["y_target"], torch.Tensor) or sample["y_target"].ndim != 2:
56
+ raise ValueError("sample y_target must be a rank-2 torch.Tensor")
57
+ return {
58
+ "top_level_keys": sorted(sample),
59
+ "nested_keys": {name: sorted(value) for name, value in expected.items()},
60
+ "y_target_shape": list(sample["y_target"].shape),
61
+ "nan_counts": {
62
+ name: int(value.isnan().sum())
63
+ for name, value in sample["downscaling"].items()
64
+ if isinstance(value, torch.Tensor) and value.is_floating_point()
65
+ },
66
+ }
67
+
68
+
69
+ def validate_checkpoint(path: Path) -> dict[str, Any]:
70
+ checkpoint = torch.load(path, map_location="cpu")
71
+ if not isinstance(checkpoint, dict) or "model_state_dict" not in checkpoint:
72
+ raise ValueError(f"checkpoint contract mismatch: {path}")
73
+ state = checkpoint["model_state_dict"]
74
+ if not isinstance(state, dict) or not state:
75
+ raise ValueError(f"empty model_state_dict: {path}")
76
+ return {"path": str(path), "key_count": len(state), "has_model_state_dict": True}
77
+
78
+
79
+ def load_sample(sample_path: Path) -> dict[str, Any]:
80
+ with sample_path.open("rb") as handle:
81
+ return pickle.load(handle)
82
+
83
+
84
+ def build_one_day_model(weights_root: Path, official_root: Path, device: str):
85
+ encoder = weights_root / "trained_model/encoder"
86
+ processor = weights_root / "trained_model/processor"
87
+ decoder = weights_root / "trained_model/decoder/tas"
88
+ sys.path.insert(0, str(official_root / "aardvark"))
89
+ _install_timm_compatibility()
90
+ official_e2e = importlib.import_module("e2e_model")
91
+ caller_dir = Path.cwd()
92
+ try:
93
+ os.chdir(official_root / "aardvark")
94
+ model = official_e2e.ConvCNPWeatherE2E(
95
+ device=device,
96
+ lead_time=1,
97
+ se_model_path=str(encoder),
98
+ forecast_model_path=str(processor),
99
+ sf_model_path=str(decoder) + "/",
100
+ return_gridded=True,
101
+ aux_data_path=str(official_root / "data") + "/",
102
+ )
103
+ finally:
104
+ os.chdir(caller_dir)
105
+ return model
106
+
107
+
108
+ def run_one_day(sample_path: Path, weights_root: Path, official_root: Path, device: str) -> dict[str, Any]:
109
+ sample_report = validate_sample(sample_path)
110
+ encoder = weights_root / "trained_model/encoder"
111
+ processor = weights_root / "trained_model/processor"
112
+ decoder = weights_root / "trained_model/decoder/tas"
113
+ checkpoint_report = [
114
+ validate_checkpoint(encoder / "epoch_96"),
115
+ validate_checkpoint(processor / "forecast_1/epoch_0"),
116
+ validate_checkpoint(decoder / "lt_1/epoch_18"),
117
+ ]
118
+ sample = load_sample(sample_path)
119
+ model = build_one_day_model(weights_root, official_root, device)
120
+ model.eval()
121
+ with torch.inference_mode():
122
+ station, global_forecast, initial_state = model(sample)
123
+ for name, tensor in (("station_tas", station), ("global_forecast", global_forecast), ("initial_state", initial_state)):
124
+ if not isinstance(tensor, torch.Tensor) or not bool(torch.isfinite(tensor).all()):
125
+ raise ValueError(f"{name} contains non-finite values")
126
+ return {
127
+ "sample": sample_report,
128
+ "checkpoints": checkpoint_report,
129
+ "device": device,
130
+ "lead_time_days": 1,
131
+ "station_tas_shape": list(station.shape),
132
+ "global_forecast_shape": list(global_forecast.shape),
133
+ "initial_state_shape": list(initial_state.shape),
134
+ "finite_outputs": True,
135
+ }
136
+
137
+
138
+ def _install_timm_compatibility() -> None:
139
+ """Bridge the old timm 0.6 Block constructor used by the official code."""
140
+ import timm.models.vision_transformer as vision_transformer
141
+
142
+ original = vision_transformer.Block
143
+ if getattr(original, "_aardvark_compat", False):
144
+ return
145
+
146
+ class AardvarkBlock(original):
147
+ _aardvark_compat = True
148
+
149
+ def __init__(self, *args: Any, drop: float = 0.0, **kwargs: Any) -> None:
150
+ kwargs.setdefault("proj_drop", drop)
151
+ super().__init__(*args, **kwargs)
152
+
153
+ vision_transformer.Block = AardvarkBlock
model/sample_dataset.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Datasets for training Aardvark from official-schema pickle tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import pickle
7
+ import random
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import torch
13
+ from torch.utils.data import Dataset
14
+
15
+ from .aardvark_adapter import validate_sample
16
+
17
+
18
+ def discover_samples(path: Path) -> list[Path]:
19
+ path = path.resolve()
20
+ samples = sorted(path.glob("*.pkl")) if path.is_dir() else [path]
21
+ if not samples or any(not sample.is_file() for sample in samples):
22
+ raise FileNotFoundError(f"No Aardvark sample pickle found at {path}")
23
+ for sample in samples:
24
+ validate_sample(sample)
25
+ return samples
26
+
27
+
28
+ def split_samples(samples: list[Path], validation_fraction: float, seed: int) -> tuple[list[Path], list[Path]]:
29
+ if not 0.0 < validation_fraction < 1.0:
30
+ raise ValueError("validation_fraction must be between 0 and 1")
31
+ if len(samples) == 1:
32
+ return samples, samples
33
+ shuffled = samples.copy()
34
+ random.Random(seed).shuffle(shuffled)
35
+ validation_count = max(1, round(len(shuffled) * validation_fraction))
36
+ validation_count = min(validation_count, len(shuffled) - 1)
37
+ return shuffled[validation_count:], shuffled[:validation_count]
38
+
39
+
40
+ class AardvarkTaskDataset(Dataset):
41
+ """Repeat one or more already-batched official tasks for a fixed number of steps."""
42
+
43
+ def __init__(self, samples: list[Path], steps: int) -> None:
44
+ if steps < 1:
45
+ raise ValueError("steps must be at least 1")
46
+ self.tasks = [self._load(path) for path in samples]
47
+ self.steps = steps
48
+
49
+ @staticmethod
50
+ def _load(path: Path) -> dict[str, Any]:
51
+ with path.open("rb") as handle:
52
+ return pickle.load(handle)
53
+
54
+ def __len__(self) -> int:
55
+ return self.steps
56
+
57
+ def __getitem__(self, index: int) -> dict[str, Any]:
58
+ return copy.deepcopy(self.tasks[index % len(self.tasks)])
59
+
60
+
61
+ def collate_tasks(items: list[Any]) -> Any:
62
+ """Concatenate the batch dimension already present in official task tensors."""
63
+ first = items[0]
64
+ if isinstance(first, torch.Tensor):
65
+ return torch.cat(items, dim=0)
66
+ if isinstance(first, np.ndarray):
67
+ return np.concatenate(items, axis=0)
68
+ if isinstance(first, dict):
69
+ return {key: collate_tasks([item[key] for item in items]) for key in first}
70
+ if isinstance(first, tuple):
71
+ return tuple(collate_tasks(list(values)) for values in zip(*items))
72
+ if isinstance(first, list):
73
+ return [collate_tasks(list(values)) for values in zip(*items)]
74
+ if all(item == first for item in items):
75
+ return first
76
+ raise TypeError(f"Cannot collate Aardvark values of type {type(first).__name__}")
official-src/LICENSE ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
official-src/README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aardvark Weather
2
+
3
+ This repo contains code and weights used to run the Aardvark Weather model (https://www.nature.com/articles/s41586-025-08897-0). We provide sample data, model weights and code to demonstrate how the trained model is run to produce forecasts.
4
+
5
+ ## Model weights
6
+ Weights for the trained model presented in the paper are provided at `https://huggingface.co/datasets/av555/aardvark-weather/tree/main/trained_model`.
7
+ Within this folder there are weights for the encoder, decoder and processor and end to end finetuned weights for one day lead time.
8
+ For the decoder and end-to-end, we provide weights for both temperature and wind speed.
9
+
10
+ ## Data
11
+ Sample data is provided in `data/sample_data_final.pkl`. In addition we provide normalisation factors used to generate plots of predictions. For a visualisation of what is included in each timeslice of data passed to the model, see notebooks/data_demo.ipynb. For those interested in training their own end-to-end models, we have additionally prepared a machine learning ready dataset for the data sources utilised in the paper available at https://huggingface.co/datasets/av555/aardvark-weather.
12
+
13
+ ## Forecast notebooks
14
+ Notebooks demonstrating producing a forecast using the trained models are included in the `notebooks/` folder.
15
+ Aardvark produces multiple modalities of forecasts.
16
+ The notebook `forecast_demo.ipynb` demonstrates loading the complete Aardvark Weather model, generating predictions from the sample data and provides visualisations of the output global gridded and station forecasts.
17
+ The notebook `e2e_finetune_demo.ipynb` provides a demonstration of generating optimised station forecasts from the sample data using the end to end finetuned model.
18
+
19
+ ## Training
20
+ Aardvark weather is trained in multiple stages.
21
+ Scripts to train the model are included in `training/`.
22
+ Training of the three main modules is handled in `train_module.py`, processor finetuning in `finetune.py` and end-to-end finetuning in `e2e_train.py`.
23
+
24
+ __Please note:__ the commands and related scripts under the "Encoder," "Processor," and "Decoder" sections below cannot be executed as they depend on local data loading pipelines, setup for the specific training compute infrastructure. The purpose of these training and finetuning commands is to illustrate the salient points of the training process for the purposes of transperancy and for those interested in the details of the training process and not to provide an executable version.
25
+
26
+ ### Encoder
27
+ To train the encoder, run
28
+ ```
29
+ bash train_encoder.sh
30
+ ```
31
+
32
+ ### Processor
33
+ To pre-train the processor, we use the code in
34
+ ```
35
+ bash train_processor.sh
36
+ ```
37
+
38
+ To finetune the processor, we use the code in
39
+ ```
40
+ bash finetune.sh
41
+ ```
42
+
43
+ ### Decoder
44
+ To train the decoder, we use the code in
45
+ ```
46
+ bash train_decoder.sh
47
+ ```
48
+
49
+ ### End-to-end
50
+ To tune the model end-to-end, we use the code in
51
+ ```
52
+ bash train_e2e.sh
53
+ ```
54
+ ## FAQ
55
+
56
+ #### Can I access the data the model was trained on?
57
+ We provide a dataset with observational data from 2007-2019 at 24 hour resolution at https://huggingface.co/datasets/av555/aardvark-weather. We hope that this will allow others to develop their own end-to-end weather models and explore the vast design space of ML architectures for this task.
58
+
59
+ #### Can I run the model in real time?
60
+ Unfortunately the datasets used in this initial prototype are not available in real time. We are however in the process of building a fully operational system, Aardvark 2.0. Updates to this will be provided here.
61
+
62
+ #### This model is at lower resolution than other AI models. Do you have any plans to improve on this?
63
+ We are currently working on a new version of our model which runs at 0.25 degrees for a wider range of pressure levels.
64
+
65
+ #### Will further versions of the model and dataset be provided?
66
+
67
+ Yes, this project is ongoing and we aim to continue updating this dataset and model. Updates currently in progress include the development of a fully operational system with real time data feed, extension of current historical dataset to 2025 and inclusion of further instruments. If you would like to receive updates as new products become available please email av555@cam.ac.uk with subject line 'Aardvark updates'.
68
+
69
+ #### Can this dataset be used commercially?
70
+
71
+ The dataset is released under a non-commercial no-derivatives license. This is due to the lisencing inherited from the source datasets.
official-src/aardvark/architectures.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from utils import *
5
+
6
+
7
+ class MLP(nn.Module):
8
+ """
9
+ Multi-layer perceptron
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ in_channels,
15
+ out_channels,
16
+ h_channels=64,
17
+ h_layers=4,
18
+ ):
19
+
20
+ super().__init__()
21
+
22
+ def hidden_block(h_channels):
23
+ h = nn.Sequential(
24
+ nn.Linear(h_channels, h_channels),
25
+ nn.ReLU(),
26
+ )
27
+ return h
28
+
29
+ self.mlp = nn.Sequential(
30
+ nn.Linear(in_channels, h_channels),
31
+ nn.ReLU(),
32
+ *[hidden_block(h_channels) for _ in range(h_layers)],
33
+ nn.Linear(h_channels, out_channels)
34
+ )
35
+
36
+ def forward(self, x):
37
+ return self.mlp(x)
official-src/aardvark/data_shapes.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data is stored in memmaps for access speed. Here we give the shapes required to load these files
3
+ """
4
+
5
+ CLIMATOLOGY_SHAPE = (4, 366, 24, 240, 121)
6
+
7
+ ICOADS_Y_SHAPE = (33601, 5, 12000)
8
+ ICOADS_X_SHAPE = (33601, 2, 12000)
9
+
10
+ IGRA_Y_SHAPE = (33604, 24, 1375)
11
+ IGRA_X_SHAPE = (1375, 2)
12
+
13
+ AMSUA_Y_SHAPE = (21916, 180, 360, 13)
14
+ AMSUB_Y_SHAPE = (21916, 360, 181, 12)
15
+ ASCAT_Y_SHAPE = (21913, 360, 181, 17)
16
+ HIRS_Y_SHAPE = (21913, 360, 181, 26)
17
+ GRIDSAT_Y_SHAPE = (48211, 2, 514, 200)
18
+ IASI_Y_SHAPE = (23373, 360, 181, 52)
19
+
20
+
21
+ def get_hadisd_shape(mode):
22
+ """
23
+ Return the shape of the HadISD array depending on variable
24
+ """
25
+
26
+ if mode != "train":
27
+ dim_1 = 415
28
+ else:
29
+ var_dict = {"tas": 8719, "tds": 8617, "psl": 8016, "u": 8721, "v": 8721}
30
+ dim_1 = var_dict[var]
31
+ return (106652, dim_1)
official-src/aardvark/e2e_model.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import numpy as np
6
+
7
+ from models import ConvCNPWeather
8
+ from misc_downscaling_functionality import ConvCNPWeatherOnToOff
9
+
10
+
11
+ class ConvCNPWeatherE2E(nn.Module):
12
+ """
13
+ Complete Aardvark weather model. This chains together the trained encoder,
14
+ processor and decoder modules to create complete forecasts. It can be finetuned
15
+ end to end to optimise predictions for a specific variable and location.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ device,
21
+ lead_time,
22
+ se_model_path,
23
+ forecast_model_path,
24
+ sf_model_path,
25
+ return_gridded=False,
26
+ aux_data_path=None,
27
+ ):
28
+
29
+ super().__init__()
30
+
31
+ self.device = device
32
+ self.lead_time = lead_time
33
+ self.return_gridded = return_gridded
34
+
35
+ # Load encoder
36
+ self.se_model = self.load_se_model(se_model_path)
37
+
38
+ # Load processor
39
+ self.forecast_model = nn.ModuleList(
40
+ [
41
+ self.load_forecast_model(forecast_model_path, l + 1)
42
+ for l in range(lead_time)
43
+ ]
44
+ )
45
+
46
+ # Load decoder
47
+ self.sf_model = self.load_sf_model(sf_model_path, lead_time)
48
+ self.decoder = None
49
+
50
+ # Setup normalisation factors
51
+ self.forecast_input_means = (
52
+ self.to_tensor(np.load(aux_data_path + "norm_factors/mean_4u_1.npy"))
53
+ .unsqueeze(0)
54
+ .unsqueeze(0)
55
+ .unsqueeze(0)
56
+ )
57
+ self.forecast_input_stds = (
58
+ self.to_tensor(np.load(aux_data_path + "norm_factors/std_4u_1.npy"))
59
+ .unsqueeze(0)
60
+ .unsqueeze(0)
61
+ .unsqueeze(0)
62
+ )
63
+
64
+ self.forecast_pred_diff_means = (
65
+ self.to_tensor(np.load(aux_data_path + "norm_factors/mean_diff_4u_1.npy"))
66
+ .unsqueeze(0)
67
+ .unsqueeze(0)
68
+ .unsqueeze(0)
69
+ )
70
+ self.forecast_pred_diff_stds = (
71
+ self.to_tensor(np.load(aux_data_path + "norm_factors/std_diff_4u_1.npy"))
72
+ .unsqueeze(0)
73
+ .unsqueeze(0)
74
+ .unsqueeze(0)
75
+ )
76
+
77
+ def to_tensor(self, arr):
78
+ return torch.from_numpy(arr).float().to(self.device)
79
+
80
+ def load_se_model(self, se_model_path):
81
+ """
82
+ Load the trained encoder module
83
+ """
84
+
85
+ with open(se_model_path + "/config.pkl", "rb") as handle:
86
+ forecast_config = pickle.load(handle)
87
+
88
+ model = ConvCNPWeather(
89
+ in_channels=forecast_config["in_channels"],
90
+ out_channels=forecast_config["out_channels"],
91
+ int_channels=forecast_config["int_channels"],
92
+ device="cuda",
93
+ res=forecast_config["res"],
94
+ gnp=bool(0),
95
+ decoder=forecast_config["decoder"],
96
+ mode=forecast_config["mode"],
97
+ film=bool(0),
98
+ )
99
+
100
+ best_epoch = np.argmin(np.load("{}/losses_0.npy".format(se_model_path)))
101
+ state_dict = torch.load(
102
+ "{}/epoch_{}".format(se_model_path, best_epoch),
103
+ map_location="cuda",
104
+ )["model_state_dict"]
105
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
106
+ model.load_state_dict(state_dict)
107
+ model = model.to("cuda")
108
+ return model
109
+
110
+ def load_forecast_model(self, forecast_model_path, lead_time):
111
+ """
112
+ Load the trained processor module
113
+ """
114
+
115
+ with open(forecast_model_path + "/config.pkl", "rb") as handle:
116
+ forecast_config = pickle.load(handle)
117
+
118
+ model = ConvCNPWeather(
119
+ in_channels=forecast_config["in_channels"],
120
+ out_channels=forecast_config["out_channels"],
121
+ int_channels=forecast_config["int_channels"],
122
+ device="cuda",
123
+ res=forecast_config["res"],
124
+ gnp=bool(0),
125
+ decoder=forecast_config["decoder"],
126
+ mode=forecast_config["mode"],
127
+ film=False,
128
+ )
129
+ state_dict = torch.load(
130
+ f"{forecast_model_path}/forecast_{lead_time}/epoch_0",
131
+ map_location="cuda",
132
+ )["model_state_dict"]
133
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
134
+ model.load_state_dict(state_dict)
135
+ model = model.to("cuda")
136
+ return model
137
+
138
+ def load_sf_model(self, sf_model_path, lead_time):
139
+ """
140
+ Load the trained decoder module
141
+ """
142
+
143
+ with open(sf_model_path + "config.pkl", "rb") as handle:
144
+ config = pickle.load(handle)
145
+
146
+ model = ConvCNPWeatherOnToOff(
147
+ in_channels=config["in_channels"],
148
+ out_channels=config["out_channels"],
149
+ int_channels=config["int_channels"],
150
+ device="cuda",
151
+ res=config["res"],
152
+ decoder=config["decoder"],
153
+ mode=config["mode"],
154
+ film=False,
155
+ )
156
+
157
+ best_epoch = np.argmin(
158
+ np.load("{}/lt_{}/losses_0.npy".format(sf_model_path, lead_time))
159
+ )
160
+ full_state_dict = torch.load(
161
+ sf_model_path + f"/lt_{lead_time}/epoch_{best_epoch}", map_location="cuda"
162
+ )
163
+ state_dict = full_state_dict["model_state_dict"]
164
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
165
+ model.load_state_dict(state_dict)
166
+ model = model.to("cuda")
167
+ model.eval()
168
+
169
+ return model
170
+
171
+ def process_se_output(self, task, x):
172
+ """
173
+ Reshape and normalise encoder output for input to processor
174
+ """
175
+
176
+ task["forecast"]["y_context"][:, :24, ...] = x.permute(0, 3, 2, 1)
177
+ if self.return_gridded:
178
+ return task, x.permute(0, 3, 2, 1)
179
+ return task
180
+
181
+ def process_forecast_output(self, task, x, last=False):
182
+ """
183
+ Reshape and normalise processor output for input to decoder
184
+ """
185
+
186
+ base_context = task["forecast"]["y_context"][:, :-11, ...].permute(0, 2, 3, 1)
187
+
188
+ base_context = (
189
+ base_context * self.forecast_input_stds + self.forecast_input_means
190
+ ).permute(0, 3, 2, 1)
191
+
192
+ x = self.forecast_pred_diff_means + x * self.forecast_pred_diff_stds
193
+
194
+ unnorm_x = x + base_context.permute(0, 2, 3, 1)
195
+
196
+ forecast = unnorm_x
197
+
198
+ x = (unnorm_x - self.forecast_input_means) / self.forecast_input_stds
199
+
200
+ task["downscaling"]["y_context"][:, :24, ...] = x.permute(0, 3, 2, 1)
201
+ task["forecast"]["y_context"] = torch.cat(
202
+ [
203
+ x.permute(0, 3, 2, 1),
204
+ task["forecast"]["y_context"][:, 24:, ...],
205
+ ],
206
+ axis=1,
207
+ )
208
+ if self.return_gridded:
209
+ return task, forecast
210
+ return task
211
+
212
+ def forward(self, task, film_index=None):
213
+ """
214
+ Produce a forecast
215
+ """
216
+
217
+ # Generate initial state
218
+ x = self.se_model(task["assimilation"], film_index=None)
219
+ if self.return_gridded:
220
+ task, initial_state = self.process_se_output(task, x)
221
+ else:
222
+ task = self.process_se_output(task, x)
223
+
224
+ # Generate forecast
225
+ for lt in range(self.lead_time):
226
+ x = self.forecast_model[lt](task["forecast"], film_index=None)
227
+ if self.return_gridded:
228
+ task, forecast = self.process_forecast_output(task, x)
229
+ else:
230
+ task = self.process_forecast_output(task, x)
231
+
232
+ # Generate station forecast
233
+ x = self.sf_model(task["downscaling"], film_index=None)
234
+
235
+ if self.return_gridded:
236
+ initial_state = (
237
+ initial_state.permute(0, 3, 2, 1) * self.forecast_input_stds
238
+ + self.forecast_input_means
239
+ )
240
+ return x, forecast, initial_state
241
+ return x
official-src/aardvark/e2e_train.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pickle
4
+ import argparse
5
+
6
+ import torch
7
+ from torch.utils.data import DataLoader
8
+ import torch.distributed as dist
9
+ import torch.utils.data.distributed
10
+ import torch.multiprocessing as mp
11
+ from torch.utils.data.distributed import DistributedSampler
12
+ from torch.distributed import init_process_group, destroy_process_group
13
+
14
+ from misc_downscaling_functionality import DownscalingRmseLoss
15
+ from e2e_model import *
16
+ from loader import *
17
+ from models import *
18
+ from unet_wrap_padding import *
19
+ from trainer import DDPTrainerE2E
20
+ from loss_functions import WeightedRmseLoss, PressureWeightedRmseLoss, RmseLoss
21
+
22
+ torch.set_float32_matmul_precision("medium")
23
+
24
+
25
+ def ddp_setup(rank, world_size, master_port):
26
+ """
27
+ Setup DDP
28
+ """
29
+
30
+ os.environ["MASTER_ADDR"] = "localhost"
31
+ os.environ["MASTER_PORT"] = master_port
32
+ init_process_group(
33
+ backend="nccl",
34
+ rank=rank,
35
+ world_size=world_size,
36
+ )
37
+ torch.cuda.set_device(rank)
38
+
39
+
40
+ def start_date(name):
41
+ """
42
+ Set split start dates
43
+ """
44
+
45
+ if name == "train":
46
+ return "2007-01-02"
47
+ elif name == "val":
48
+ return "2019-01-01"
49
+ elif name == "test":
50
+ return "2018-01-01"
51
+ else:
52
+ raise Exception(f"Unrecognised split name {name}")
53
+
54
+
55
+ def end_date(name):
56
+ """
57
+ Set split end dates
58
+ """
59
+
60
+ if name == "train":
61
+ return "2017-12-31"
62
+ elif name == "val":
63
+ return "2019-11-01"
64
+ elif name == "test":
65
+ return "2018-12-21"
66
+ else:
67
+ raise Exception(f"Unrecognised split name {name}")
68
+
69
+
70
+ def main(rank, world_size, output_dir, args):
71
+ """
72
+ Run end to end training
73
+ """
74
+
75
+ master_port = args.master_port
76
+ lead_time = args.lead_time
77
+ era5_mode = args.era5_mode
78
+ ddp_setup(rank, world_size, master_port)
79
+
80
+ # Setup loss function
81
+ if args.loss == "lw_rmse":
82
+ lf = WeightedRmseLoss(
83
+ args.res,
84
+ start_ind=0,
85
+ end_ind=24,
86
+ weight_per_variable=False,
87
+ )
88
+ elif args.loss == "lw_rmse_pressure_weighted":
89
+ lf = PressureWeightedRmseLoss(args.res, era5_mode)
90
+ elif args.loss == "rmse":
91
+ lf = RmseLoss()
92
+ elif args.loss == "downscaling_rmse":
93
+ lf = DownscalingRmseLoss()
94
+
95
+ # Instantiate model
96
+ model = ConvCNPWeatherE2E(
97
+ device="cuda",
98
+ lead_time=lead_time,
99
+ se_model_path=args.se_model_path,
100
+ forecast_model_path=args.forecast_model_path,
101
+ sf_model_path=args.sf_model_path,
102
+ )
103
+ dist.barrier()
104
+
105
+ # Instantiate loaders
106
+ train_dataset = WeatherDatasetE2E(
107
+ device="cuda",
108
+ hadisd_mode="train",
109
+ start_date="2007-01-02",
110
+ end_date="2017-12-31",
111
+ lead_time=lead_time,
112
+ era5_mode="4u",
113
+ mode="train",
114
+ res=args.res,
115
+ var_start=0,
116
+ var_end=24,
117
+ diff=bool(0),
118
+ two_frames=bool(0),
119
+ region=args.region,
120
+ hadisd_var=args.var,
121
+ max_steps_per_epoch=args.max_steps_per_epoch,
122
+ )
123
+
124
+ val_dataset = WeatherDatasetE2E(
125
+ device="cuda",
126
+ hadisd_mode="train",
127
+ start_date="2019-01-01",
128
+ end_date="2019-12-21",
129
+ lead_time=lead_time,
130
+ era5_mode="4u",
131
+ mode="train",
132
+ res=args.res,
133
+ var_start=0,
134
+ var_end=24,
135
+ diff=bool(0),
136
+ two_frames=bool(0),
137
+ region=args.region,
138
+ hadisd_var=args.var,
139
+ )
140
+
141
+ test_dataset = WeatherDatasetE2E(
142
+ device="cuda",
143
+ hadisd_mode="train",
144
+ start_date="2018-01-01",
145
+ end_date="2018-12-21",
146
+ lead_time=lead_time,
147
+ era5_mode="4u",
148
+ mode="train",
149
+ res=args.res,
150
+ var_start=0,
151
+ var_end=24,
152
+ diff=bool(0),
153
+ two_frames=bool(0),
154
+ region=args.region,
155
+ hadisd_var=args.var,
156
+ )
157
+
158
+ train_sampler = DistributedSampler(train_dataset)
159
+ val_sampler = DistributedSampler(val_dataset)
160
+ test_sampler = DistributedSampler(test_dataset)
161
+
162
+ train_loader = DataLoader(
163
+ train_dataset,
164
+ batch_size=args.batch_size,
165
+ shuffle=False,
166
+ sampler=train_sampler,
167
+ )
168
+
169
+ val_loader = DataLoader(
170
+ val_dataset,
171
+ batch_size=args.batch_size,
172
+ shuffle=False,
173
+ sampler=val_sampler,
174
+ )
175
+
176
+ test_loader = DataLoader(
177
+ test_dataset,
178
+ batch_size=args.batch_size,
179
+ shuffle=False,
180
+ sampler=test_sampler,
181
+ )
182
+
183
+ # Instantiate trainer
184
+ trainer = DDPTrainerE2E(
185
+ model,
186
+ rank,
187
+ train_loader,
188
+ val_loader,
189
+ lf,
190
+ output_dir,
191
+ args.lr,
192
+ train_sampler,
193
+ hadisd_variable_name=args.var,
194
+ weight_decay=args.weight_decay,
195
+ weights_path=None,
196
+ tune_film=0,
197
+ test_loader=test_loader,
198
+ )
199
+
200
+ # Train model
201
+ trainer.train(n_epochs=args.epoch)
202
+ destroy_process_group()
203
+
204
+
205
+ if __name__ == "__main__":
206
+
207
+ parser = argparse.ArgumentParser()
208
+ parser.add_argument("--output_dir")
209
+ parser.add_argument("--loss")
210
+ parser.add_argument("--batch_size", type=int, default=3)
211
+ parser.add_argument("--epoch", type=int, default=10)
212
+ parser.add_argument("--master_port", default="12345")
213
+ parser.add_argument("--lr", type=float, default=5e-5)
214
+ parser.add_argument("--lead_time", type=int)
215
+ parser.add_argument("--era5_mode", default="4u")
216
+ parser.add_argument("--sf_model_path")
217
+ parser.add_argument("--se_model_path")
218
+ parser.add_argument("--forecast_model_path")
219
+
220
+ parser.add_argument("--weight_decay", type=float, default=1e-6)
221
+ parser.add_argument("--max_steps_per_epoch", type=int)
222
+ parser.add_argument("--res", type=int, default=1)
223
+ parser.add_argument("--frequency", type=int, default=6)
224
+ parser.add_argument("--region", default="global")
225
+ parser.add_argument("--var", choices=["tas", "u", "v", "psl", "ws"])
226
+ args = parser.parse_args()
227
+
228
+ torch.device("cuda")
229
+
230
+ # Make results directory
231
+ output_dir = args.output_dir
232
+ try:
233
+ os.mkdir(output_dir)
234
+ except:
235
+ pass
236
+
237
+ # Save config
238
+ with open(output_dir + "/config.pkl", "wb") as f:
239
+ pickle.dump(vars(args), f)
240
+
241
+ world_size = torch.cuda.device_count()
242
+ mp.spawn(main, args=[world_size, output_dir, args], nprocs=world_size)
official-src/aardvark/finetune.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NB: this script is for illustration purposes only and is not runnable as our
3
+ full dataset is not provided as part of the submission due to size constraints.
4
+ Many of the relevant paths to the data have been thus replaced by dummy paths.
5
+ """
6
+
7
+ import os
8
+ import argparse
9
+ import pickle
10
+ import subprocess
11
+ from collections import OrderedDict
12
+
13
+ import numpy as np
14
+ from tqdm import tqdm
15
+ import torch
16
+ import torch.multiprocessing as mp
17
+ import torch.utils.data.distributed
18
+ from torch.utils.data import DataLoader, DistributedSampler
19
+ from torch.distributed import init_process_group, destroy_process_group
20
+
21
+ from loss_functions import WeightedRmseLoss, PressureWeightedRmseLoss, RmseLoss
22
+ from trainer import DDPTrainer
23
+ from loader import *
24
+ from models import *
25
+ from unet_wrap_padding import *
26
+
27
+ torch.set_float32_matmul_precision("medium")
28
+
29
+ weights = np.load(
30
+ "path_to_lat_weights/weights_lat_1.npy",
31
+ ).T[np.newaxis, ..., np.newaxis]
32
+
33
+
34
+ def weighted_rmse_loss(target, output):
35
+ return np.sqrt(np.nanmean(((target - output) ** 2) * weights, axis=(0, 1, 2)))
36
+
37
+
38
+ def unnorm_era5(x, mean, std):
39
+ x = x * std + mean
40
+ return x
41
+
42
+
43
+ def norm_era5(x, mean, std):
44
+ x = (x - mean) / std
45
+ return x
46
+
47
+
48
+ def ddp_setup(rank, world_size, master_port):
49
+ os.environ["MASTER_ADDR"] = "localhost"
50
+ os.environ["MASTER_PORT"] = master_port
51
+ init_process_group(backend="nccl", rank=rank, world_size=world_size)
52
+ torch.cuda.set_device(rank)
53
+
54
+
55
+ def main(rank, world_size, output_dir, args):
56
+ """
57
+ Finetune the processor module for each leadtime
58
+ """
59
+
60
+ # Setup
61
+ master_port = args.master_port
62
+ lead_time = args.lead_time
63
+ quicklook_id = args.output_dir
64
+ era5_mode = args.era5_mode
65
+ ddp_setup(rank, world_size, master_port)
66
+
67
+ # Instantiate loss function
68
+ if args.loss == "lw_rmse":
69
+ lf = WeightedRmseLoss(args.res, weight_per_variable=False)
70
+ elif args.loss == "lw_rmse_pressure_weighted":
71
+ lf = PressureWeightedRmseLoss(args.res, args.era5_mode)
72
+ elif args.loss == "rmse":
73
+ lf = RmseLoss()
74
+
75
+ # Load the pre-trained model
76
+ with open(args.forecast_model_path + "/config.pkl", "rb") as handle:
77
+ forecast_config = pickle.load(handle)
78
+
79
+ model = ConvCNPWeather(
80
+ in_channels=forecast_config["in_channels"],
81
+ out_channels=forecast_config["out_channels"],
82
+ int_channels=forecast_config["int_channels"],
83
+ device="cuda",
84
+ res=forecast_config["res"],
85
+ gnp=bool(0),
86
+ decoder=forecast_config["decoder"],
87
+ mode=forecast_config["mode"],
88
+ film=bool(forecast_config["film"]),
89
+ )
90
+
91
+ try:
92
+ best_epoch = np.argmin(np.load(forecast_config["output_dir"] + "/losses_0.npy"))
93
+ checkpoint = torch.load(
94
+ forecast_config["output_dir"] + "/epoch_{}".format(best_epoch)
95
+ )
96
+
97
+ except:
98
+ best_epoch = np.argmin(
99
+ np.load("../results/" + forecast_config["output_dir"] + "/losses_0.npy")
100
+ )
101
+ checkpoint = torch.load(
102
+ "../results/"
103
+ + forecast_config["output_dir"]
104
+ + "/epoch_{}".format(best_epoch)
105
+ )
106
+
107
+ new_state_dict = OrderedDict()
108
+ for k, v in checkpoint["model_state_dict"].items():
109
+ name = k[7:] # remove `module.`
110
+ new_state_dict[name] = v
111
+
112
+ # Set the path to the prediction from the encoder
113
+ path_to_context = args.assimilation_model_path
114
+
115
+ # Finetune for each leadtime
116
+ for lead_time in range(1, 11):
117
+ print(f"Training lead time {lead_time}")
118
+
119
+ # Setup the model
120
+ model = ConvCNPWeather(
121
+ in_channels=forecast_config["in_channels"],
122
+ out_channels=forecast_config["out_channels"],
123
+ int_channels=forecast_config["int_channels"],
124
+ device="cuda",
125
+ res=forecast_config["res"],
126
+ gnp=bool(0),
127
+ decoder=forecast_config["decoder"],
128
+ mode=forecast_config["mode"],
129
+ film=bool(forecast_config["film"]),
130
+ )
131
+
132
+ model.load_state_dict(new_state_dict)
133
+
134
+ # Setup the loaders
135
+ if lead_time == 1:
136
+ path_to_context = args.assimilation_model_path
137
+ else:
138
+ path_to_context = f"{args.output_dir}/"
139
+
140
+ print(f"Loading context data from {path_to_context}...")
141
+ train_dataset = ForecastLoader(
142
+ device="cuda",
143
+ mode="train",
144
+ lead_time=lead_time,
145
+ era5_mode=args.era5_mode,
146
+ res=args.res,
147
+ frequency=args.frequency,
148
+ diff=bool(args.diff),
149
+ ic_path=path_to_context,
150
+ finetune_step=lead_time,
151
+ random_lt=False,
152
+ finetune_eval_every=250,
153
+ eval_steps=False,
154
+ )
155
+
156
+ val_dataset = ForecastLoader(
157
+ device="cuda",
158
+ mode="val",
159
+ lead_time=lead_time,
160
+ era5_mode=args.era5_mode,
161
+ res=args.res,
162
+ frequency=args.frequency,
163
+ diff=bool(args.diff),
164
+ ic_path=path_to_context,
165
+ finetune_step=lead_time,
166
+ )
167
+
168
+ test_dataset = ForecastLoader(
169
+ device="cuda",
170
+ mode="test",
171
+ lead_time=lead_time,
172
+ era5_mode=args.era5_mode,
173
+ res=args.res,
174
+ frequency=args.frequency,
175
+ diff=bool(args.diff),
176
+ ic_path=path_to_context,
177
+ finetune_step=lead_time,
178
+ )
179
+
180
+ train_sampler = DistributedSampler(train_dataset)
181
+ val_sampler = DistributedSampler(val_dataset)
182
+ test_sampler = DistributedSampler(test_dataset)
183
+
184
+ train_loader = DataLoader(
185
+ train_dataset,
186
+ batch_size=args.batch_size,
187
+ shuffle=False,
188
+ sampler=train_sampler,
189
+ )
190
+
191
+ val_loader = DataLoader(
192
+ val_dataset, batch_size=args.batch_size, shuffle=False, sampler=val_sampler
193
+ )
194
+
195
+ test_loader = DataLoader(
196
+ test_dataset,
197
+ batch_size=args.batch_size,
198
+ shuffle=False,
199
+ sampler=test_sampler,
200
+ )
201
+
202
+ # Setup output dir
203
+ try:
204
+ output_dir = f"{args.output_dir}/forecast_{lead_time}/"
205
+ os.mkdir(output_dir)
206
+ except:
207
+ pass
208
+
209
+ # Run the finetuning
210
+ trainer = DDPTrainer(
211
+ model,
212
+ rank,
213
+ train_loader,
214
+ val_loader,
215
+ lf,
216
+ output_dir,
217
+ args.lr,
218
+ train_sampler,
219
+ weight_decay=args.weight_decay,
220
+ tune_film=False,
221
+ )
222
+
223
+ # Finetune
224
+ n_epochs = args.finetune_epochs
225
+ trainer.train(n_epochs=n_epochs)
226
+
227
+ torch.distributed.barrier()
228
+
229
+ # Load the model just trained
230
+
231
+ best_epoch = np.argmin(
232
+ np.load(f"{args.output_dir}/forecast_{lead_time}/losses_0.npy")
233
+ )
234
+ checkpoint = torch.load(
235
+ f"{args.output_dir}/forecast_{lead_time}/epoch_{best_epoch}"
236
+ )
237
+
238
+ new_state_dict = OrderedDict()
239
+ for k, v in checkpoint["model_state_dict"].items():
240
+ name = k[7:]
241
+ new_state_dict[name] = v
242
+
243
+ model.load_state_dict(new_state_dict)
244
+ model.eval()
245
+
246
+ # Now need to make predictions using this model to create data to train on for the next leadtime
247
+
248
+ # Setup the output arrays
249
+ if rank == 0:
250
+ val_ic = np.memmap(
251
+ f"{args.output_dir}/ic_val_{lead_time}.mmap",
252
+ dtype="float32",
253
+ mode="w+",
254
+ shape=(len(val_dataset), 121, 240, 24),
255
+ )
256
+
257
+ test_ic = np.memmap(
258
+ f"{args.output_dir}/ic_test_{lead_time}.mmap",
259
+ dtype="float32",
260
+ mode="w+",
261
+ shape=(len(test_dataset), 121, 240, 24),
262
+ )
263
+
264
+ test_ic_target = np.memmap(
265
+ f"{args.output_dir}/ic_test_target_{lead_time}.mmap",
266
+ dtype="float32",
267
+ mode="w+",
268
+ shape=(len(test_dataset), 121, 240, 24),
269
+ )
270
+
271
+ train_ic = np.memmap(
272
+ f"{args.output_dir}/ic_train_{lead_time}.mmap",
273
+ dtype="float32",
274
+ mode="w+",
275
+ shape=(len(train_dataset), 121, 240, 24),
276
+ )
277
+
278
+ torch.distributed.barrier()
279
+ val_ic = np.memmap(
280
+ f"{args.output_dir}/ic_val_{lead_time}.mmap",
281
+ dtype="float32",
282
+ mode="r+",
283
+ shape=(len(val_dataset), 121, 240, 24),
284
+ )
285
+
286
+ train_ic = np.memmap(
287
+ f"{args.output_dir}/ic_train_{lead_time}.mmap",
288
+ dtype="float32",
289
+ mode="r+",
290
+ shape=(len(train_dataset), 121, 240, 24),
291
+ )
292
+
293
+ test_ic = np.memmap(
294
+ f"{args.output_dir}/ic_test_{lead_time}.mmap",
295
+ dtype="float32",
296
+ mode="r+",
297
+ shape=(len(test_dataset), 121, 240, 24),
298
+ )
299
+
300
+ test_ic_target = np.memmap(
301
+ f"{args.output_dir}/ic_test_target_{lead_time}.mmap",
302
+ dtype="float32",
303
+ mode="r+",
304
+ shape=(len(test_dataset), 121, 240, 24),
305
+ )
306
+
307
+ print(f"{args.output_dir}/ic_train_{lead_time}.mmap")
308
+ if lead_time > 1:
309
+ if rank == 0:
310
+ subprocess.run(
311
+ [
312
+ "rm",
313
+ f"{args.output_dir}/ic_train_{lead_time-1}.mmap",
314
+ ]
315
+ )
316
+
317
+ torch.distributed.barrier()
318
+
319
+ # Generate predictions on the test set
320
+ with tqdm(test_loader, unit="batch") as tepoch:
321
+ for count, batch in enumerate(tepoch):
322
+ inds = batch["target_index"].detach().cpu().numpy().astype(int)[:, 0]
323
+ out = model(batch, film_index=batch["lt"])
324
+
325
+ base_context = test_loader.dataset.unnorm_base_context(
326
+ batch["y_context"][:, :-11, ...]
327
+ ).permute(0, 3, 2, 1)
328
+ unnorm_pred = test_loader.dataset.unnorm_pred(out)
329
+ unnorm_target = test_loader.dataset.unnorm_pred(batch["y_target"])
330
+
331
+ unnorm_pred = unnorm_pred + base_context
332
+ unnorm_target = unnorm_target + base_context
333
+
334
+ test_ic[inds, ...] = unnorm_pred.detach().cpu()
335
+ test_ic_target[inds, ...] = unnorm_target.detach().cpu()
336
+
337
+ torch.distributed.barrier()
338
+
339
+ # Generate predictions on the validation set
340
+ with tqdm(val_loader, unit="batch") as tepoch:
341
+ for count, batch in enumerate(tepoch):
342
+ inds = batch["target_index"].detach().cpu().numpy().astype(int)[:, 0]
343
+ out = model(batch, film_index=batch["lt"])
344
+
345
+ base_context = val_loader.dataset.unnorm_base_context(
346
+ batch["y_context"][:, :-11, ...]
347
+ ).permute(0, 3, 2, 1)
348
+ unnorm_pred = val_loader.dataset.unnorm_pred(out)
349
+ unnorm_pred = unnorm_pred + base_context
350
+
351
+ val_ic[inds, ...] = unnorm_pred.detach().cpu()
352
+
353
+ torch.distributed.barrier()
354
+
355
+ # Generate predictions on the train set
356
+ with tqdm(train_loader, unit="batch") as tepoch:
357
+ for count, batch in enumerate(tepoch):
358
+ inds = batch["target_index"].detach().cpu().numpy().astype(int)[:, 0]
359
+ out = model(batch, film_index=batch["lt"])
360
+ base_context = val_loader.dataset.unnorm_base_context(
361
+ batch["y_context"][:, :-11, ...]
362
+ ).permute(0, 3, 2, 1)
363
+ unnorm_pred = val_loader.dataset.unnorm_pred(out)
364
+ unnorm_pred = unnorm_pred + base_context
365
+
366
+ train_ic[inds, ...] = unnorm_pred.detach().cpu()
367
+
368
+ torch.distributed.barrier()
369
+
370
+ print("Setting up next lead time...")
371
+ path_to_context = f"{args.output_dir}/"
372
+
373
+ # Set the initial model weights for the next leadtime to be the current trained weights
374
+ if lead_time > 1:
375
+ best_epoch = np.argmin(
376
+ np.load(f"{args.output_dir}/forecast_{lead_time}/losses_0.npy")
377
+ )
378
+ checkpoint = torch.load(
379
+ f"{args.output_dir}/forecast_{lead_time}/epoch_{best_epoch}"
380
+ )
381
+ new_state_dict = OrderedDict()
382
+ for k, v in checkpoint["model_state_dict"].items():
383
+ name = k[7:]
384
+ new_state_dict[name] = v
385
+
386
+ torch.cuda.empty_cache()
387
+
388
+ destroy_process_group()
389
+
390
+
391
+ if __name__ == "__main__":
392
+ parser = argparse.ArgumentParser()
393
+
394
+ parser.add_argument("--output_dir")
395
+ parser.add_argument("--assimilation_model_path")
396
+ parser.add_argument("--forecast_model_path")
397
+
398
+ parser.add_argument("--loss", default="lw_rmse_pressure_weighted")
399
+ parser.add_argument("--batch_size", type=int, default=12)
400
+ parser.add_argument("--master_port", default="12345")
401
+ parser.add_argument("--lr", type=float, default=1e-4)
402
+ parser.add_argument("--lead_time", type=int, default=1)
403
+ parser.add_argument("--era5_mode", default="4u")
404
+ parser.add_argument("--weight_decay", type=float, default=1e-6)
405
+ parser.add_argument("--res", type=int, default=1)
406
+ parser.add_argument("--frequency", type=int, default=6)
407
+
408
+ parser.add_argument("--diff", type=int, default=1)
409
+ parser.add_argument("--start_ind", type=int, default=0)
410
+ parser.add_argument("--end_ind", type=int, default=24)
411
+ parser.add_argument("--finetune_epochs", type=int, default=5)
412
+
413
+ args = parser.parse_args()
414
+
415
+ device = torch.device("cuda")
416
+
417
+ # Setup results directory
418
+ output_dir = args.output_dir
419
+ try:
420
+ os.mkdir(output_dir)
421
+ except:
422
+ pass
423
+
424
+ # Save config
425
+ config = vars(args)
426
+
427
+ with open(output_dir + "/config.pkl", "wb") as f:
428
+ pickle.dump(config, f)
429
+
430
+ world_size = torch.cuda.device_count()
431
+ mp.spawn(main, args=[world_size, output_dir, args], nprocs=world_size)
official-src/aardvark/generate_initial_condition_single.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NB: this script is for illustration purposes only and is not runnable as our
3
+ full dataset is not provided as part of the submission due to size constraints.
4
+ Many of the relevant paths to the data have been thus replaced by dummy paths.
5
+ """
6
+
7
+ import argparse
8
+ import pickle
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ from tqdm import tqdm
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.utils.data.distributed
16
+ from torch.utils.data import DataLoader
17
+
18
+ from loader import WeatherDatasetAssimilation
19
+ from models import *
20
+
21
+ torch.set_float32_matmul_precision("medium")
22
+
23
+
24
+ def unnorm(x, mean, std, diff=False, av_2019=None):
25
+
26
+ x = x * std + mean
27
+ if diff:
28
+ return x + av_2019.transpose(0, 3, 2, 1)
29
+ return x
30
+
31
+
32
+ if __name__ == "__main__":
33
+
34
+ """
35
+ Generate encoder predictions to be used as finetuning data for the processor module
36
+ """
37
+
38
+ parser = argparse.ArgumentParser()
39
+ parser.add_argument("--encoder_model_path")
40
+ args = parser.parse_args()
41
+
42
+ # Load experiment config
43
+ with open(args.encoder_model_path + "/config.pkl", "rb") as handle:
44
+ forecast_config = pickle.load(handle)
45
+
46
+ device = "cuda"
47
+
48
+ # Setup normalisation factors
49
+ era5_mean_spatial = None
50
+ means = np.load(
51
+ "aux_data_path/norm_factors/mean_{}_{}.npy".format(
52
+ forecast_config["era5_mode"], forecast_config["res"]
53
+ )
54
+ )[np.newaxis, np.newaxis, np.newaxis, :]
55
+ stds = np.load(
56
+ "aux_data_path/norm_factors/std_{}_{}.npy".format(
57
+ forecast_config["era5_mode"], forecast_config["res"]
58
+ )
59
+ )[np.newaxis, np.newaxis, np.newaxis, :]
60
+
61
+ # Specify dates to generate predictions for
62
+ labels = ["train", "test", "val"]
63
+ dates = [
64
+ ["2007-01-02", "2017-12-31"],
65
+ ["2018-01-01", "2018-12-31"],
66
+ ["2019-01-01", "2019-12-31"],
67
+ ]
68
+
69
+ # Iterate over loaders
70
+ for label, date in zip(labels, dates):
71
+
72
+ n_times = pd.date_range(date[0], date[1], freq="6H")
73
+
74
+ # Setup a memmap to write out to
75
+ ic = np.memmap(
76
+ "{}/ic_{}.mmap".format(args.encoder_model_path, label),
77
+ dtype="float32",
78
+ mode="w+",
79
+ shape=(len(n_times), 121, 240, 24),
80
+ )
81
+
82
+ var_group_preds = []
83
+ var_group_targets = []
84
+
85
+ # Setup loader
86
+ dataset = WeatherDatasetAssimilation(
87
+ device="cuda",
88
+ hadisd_mode="train",
89
+ start_date=date[0],
90
+ end_date=date[1],
91
+ lead_time=0,
92
+ era5_mode="4u",
93
+ res=1,
94
+ var_start=0,
95
+ var_end=24,
96
+ diff=False,
97
+ )
98
+
99
+ loader = DataLoader(dataset, batch_size=64, shuffle=False) # ,
100
+
101
+ # Instantiate and load model
102
+ model = ConvCNPWeather(
103
+ in_channels=forecast_config["in_channels"],
104
+ out_channels=forecast_config["out_channels"],
105
+ int_channels=forecast_config["int_channels"],
106
+ device="cuda",
107
+ res=forecast_config["res"],
108
+ gnp=bool(0),
109
+ decoder=forecast_config["decoder"],
110
+ mode=forecast_config["mode"],
111
+ film=bool(0),
112
+ )
113
+
114
+ best_epoch = np.argmin(
115
+ np.load("{}/losses_0.npy".format(args.encoder_model_path))
116
+ )
117
+ state_dict = torch.load(
118
+ "{}/epoch_{}".format(args.encoder_model_path, best_epoch),
119
+ map_location=device,
120
+ )["model_state_dict"]
121
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
122
+ model.load_state_dict(state_dict)
123
+ model = nn.DataParallel(model)
124
+ model = model.cuda()
125
+
126
+ model.eval()
127
+
128
+ # Generate predictions
129
+ total = []
130
+ target = []
131
+
132
+ sum_count = 0
133
+ with torch.no_grad():
134
+ with tqdm(loader, unit="batch") as tepoch:
135
+ for count, batch in enumerate(tepoch):
136
+
137
+ out = model(batch, film_index=batch["lt"]).detach().cpu().numpy()
138
+
139
+ out_unnorm = unnorm(
140
+ out,
141
+ means,
142
+ stds,
143
+ diff=False,
144
+ av_2019=era5_mean_spatial,
145
+ )
146
+
147
+ ic[sum_count : sum_count + out.shape[0], ...] = out_unnorm
148
+ sum_count += out.shape[0]
official-src/aardvark/loader.py ADDED
@@ -0,0 +1,1825 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time as timelib
2
+ from time import time
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import torch
7
+ from torch.utils.data import Dataset
8
+
9
+ from loader_utils_new import *
10
+ from data_shapes import *
11
+
12
+
13
+ class WeatherDataset(Dataset):
14
+ """
15
+ Base weather dataset class
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ device,
21
+ hadisd_mode,
22
+ start_date,
23
+ end_date,
24
+ lead_time,
25
+ era5_mode="train",
26
+ res=1,
27
+ filter_dates=None,
28
+ diff=None,
29
+ ):
30
+
31
+ super().__init__()
32
+
33
+ # Setup
34
+ self.device = device
35
+ self.mode = hadisd_mode
36
+ self.data_path = "path_to_data/"
37
+ self.aux_data_path = "path_to_auxiliary_data/"
38
+ self.start_date = start_date
39
+ self.end_date = end_date
40
+ self.lead_time = lead_time
41
+ self.era5_mode = era5_mode
42
+ self.res = res
43
+ self.filter_dates = filter_dates
44
+ self.diff = diff
45
+
46
+ # Date indexing
47
+ self.dates = pd.date_range(start_date, end_date, freq="6H")
48
+ if self.filter_dates == "start":
49
+ self.index = np.array([i for i, d in enumerate(self.dates) if d.month < 7])
50
+ elif self.filter_dates == "end":
51
+ self.index = np.array([i for i, d in enumerate(self.dates) if d.month >= 7])
52
+ else:
53
+ self.index = np.array(range(len(self.dates)))
54
+
55
+ # Load the input modalities
56
+ print("Loading IGRA")
57
+ self.load_igra()
58
+
59
+ print("Loading AMSU-A")
60
+ self.load_amsua()
61
+
62
+ print("Loading AMSU-B")
63
+ self.load_amsub()
64
+
65
+ print("Loading ICOADS")
66
+ self.load_icoads()
67
+
68
+ print("Loading IASI")
69
+ self.load_iasi()
70
+
71
+ print("Loading GEO")
72
+ self.load_sat_data()
73
+
74
+ print("Loading HADISD")
75
+ self.load_hadisd(self.mode)
76
+
77
+ print("Loading ASCAT")
78
+ self.load_ascat_data()
79
+ self.load_hirs_data()
80
+
81
+ # Load the ground truth data for training
82
+ print("Loading ERA5")
83
+ self.era5_sfc = [
84
+ self.load_era5(year)
85
+ for year in range(int(start_date[:4]), int(end_date[:4]) + 1)
86
+ ]
87
+
88
+ # Internal grid to longitude latitude correspondence
89
+ self.era5_x = [
90
+ self.to_tensor(
91
+ np.load(self.data_path + "era5/era5_x_{}.npy".format(self.res))
92
+ )
93
+ / LATLON_SCALE_FACTOR,
94
+ self.to_tensor(
95
+ np.load(self.data_path + "era5/era5_y_{}.npy".format(self.res))
96
+ )
97
+ / LATLON_SCALE_FACTOR,
98
+ ]
99
+
100
+ # Orography
101
+ self.era5_elev = self.to_tensor(
102
+ np.load(self.data_path + "era5/elev_vars_{}.npy".format(self.res))
103
+ )
104
+ self.era5_elev = torch.flip(self.era5_elev.permute(0, 2, 1), [-1])
105
+ xx, yy = torch.meshgrid(self.era5_x[0], self.era5_x[1])
106
+ self.era5_lonlat = torch.stack([xx, yy])
107
+
108
+ # Climatology
109
+ self.climatology = np.memmap(
110
+ self.data_path + "climatology_data.mmap",
111
+ dtype="float32",
112
+ mode="r",
113
+ shape=CLIMATOLOGY_SHAPE,
114
+ )
115
+
116
+ # Setup normalisation factors
117
+ if self.diff:
118
+ self.era5_mean_spatial = np.load(
119
+ self.aux_data_path + "era5_spatial_means.npy"
120
+ )[0, ...]
121
+ self.means = np.load(self.aux_data_path + "era5_avdiff_means.npy")[
122
+ :, np.newaxis, np.newaxis, ...
123
+ ]
124
+ self.stds = np.load(self.aux_data_path + "era5_avdiff_stds.npy")[
125
+ :, np.newaxis, np.newaxis, ...
126
+ ]
127
+ else:
128
+ self.means = np.load(
129
+ self.aux_data_path
130
+ + "norm_factors/mean_{}_{}.npy".format(self.era5_mode, self.res)
131
+ )[:, np.newaxis, np.newaxis, ...]
132
+ self.stds = np.load(
133
+ self.aux_data_path
134
+ + "norm_factors/std_{}_{}.npy".format(self.era5_mode, self.res)
135
+ )[:, np.newaxis, np.newaxis, ...]
136
+
137
+ def load_icoads(self):
138
+ """
139
+ Load the ICOADS data
140
+ """
141
+
142
+ self.icoads_y = np.memmap(
143
+ self.data_path + "icoads/1999_2021_icoads_y.mmap",
144
+ dtype="float32",
145
+ mode="r",
146
+ shape=ICOADS_Y_SHAPE,
147
+ )
148
+
149
+ self.icoads_x = (
150
+ np.memmap(
151
+ self.data_path + "icoads/1999_2021_icoads_x.mmap",
152
+ dtype="float32",
153
+ mode="r",
154
+ shape=ICOADS_X_SHAPE,
155
+ )
156
+ / LATLON_SCALE_FACTOR
157
+ )
158
+ self.icoads_means = self.to_tensor(
159
+ np.load(self.aux_data_path + "norm_factors/mean_icoads.npy")
160
+ )
161
+ self.icoads_stds = self.to_tensor(
162
+ np.load(self.aux_data_path + "norm_factors/std_icoads.npy")
163
+ )
164
+ self.icoads_means = self.to_tensor(
165
+ np.nanmean(self.icoads_y[-365 * 4 :, ...], axis=(0, 2))[:, np.newaxis]
166
+ )
167
+ self.icoads_stds = self.to_tensor(
168
+ np.nanstd(self.icoads_y[-365 * 4 :, ...], axis=(0, 2))[:, np.newaxis]
169
+ )
170
+ self.icoads_index_offset = ICOADS_OFFSETS[self.start_date]
171
+ return
172
+
173
+ def load_igra(self):
174
+ """
175
+ Load the IGRA data
176
+ """
177
+
178
+ self.igra_y = np.memmap(
179
+ self.data_path + "igra/1999_2021_igra_y.mmap",
180
+ dtype="float32",
181
+ mode="r",
182
+ shape=IGRA_Y_SHAPE,
183
+ )
184
+
185
+ self.igra_x = np.copy(
186
+ np.memmap(
187
+ self.data_path + "igra/1999_2021_igra_x.mmap",
188
+ dtype="float32",
189
+ mode="r",
190
+ shape=IGRA_X_SHAPE,
191
+ )
192
+ )
193
+ self.igra_x = self.igra_x / LATLON_SCALE_FACTOR
194
+
195
+ self.igra_means = self.to_tensor(
196
+ np.load(self.aux_data_path + "norm_factors/mean_igra.npy")
197
+ )
198
+ self.igra_stds = self.to_tensor(
199
+ np.load(self.aux_data_path + "norm_factors/std_igra.npy")
200
+ )
201
+
202
+ self.igra_index_offset = IGRA_OFFSETS[self.start_date]
203
+
204
+ return
205
+
206
+ def load_amsua(self):
207
+ """
208
+ Load the AMSU-A data
209
+ """
210
+
211
+ self.amsua_y = np.memmap(
212
+ self.data_path + "amsua/2007_2021_amsua.mmap",
213
+ dtype="float32",
214
+ mode="r",
215
+ shape=AMSUA_Y_SHAPE,
216
+ )
217
+ self.amsua_index_offset = AMSUA_OFFSETS[self.start_date]
218
+
219
+ xx = np.linspace(-180, 179, 360, dtype=np.float32)
220
+ xx = ((xx + 360) % 360) / LATLON_SCALE_FACTOR
221
+ yy = np.linspace(90, -90, 180, dtype=np.float32) / LATLON_SCALE_FACTOR
222
+ self.amsua_x = [xx, yy]
223
+
224
+ self.amsua_means = self.to_tensor(
225
+ np.load(self.aux_data_path + "norm_factors/mean_amsua.npy")
226
+ )
227
+ self.amsua_stds = self.to_tensor(
228
+ np.load(self.aux_data_path + "norm_factors/std_amsua.npy")
229
+ )
230
+
231
+ return
232
+
233
+ def load_amsub(self):
234
+ """
235
+ Load the AMSU-B data
236
+ """
237
+
238
+ self.amsub_y = np.memmap(
239
+ self.data_path + "amsub_mhs/2007_2021_amsub.mmap",
240
+ dtype="float32",
241
+ mode="r",
242
+ shape=AMSUB_Y_SHAPE,
243
+ )
244
+ self.amsub_index_offset = AMSUB_OFFSETS[self.start_date]
245
+
246
+ xx = np.linspace(0, 359, 360, dtype=np.float32)
247
+ xx = ((xx + 360) % 360) / LATLON_SCALE_FACTOR
248
+ yy = np.linspace(90, -90, 181, dtype=np.float32) / LATLON_SCALE_FACTOR
249
+ self.amsub_x = [xx, yy]
250
+
251
+ self.amsub_means = self.to_tensor(
252
+ np.load(self.aux_data_path + "norm_factors/mean_amsub.npy")
253
+ )
254
+ self.amsub_stds = self.to_tensor(
255
+ np.load(self.aux_data_path + "norm_factors/std_amsub.npy")
256
+ )
257
+
258
+ return
259
+
260
+ def load_ascat_data(self):
261
+ """
262
+ Load the ASCAT data
263
+ """
264
+
265
+ self.ascat_y = np.memmap(
266
+ self.data_path + "ascat/2007_2021_ascat.mmap",
267
+ dtype="float32",
268
+ mode="r",
269
+ shape=ASCAT_Y_SHAPE,
270
+ )
271
+ self.ascat_index_offset = ASCAT_OFFSETS[self.start_date]
272
+
273
+ xx = np.linspace(0, 359, 360, dtype=np.float32)
274
+ xx = ((xx + 360) % 360) / LATLON_SCALE_FACTOR
275
+ yy = np.linspace(-90, 90, 181, dtype=np.float32) / LATLON_SCALE_FACTOR
276
+ self.ascat_x = [xx, np.copy(yy[::-1])]
277
+
278
+ self.ascat_means = self.to_tensor(
279
+ np.load(self.aux_data_path + "norm_factors/mean_ascat.npy")
280
+ )
281
+ self.ascat_stds = self.to_tensor(
282
+ np.load(self.aux_data_path + "norm_factors/std_ascat.npy")
283
+ )
284
+
285
+ return
286
+
287
+ def load_hirs_data(self):
288
+ """
289
+ Load the HIRS data
290
+ """
291
+
292
+ self.hirs_y = np.memmap(
293
+ self.data_path + "hirs/2007_2021_hirs.mmap",
294
+ dtype="float32",
295
+ mode="r",
296
+ shape=HIRS_Y_SHAPE,
297
+ )
298
+ self.hirs_index_offset = ASCAT_OFFSETS[self.start_date]
299
+
300
+ xx = np.linspace(0, 359, 360, dtype=np.float32)
301
+ xx = ((xx + 360) % 360) / LATLON_SCALE_FACTOR
302
+ yy = np.linspace(-90, 90, 181, dtype=np.float32) / LATLON_SCALE_FACTOR
303
+ self.hirs_x = [xx, np.copy(yy[::-1])]
304
+
305
+ self.hirs_means = self.to_tensor(
306
+ np.load(self.aux_data_path + "norm_factors/hirs_means.npy")
307
+ )
308
+ self.hirs_stds = self.to_tensor(
309
+ np.load(self.aux_data_path + "norm_factors/hirs_stds.npy")
310
+ )
311
+
312
+ return
313
+
314
+ def load_sat_data(self):
315
+ """
316
+ Load the GRIDSAT data
317
+ """
318
+
319
+ self.sat_y = np.memmap(
320
+ self.data_path + "gridsat/gridsat_data.mmap",
321
+ dtype="float32",
322
+ mode="r",
323
+ shape=GRIDSAT_Y_SHAPE,
324
+ )
325
+
326
+ xx = np.load(self.data_path + "gridsat/sat_x.npy") / LATLON_SCALE_FACTOR
327
+ yy = np.load(self.data_path + "gridsat/sat_y.npy") / LATLON_SCALE_FACTOR
328
+ self.sat_x = [xx, yy]
329
+ self.sat_index_offset = SAT_OFFSETS[self.start_date]
330
+
331
+ self.sat_means = self.to_tensor(
332
+ np.load(self.aux_data_path + "norm_factors/mean_sat.npy")
333
+ )
334
+ self.sat_stds = self.to_tensor(
335
+ np.load(self.aux_data_path + "norm_factors/std_sat.npy")
336
+ )
337
+
338
+ return
339
+
340
+ def load_iasi(self):
341
+ """
342
+ Load the IASI data
343
+ """
344
+
345
+ self.iasi = np.memmap(
346
+ self.data_path + "2007_2021_iasi_subset.mmap",
347
+ dtype="float32",
348
+ mode="r",
349
+ shape=IASI_Y_SHAPE,
350
+ )
351
+ self.iasi_index_offset = ASCAT_OFFSETS[self.start_date]
352
+
353
+ xx = np.linspace(0, 359, 360, dtype=np.float32)
354
+ xx = ((xx + 360) % 360) / LATLON_SCALE_FACTOR
355
+ yy = np.linspace(-90, 90, 181, dtype=np.float32) / LATLON_SCALE_FACTOR
356
+ self.iasi_x = [xx, np.copy(yy[::-1])]
357
+
358
+ self.iasi_means = self.to_tensor(
359
+ np.load(self.aux_data_path + "norm_factors/mean_iasi.npy")
360
+ )
361
+ self.iasi_stds = self.to_tensor(
362
+ np.load(self.aux_data_path + "norm_factors/std_iasi.npy")
363
+ )
364
+
365
+ return
366
+
367
+ def load_hadisd(self, mode):
368
+ """
369
+ Load the HADISD data
370
+ """
371
+
372
+ self.hadisd_x = []
373
+ self.hadisd_alt = []
374
+ self.hadisd_y = []
375
+ hadisd_vars = ["tas", "tds", "psl", "u", "v"]
376
+ for var in hadisd_vars:
377
+ lon = lon_to_0_360(
378
+ np.load(
379
+ self.data_path + "hadisd_processed/{}_lon_{}.npy".format(var, mode)
380
+ )
381
+ )
382
+ lat = np.load(
383
+ self.data_path + "hadisd_processed/{}_lat_{}.npy".format(var, mode)
384
+ )
385
+ alt = np.load(
386
+ self.data_path + "hadisd_processed/{}_alt_{}.npy".format(var, mode)
387
+ )
388
+
389
+ vals = np.memmap(
390
+ self.data_path
391
+ + "hadisd_processed/{}_vals_{}.memmap".format(var, self.mode),
392
+ dtype="float32",
393
+ mode="r",
394
+ shape=get_hadisd_shape(mode),
395
+ )
396
+
397
+ self.hadisd_x.append(np.stack([lon, lat], axis=-1) / LATLON_SCALE_FACTOR)
398
+ self.hadisd_alt.append(alt)
399
+ self.hadisd_y.append(vals)
400
+
401
+ self.hadisd_index_offset = HADISD_OFFSETS[self.start_date]
402
+
403
+ self.hadisd_means = [
404
+ self.to_tensor(
405
+ np.load(
406
+ self.aux_data_path + "norm_factors/mean_hadisd_{}.npy".format(var)
407
+ )
408
+ )
409
+ for var in hadisd_vars
410
+ ]
411
+ self.hadisd_stds = [
412
+ self.to_tensor(
413
+ np.load(
414
+ self.aux_data_path + "norm_factors/std_hadisd_{}.npy".format(var)
415
+ )
416
+ )
417
+ for var in hadisd_vars
418
+ ]
419
+
420
+ return
421
+
422
+ def load_era5(self, year):
423
+ """
424
+ Load the ERA5 training data
425
+ """
426
+
427
+ if year % 4 == 0:
428
+ d = 366 * 4
429
+ else:
430
+ d = 365 * 4
431
+
432
+ if self.era5_mode == "sfc":
433
+ levels = 4
434
+ elif self.era5_mode == "13u":
435
+ levels = 69
436
+ else:
437
+ levels = 24
438
+
439
+ if self.res == 1:
440
+ x = 240
441
+ y = 121
442
+ elif self.res == 5:
443
+ x = 64
444
+ y = 32
445
+ mmap = np.memmap(
446
+ self.data_path
447
+ + "/era5/era5_{}_{}_6_{}.memmap".format(self.era5_mode, self.res, year),
448
+ dtype="float32",
449
+ mode="r",
450
+ shape=(d, levels, x, y),
451
+ )
452
+ return mmap
453
+
454
+ def norm_era5(self, x):
455
+
456
+ x = (x - self.means) / self.stds
457
+ return x
458
+
459
+ def unnorm_era5(self, x):
460
+
461
+ x = x * self.stds + self.means
462
+ return x
463
+
464
+ def norm_data(self, x, means, stds):
465
+ return (x - means) / stds
466
+
467
+ def norm_hadisd(self, x):
468
+ for i in range(5):
469
+ x[i] = (x[i] - self.hadisd_means[i]) / self.hadisd_stds[i]
470
+ return x
471
+
472
+ def __len__(self):
473
+ return self.index.shape[0] - 1 - 1
474
+
475
+ def to_tensor(self, arr):
476
+ return torch.from_numpy(arr).float().to(self.device)
477
+
478
+ def get_time_aux(self, current_date):
479
+ """
480
+ Return the auxiliary temporal channels given a date
481
+ """
482
+
483
+ doy = current_date.dayofyear
484
+ year = (current_date.year - 2007) / 15
485
+ time_of_day = current_date.hour
486
+ return np.array(
487
+ [
488
+ np.cos(np.pi * 2 * doy / DAYS_IN_YEAR),
489
+ np.sin(np.pi * 2 * doy / DAYS_IN_YEAR),
490
+ np.cos(np.pi * 2 * time_of_day / 24),
491
+ np.sin(np.pi * 2 * time_of_day / 24),
492
+ year,
493
+ ]
494
+ )
495
+
496
+
497
+ class WeatherDatasetAssimilation(WeatherDataset):
498
+ """
499
+ Encoder training loader
500
+ """
501
+
502
+ def __init__(
503
+ self,
504
+ device,
505
+ hadisd_mode,
506
+ start_date,
507
+ end_date,
508
+ lead_time,
509
+ era5_mode="sfc",
510
+ res=1,
511
+ filter_dates=None,
512
+ var_start=0,
513
+ var_end=24,
514
+ diff=False,
515
+ two_frames=False,
516
+ ):
517
+
518
+ super().__init__(
519
+ device,
520
+ hadisd_mode,
521
+ start_date,
522
+ end_date,
523
+ lead_time,
524
+ era5_mode,
525
+ res=res,
526
+ filter_dates=filter_dates,
527
+ diff=diff,
528
+ )
529
+
530
+ # Setup
531
+
532
+ self.var_start = var_start
533
+ self.var_end = var_end
534
+ self.diff = diff
535
+ self.two_frames = two_frames
536
+
537
+ def load_era5_time(self, index):
538
+ """
539
+ ERA5 ground truth data loading
540
+ """
541
+
542
+ date = self.dates[index]
543
+ year = date.year
544
+ hour = date.hour
545
+ doy = (date.dayofyear - 1) * 4 + (hour // 6)
546
+
547
+ era5 = self.era5_sfc[year - int(self.start_date[:4])][doy, ...]
548
+ era5 = np.copy(era5)
549
+ if self.diff:
550
+ era5 = era5 - self.era5_mean_spatial
551
+ era5 = self.norm_era5(era5[np.newaxis, ...])[0, ...]
552
+ return era5
553
+
554
+ def load_year_end(self, year, doy):
555
+ data_1 = self.era5_sfc[year - int(self.start_date[:4])][doy : doy + 1, ...]
556
+ missing = self.lead_time - data_1.shape[0] + 1
557
+ data_2 = self.era5_sfc[year - int(self.start_date[:4]) + 1][:missing, ...]
558
+ data = np.concatenate([data_1, data_2])
559
+ return data
560
+
561
+ def load_era5_slice(self, index):
562
+ """
563
+ ERA5 ground truth data loading
564
+ """
565
+
566
+ date = self.dates[index]
567
+ year = date.year
568
+ doy = (date.dayofyear - 1) * 4
569
+
570
+ next_date = self.dates[index + 1]
571
+ next_year = next_date.year
572
+
573
+ if next_year != year:
574
+ era5 = self.load_year_end(year, doy)
575
+ else:
576
+ era5 = self.era5_sfc[year - int(self.start_date[:4])][doy : doy + 1, ...]
577
+
578
+ era5 = self.norm_era5(np.copy(era5))
579
+ return era5
580
+
581
+ def __getitem__(self, index):
582
+
583
+ if self.two_frames:
584
+ # Case 1: loading t=0 and t=-1
585
+ index = index + 1
586
+ current = self.get_index(index, "current")
587
+ prev = self.get_index(index - 1, "prev")
588
+ current["y_target"] = current["y_target_current"]
589
+
590
+ return {**current, **prev}
591
+ else:
592
+ # Case 2: loading t=0
593
+ current = self.get_index(index, "current")
594
+ current["y_target"] = current["y_target_current"]
595
+
596
+ return {**current}
597
+
598
+ def unnorm_pred(self, x):
599
+ dev = x.device
600
+ x = x.detach().cpu().numpy()
601
+
602
+ x = (
603
+ x
604
+ * self.stds[np.newaxis, ...].transpose(0, 2, 3, 1)[
605
+ ..., self.var_start : self.var_end
606
+ ]
607
+ + self.means[np.newaxis, ...].transpose(0, 2, 3, 1)[
608
+ ..., self.var_start : self.var_end
609
+ ]
610
+ )
611
+ if bool(self.diff):
612
+ x = (
613
+ x
614
+ + self.era5_mean_spatial[np.newaxis, ...].transpose(0, 3, 2, 1)[
615
+ ..., self.var_start : self.var_end
616
+ ]
617
+ )
618
+ return torch.from_numpy(x).float().to(dev)
619
+
620
+ def get_index(self, index, prefix):
621
+ """
622
+ Load data for the relevant index respecting different offsets depending on the modality
623
+ """
624
+
625
+ index = self.index[index]
626
+ date = self.dates[index]
627
+
628
+ # ICOADS
629
+ icoads_x = self.icoads_x[index + self.icoads_index_offset, ...]
630
+ icoads_y = self.icoads_y[index + self.icoads_index_offset, ...]
631
+ icoads_x = [icoads_x[0, :], icoads_x[1, :]]
632
+ icoads_x = [self.to_tensor(i) for i in icoads_x]
633
+ icoads_y = self.to_tensor(icoads_y)
634
+ icoads_y = self.norm_data(icoads_y, self.icoads_means, self.icoads_stds)
635
+
636
+ # GRIDSAT
637
+ sat_y = self.sat_y[index + self.sat_index_offset, ...]
638
+ sat_x = [self.to_tensor(i) for i in self.sat_x]
639
+ sat_y = self.to_tensor(sat_y)
640
+ sat_y = self.norm_data(sat_y, self.sat_means, self.sat_stds)
641
+
642
+ # AMSU-A
643
+ amsua_y = self.to_tensor(self.amsua_y[index + self.amsua_index_offset, ...])
644
+ amsua_y[amsua_y < -998] = torch.nan
645
+ amsua_x = [self.to_tensor(i) for i in self.amsua_x]
646
+ amsua_y[amsua_y < -998] = np.nan
647
+ amsua_y = self.norm_data(amsua_y, self.amsua_means, self.amsua_stds)
648
+
649
+ # AMSU-B
650
+ amsub_y = self.to_tensor(self.amsub_y[index + self.amsub_index_offset, ...])
651
+ amsub_y[amsub_y < -998] = torch.nan
652
+ amsub_x = [self.to_tensor(i) for i in self.amsub_x]
653
+ amsub_y[amsub_y < -998] = np.nan
654
+ amsub_y = self.norm_data(amsub_y, self.amsub_means, self.amsub_stds)
655
+
656
+ # IASI
657
+ iasi_y = self.to_tensor(self.iasi[index + self.iasi_index_offset, ...])
658
+ iasi_x = [self.to_tensor(i) for i in self.iasi_x]
659
+ iasi_y = self.norm_data(iasi_y, self.iasi_means, self.iasi_stds)
660
+
661
+ # IGRA
662
+ igra_y = self.to_tensor(self.igra_y[index + self.igra_index_offset, ...])
663
+ igra_x = [self.igra_x[:, 0], self.igra_x[:, 1]]
664
+ igra_x = [self.to_tensor(i) for i in igra_x]
665
+ igra_y = self.norm_data(igra_y, self.igra_means, self.igra_stds)
666
+
667
+ # ASCAT
668
+ ascat_y = self.to_tensor(self.ascat_y[index + self.ascat_index_offset, ...])
669
+ ascat_x = [self.to_tensor(i) for i in self.ascat_x]
670
+ ascat_y[..., 4][ascat_y[..., 4] < -990] = np.nan
671
+ ascat_y = self.norm_data(ascat_y, self.ascat_means, self.ascat_stds)
672
+
673
+ # HIRS
674
+ hirs_y = self.to_tensor(self.hirs_y[index + self.hirs_index_offset, ...])
675
+ hirs_y[hirs_y < -998] = np.nan
676
+ hirs_x = [self.to_tensor(i) for i in self.hirs_x]
677
+ hirs_y = self.norm_data(hirs_y, self.hirs_means, self.hirs_stds)
678
+
679
+ # HadISD
680
+ x_context_hadisd = self.hadisd_x
681
+ y_context_hadisd = [
682
+ i[index + self.hadisd_index_offset, :] for i in self.hadisd_y
683
+ ]
684
+ x_context_hadisd = [self.to_tensor(i).permute(1, 0) for i in x_context_hadisd]
685
+ y_context_hadisd = [self.to_tensor(i) for i in y_context_hadisd]
686
+ y_context_hadisd = self.norm_hadisd(y_context_hadisd)
687
+
688
+ # ERA5
689
+ era5 = self.to_tensor(self.load_era5_time(index))
690
+ era5_target = era5.permute(2, 1, 0)
691
+ era5_x = self.era5_x
692
+
693
+ # AUxiliary variables
694
+ aux_time = self.to_tensor(self.get_time_aux(date))
695
+ climatology = self.climatology[date.hour // 6, date.dayofyear - 1, ...]
696
+
697
+ task = {
698
+ "x_context_hadisd_{}".format(prefix): x_context_hadisd,
699
+ "y_context_hadisd_{}".format(prefix): y_context_hadisd,
700
+ "climatology_{}".format(prefix): self.to_tensor(climatology),
701
+ "sat_x_{}".format(prefix): sat_x,
702
+ "sat_{}".format(prefix): sat_y,
703
+ "icoads_x_{}".format(prefix): icoads_x,
704
+ "icoads_{}".format(prefix): icoads_y,
705
+ "igra_x_{}".format(prefix): igra_x,
706
+ "igra_{}".format(prefix): igra_y,
707
+ "amsua_{}".format(prefix): amsua_y,
708
+ "amsua_x_{}".format(prefix): amsua_x,
709
+ "amsub_{}".format(prefix): amsub_y,
710
+ "amsub_x_{}".format(prefix): amsub_x,
711
+ "iasi_{}".format(prefix): iasi_y,
712
+ "iasi_x_{}".format(prefix): iasi_x,
713
+ "ascat_{}".format(prefix): ascat_y,
714
+ "ascat_x_{}".format(prefix): ascat_x,
715
+ "hirs_{}".format(prefix): hirs_y,
716
+ "hirs_x_{}".format(prefix): hirs_x,
717
+ "y_target_{}".format(prefix): era5_target[
718
+ ..., self.var_start : self.var_end
719
+ ],
720
+ "era5_x_{}".format(prefix): era5_x,
721
+ "era5_elev_{}".format(prefix): self.era5_elev,
722
+ "era5_lonlat_{}".format(prefix): self.era5_lonlat,
723
+ "aux_time_{}".format(prefix): aux_time,
724
+ "lt": torch.Tensor([self.var_start]),
725
+ }
726
+
727
+ return task
728
+
729
+
730
+ class HadISDDataset(Dataset):
731
+ """
732
+ HadISD dataset for decoder training
733
+ """
734
+
735
+ def __init__(self, var, mode, device, start_date, end_date):
736
+ super().__init__()
737
+
738
+ # Setup
739
+ if not mode in ["train", "val", "test"]:
740
+ raise Exception(f"mode is {mode}. Must be train, val, or test.")
741
+
742
+ self.var = var
743
+ self.mode = mode
744
+ self.start_date = start_date
745
+ self.device = device
746
+ dates = pd.date_range(start_date, end_date, freq="6H")
747
+ self.index = np.array(range(len(dates)))
748
+
749
+ # Load the hadISD data
750
+ self.load_hadisd()
751
+
752
+ def load_hadisd(self):
753
+ """
754
+ Load the raw HadISD data
755
+ """
756
+
757
+ data_path = "path_to_data/"
758
+ aux_data_path = "path_to_auxiliary_data/"
759
+ var = self.var
760
+ mode = self.mode
761
+
762
+ vals = np.memmap(
763
+ data_path + f"hadisd_processed/{var}_vals_{mode}.memmap",
764
+ dtype="float32",
765
+ mode="r",
766
+ shape=get_hadisd_shape(mode),
767
+ )
768
+
769
+ lon = lon_to_0_360(
770
+ np.load(data_path + f"hadisd_processed/{var}_lon_{mode}.npy")
771
+ )
772
+ lat = np.load(data_path + f"hadisd_processed/{var}_lat_{mode}.npy")
773
+ self.hadisd_x = np.stack([lon, lat], axis=-1) / LATLON_SCALE_FACTOR
774
+ self.hadisd_alt = np.load(
775
+ data_path + f"hadisd_processed/{var}_alt_{mode}_final.npy"
776
+ )
777
+ self.hadisd_y = vals
778
+
779
+ self.hadisd_index_offset = HADISD_OFFSETS[self.start_date]
780
+ self.hadisd_means = self.to_tensor(
781
+ np.load(aux_data_path + f"norm_factors/mean_hadisd_{var}.npy")
782
+ )
783
+ self.hadisd_stds = self.to_tensor(
784
+ np.load(aux_data_path + f"norm_factors/std_hadisd_{var}.npy")
785
+ )
786
+ return
787
+
788
+ def norm_hadisd(self, x):
789
+ return (x - self.hadisd_means) / self.hadisd_stds
790
+
791
+ def unnorm_pred(self, x):
792
+ return self.hadisd_means + self.hadisd_stds * x
793
+
794
+ def __len__(self):
795
+ return self.index.shape[0] - 2
796
+
797
+ def to_tensor(self, arr):
798
+ return torch.from_numpy(np.array(arr)).float().to(self.device)
799
+
800
+ def __getitem__(self, index):
801
+ index = self.index[index]
802
+
803
+ # Get longitude-latitude locations
804
+ x_target = self.to_tensor(self.hadisd_x).permute(1, 0)
805
+
806
+ # Get altitude and normalise
807
+ m_alt = np.expand_dims(np.load("path_to_mean_alt.npy"), 1)
808
+ s_alt = np.expand_dims(np.load("path_to_std_alt.npy"), 1)
809
+ alt_target = self.to_tensor((self.hadisd_alt - m_alt) / s_alt)[:, :]
810
+
811
+ # Get observations
812
+ y_target = self.norm_hadisd(
813
+ self.to_tensor(self.hadisd_y[index + self.hadisd_index_offset, :])
814
+ )
815
+
816
+ assert x_target.shape[0] == 2
817
+ n_stations = x_target.shape[1]
818
+ assert alt_target.shape[1] == n_stations
819
+ assert y_target.shape[0] == n_stations
820
+
821
+ return {"x": x_target, "altitude": alt_target, "y": y_target}
822
+
823
+
824
+ class AardvarkICDataset(Dataset):
825
+ """
826
+ Helper dataset to handle initial condition loading for decoder training
827
+ """
828
+
829
+ def __init__(self, device, start_date, end_date, lead_time=0):
830
+ super().__init__()
831
+
832
+ # Setup
833
+
834
+ if lead_time == 0:
835
+ # If leadtime is 0 load the output of the encoder
836
+ if start_date == "2007-01-02" and end_date == "2017-12-31":
837
+ ic_fname = "ic_train.mmap"
838
+ elif start_date == "2019-01-01" and end_date == "2019-12-01":
839
+ ic_fname = "ic_val.mmap"
840
+ elif start_date == "2018-01-01" and end_date == "2018-12-31":
841
+ ic_fname = "ic_test.mmap"
842
+ else:
843
+ print((start_date, end_date))
844
+ raise Exception("Invalid start and end date")
845
+
846
+ dates = pd.date_range(start_date, end_date, freq="6H")
847
+
848
+ self.data = np.memmap(
849
+ "path_to_encoder_predictions/" + ic_fname,
850
+ dtype="float32",
851
+ mode="r",
852
+ shape=(len(dates), 121, 240, 24), # shape of the output
853
+ )
854
+ else:
855
+ # if leadtime >0 load the forecast generated from the encoder prediction
856
+ if start_date == "2007-01-02" and end_date == "2017-12-31":
857
+ ic_fname = f"ic_train_{lead_time}.mmap"
858
+
859
+ elif start_date == "2019-01-01" and end_date == "2019-12-01":
860
+ ic_fname = f"ic_val_{lead_time}.mmap"
861
+ elif start_date == "2018-01-01" and end_date == "2018-12-31":
862
+ ic_fname = f"ic_test_{lead_time}.mmap"
863
+ else:
864
+ print((start_date, end_date))
865
+ raise Exception("Invalid start and end date.")
866
+
867
+ dates = pd.date_range(start_date, end_date, freq="6H")[(lead_time) * 4 :]
868
+ ic_shape = (len(dates), 121, 240, 24)
869
+
870
+ self.data = np.memmap(
871
+ self.data_path + "forecast_finetune/" + ic_fname,
872
+ dtype="float32",
873
+ mode="r",
874
+ shape=ic_shape,
875
+ )
876
+
877
+ self.device = device
878
+
879
+ # Normalisation
880
+ aux_data_path = "path_to_auxiliary_data/"
881
+ mean_factors_path = aux_data_path + f"norm_factors/mean_4u_1.npy"
882
+ std_factors_path = aux_data_path + f"norm_factors/std_4u_1.npy"
883
+ self.means = np.load(mean_factors_path)[:, np.newaxis, np.newaxis, ...]
884
+ self.stds = np.load(std_factors_path)[:, np.newaxis, np.newaxis, ...]
885
+
886
+ def __getitem__(self, index):
887
+ # Load Aardvark prediction and normalise
888
+ data_raw = np.transpose(np.copy(self.data[index, :, :, :]), (2, 1, 0))
889
+ data = (data_raw - self.means) / self.stds
890
+ return torch.from_numpy(data).to(self.device)
891
+
892
+
893
+ class WeatherDatasetDownscaling(Dataset):
894
+ """
895
+ Main decoder training dataset. Uses AardvarkICDataset and HadISDDataset to
896
+ handle processor output and station data
897
+ """
898
+
899
+ def __init__(
900
+ self,
901
+ device,
902
+ hadisd_mode,
903
+ start_date,
904
+ end_date,
905
+ context_mode,
906
+ era5_mode="sfc",
907
+ res=1,
908
+ hadisd_var="tas",
909
+ lead_time=1,
910
+ ):
911
+ # The context mode determines whether we make use of ERA5 or our own ICs.
912
+ if not context_mode in ["era5", "aardvark"]:
913
+ raise Exception(
914
+ f"context_mode must be era5 or aardvark, got {context_mode}"
915
+ )
916
+
917
+ super().__init__()
918
+
919
+ # Setup
920
+ self.lead_time = lead_time
921
+
922
+ self.device = device
923
+ self.data_path = "path_to_data/"
924
+ self.aux_data_path = "path_to_auxiliary_data/"
925
+ self.start_date = start_date
926
+ self.end_date = end_date
927
+ self.era5_mode = era5_mode
928
+ self.res = res
929
+ self.context_mode = context_mode
930
+
931
+ self.dates = pd.date_range(start_date, end_date, freq="6H")
932
+ self.index = np.array(range(len(self.dates)))
933
+
934
+ # Load ERA5 data for pre-training
935
+ self.era5_sfc = [
936
+ self.load_era5(year)
937
+ for year in range(int(start_date[:4]), int(end_date[:4]) + 1)
938
+ ]
939
+
940
+ raw_era5_lon = np.load(self.data_path + f"era5/era5_x_{res}.npy")
941
+ raw_era5_lat = np.load(self.data_path + f"era5/era5_y_{res}.npy")
942
+ self.era5_x = [
943
+ self.to_tensor(raw_era5_lon) / LATLON_SCALE_FACTOR,
944
+ self.to_tensor(raw_era5_lat) / LATLON_SCALE_FACTOR,
945
+ ]
946
+
947
+ # Load orography
948
+ elev_path = self.data_path + f"era5/elev_vars_{res}.npy"
949
+ self.era5_elev = self.to_tensor(np.load(elev_path)).permute(0, 2, 1)
950
+
951
+ # Normalisation
952
+ mean_factors_path = (
953
+ self.aux_data_path + f"norm_factors/mean_{era5_mode}_{res}.npy"
954
+ )
955
+ std_factors_path = (
956
+ self.aux_data_path + f"norm_factors/std_{era5_mode}_{res}.npy"
957
+ )
958
+ self.means = np.load(mean_factors_path)[:, np.newaxis, np.newaxis, ...]
959
+ self.stds = np.load(std_factors_path)[:, np.newaxis, np.newaxis, ...]
960
+
961
+ # HadISD data
962
+ self.hadisd_data = HadISDDataset(
963
+ var=hadisd_var,
964
+ mode=hadisd_mode,
965
+ device=device,
966
+ start_date=start_date,
967
+ end_date=end_date,
968
+ )
969
+
970
+ if context_mode == "aardvark":
971
+ # Load the Aardvark encoder predictions
972
+ self.aardvark_data = AardvarkICDataset(
973
+ device, start_date, end_date, lead_time
974
+ )
975
+
976
+ def load_era5(self, year):
977
+ """
978
+ Load the raw ERA5 data
979
+ """
980
+
981
+ if year % 4 == 0:
982
+ d = 366 * 4
983
+ else:
984
+ d = 365 * 4
985
+
986
+ if self.era5_mode == "sfc":
987
+ levels = 4
988
+ elif self.era5_mode == "13u":
989
+ levels = 69
990
+ else:
991
+ levels = 24
992
+
993
+ if self.res == 1:
994
+ x = 240
995
+ y = 121
996
+ elif self.res == 5:
997
+ x = 64
998
+ y = 32
999
+ mmap = np.memmap(
1000
+ self.data_path
1001
+ + "era5/era5_{}_{}_6_{}.memmap".format(self.era5_mode, self.res, year),
1002
+ dtype="float32",
1003
+ mode="r",
1004
+ shape=(d, levels, x, y),
1005
+ )
1006
+ return mmap
1007
+
1008
+ def norm_era5(self, x):
1009
+ x = (x - self.means) / self.stds
1010
+ return x
1011
+
1012
+ def unnorm_era5(self, x):
1013
+ x = x * self.stds + self.means
1014
+ return x
1015
+
1016
+ def unnorm_pred(self, x):
1017
+ return self.hadisd_data.unnorm_pred(x)
1018
+
1019
+ def norm_data(self, x, means, stds):
1020
+ return (x - means) / stds
1021
+
1022
+ def __len__(self):
1023
+ return self.index.shape[0] - (self.lead_time) * 4
1024
+
1025
+ def to_tensor(self, arr):
1026
+ return torch.from_numpy(np.array(arr)).float().to(self.device)
1027
+
1028
+ def get_time_aux(self, current_date):
1029
+ """
1030
+ Get auxiliary time variables for a given date
1031
+ """
1032
+
1033
+ doy = current_date.dayofyear
1034
+ year = (current_date.year - 2007) / 15
1035
+ time_of_day = current_date.hour
1036
+ return np.array(
1037
+ [
1038
+ np.cos(np.pi * 2 * doy / DAYS_IN_YEAR),
1039
+ np.sin(np.pi * 2 * doy / DAYS_IN_YEAR),
1040
+ np.cos(np.pi * 2 * time_of_day / 24),
1041
+ np.sin(np.pi * 2 * time_of_day / 24),
1042
+ year,
1043
+ ]
1044
+ )
1045
+
1046
+ def load_era5_time(self, index):
1047
+ """
1048
+ Load ERA5 training data
1049
+ """
1050
+
1051
+ date = self.dates[index]
1052
+ year = date.year
1053
+ hour = date.hour
1054
+ doy = (date.dayofyear - 1) * 4 + (hour // 6)
1055
+
1056
+ era5 = self.era5_sfc[year - int(self.start_date[:4])][doy, ...]
1057
+ era5 = np.copy(era5)
1058
+ era5 = self.norm_era5(era5[np.newaxis, ...])[0, ...]
1059
+ return era5
1060
+
1061
+ def load_year_end(self, year, doy):
1062
+ data_1 = self.era5_sfc[year - int(self.start_date[:4])][doy : doy + 1, ...]
1063
+ missing = self.lead_time - data_1.shape[0] + 1
1064
+ data_2 = self.era5_sfc[year - int(self.start_date[:4]) + 1][:missing, ...]
1065
+ data = np.concatenate([data_1, data_2])
1066
+ return data
1067
+
1068
+ def __getitem__(self, index):
1069
+
1070
+ index = self.index[index]
1071
+ date = self.dates[index + 4 * self.lead_time]
1072
+
1073
+ # Get HadISD data
1074
+ hadisd_slice = self.hadisd_data[index + 4 * self.lead_time]
1075
+
1076
+ # Get lon-lat
1077
+ x_context = self.era5_x
1078
+ n_lon = x_context[0].shape[0]
1079
+ n_lat = x_context[1].shape[0]
1080
+
1081
+ # Get auxiliary time
1082
+ aux_time = torch.reshape(self.to_tensor(self.get_time_aux(date)), (-1, 1, 1))
1083
+
1084
+ # Load the context (either aardvark or ERA5 for use in pre-training)
1085
+ if self.context_mode == "era5":
1086
+ y_context_obs = self.to_tensor(
1087
+ self.load_era5_time(index + 4 * self.lead_time)
1088
+ )
1089
+
1090
+ elif self.context_mode == "aardvark":
1091
+ y_context_obs = self.aardvark_data[index]
1092
+
1093
+ else:
1094
+ raise Exception
1095
+
1096
+ y_context = torch.cat(
1097
+ [
1098
+ y_context_obs,
1099
+ self.era5_elev.permute(0, 2, 1),
1100
+ aux_time.repeat(1, n_lon, n_lat),
1101
+ ]
1102
+ )
1103
+
1104
+ assert y_context.shape[1] == n_lon
1105
+ assert y_context.shape[2] == n_lat
1106
+
1107
+ x = hadisd_slice["x"]
1108
+ alt = hadisd_slice["altitude"]
1109
+ y = hadisd_slice["y"]
1110
+
1111
+ return {
1112
+ "x_target": x,
1113
+ "alt_target": alt,
1114
+ "y_target": y,
1115
+ "y_context": y_context,
1116
+ "x_context": x_context,
1117
+ "aux_time": aux_time,
1118
+ "lt": torch.Tensor([0]),
1119
+ }
1120
+
1121
+
1122
+ class ForecasterDatasetDownscaling(Dataset):
1123
+ """
1124
+ Dataset to generate decoder predictions from pre-saved Aardvark forecasts
1125
+ """
1126
+
1127
+ def __init__(
1128
+ self,
1129
+ start_date,
1130
+ end_date,
1131
+ lead_time,
1132
+ hadisd_var,
1133
+ mode,
1134
+ device,
1135
+ forecast_path,
1136
+ region="global",
1137
+ ):
1138
+ super().__init__()
1139
+
1140
+ # Setup
1141
+
1142
+ if not mode in ["train", "val", "test"]:
1143
+ raise Exception(f"Mode is {mode}. Must be either train, val, or test")
1144
+
1145
+ self.device = device
1146
+ self.start_date = start_date
1147
+ self.end_date = end_date
1148
+ self.lead_time = lead_time
1149
+ self.mode = mode
1150
+ self.offset = np.timedelta64(lead_time, "D").astype("timedelta64[ns]")
1151
+
1152
+ self.dates = pd.date_range(start_date, end_date, freq="6H")[:-30]
1153
+
1154
+ # Normalisation
1155
+ aux_data_path = "auxiliary_data_path/"
1156
+ self.means = np.load(aux_data_path + "norm_factors/mean_4u_1.npy")
1157
+ self.stds = np.load(aux_data_path + "norm_factors/std_4u_1.npy")
1158
+
1159
+ # Load auxiliary data
1160
+ self.load_npy_file()
1161
+ data_path = "data_path/"
1162
+ res = "1"
1163
+ raw_era5_lon = np.load(data_path + f"era5/era5_x_{res}.npy")
1164
+ raw_era5_lat = np.load(data_path + f"era5/era5_y_{res}.npy")
1165
+ self.era5_x = [
1166
+ self.to_tensor(raw_era5_lon) / LATLON_SCALE_FACTOR,
1167
+ self.to_tensor(raw_era5_lat) / LATLON_SCALE_FACTOR,
1168
+ ]
1169
+ elev_path = data_path + f"era5/elev_vars_{res}.npy"
1170
+ self.era5_elev = self.to_tensor(np.load(elev_path)).permute(0, 2, 1)
1171
+
1172
+ # Load hadISD
1173
+ self.hadisd_data = HadISDDataset(
1174
+ var=hadisd_var,
1175
+ mode="train",
1176
+ device=device,
1177
+ start_date=start_date,
1178
+ end_date=end_date,
1179
+ )
1180
+
1181
+ # Subset to region
1182
+ self.region = region
1183
+ if self.region != "global":
1184
+ self.mask = np.load(
1185
+ self.data_path + f"hadisd_processed/tas_mask_train_{region}.npy"
1186
+ )
1187
+
1188
+ def date_range(self):
1189
+ return np.arange(
1190
+ start=np.datetime64(self.start_date).astype("datetime64[ns]"),
1191
+ stop=np.datetime64(self.end_date).astype("datetime64[ns]"),
1192
+ step=np.timedelta64(1, "D").astype("timedelta64[ns]"),
1193
+ )
1194
+
1195
+ def load_npy_file(self):
1196
+ """
1197
+ Load the pre-saved Aardvark forecasts
1198
+ """
1199
+
1200
+ dates = pd.date_range(self.start_date, self.end_date, freq="6H")
1201
+
1202
+ if self.mode == "train":
1203
+ dates = dates[:-40] # Need 10 day offset at end of year
1204
+
1205
+ self.Y_context = np.memmap(
1206
+ "path_to_forecasts/forecast_{}.mmap".format(self.mode),
1207
+ dtype="float32",
1208
+ mode="r",
1209
+ shape=(len(dates), 121, 240, 24, 11),
1210
+ )
1211
+
1212
+ return
1213
+
1214
+ def norm_era5(self, x):
1215
+ return (x - self.means) / self.stds
1216
+
1217
+ def norm_hadisd(self, x):
1218
+ return self.hadisd_data.norm_hadisd(x)
1219
+
1220
+ def unnorm_pred(self, x):
1221
+ return self.hadisd_data.unnorm_pred(x)
1222
+
1223
+ def __len__(self):
1224
+ return len(self.dates) - 40 # Need 10 day offset at end of year
1225
+
1226
+ def to_tensor(self, arr):
1227
+ return torch.from_numpy(np.array(arr)).float().to(self.device)
1228
+
1229
+ def get_time_aux(self, index):
1230
+ """
1231
+ Get the auxiliary time variables
1232
+ """
1233
+
1234
+ current_date = (self.dates + self.offset)[index]
1235
+ doy = current_date.dayofyear
1236
+ year = (current_date.year - 2007) / 15
1237
+ time_of_day = current_date.hour
1238
+ return np.array(
1239
+ [
1240
+ np.cos(np.pi * 2 * doy / DAYS_IN_YEAR),
1241
+ np.sin(np.pi * 2 * doy / DAYS_IN_YEAR),
1242
+ np.cos(np.pi * 2 * time_of_day / 24),
1243
+ np.sin(np.pi * 2 * time_of_day / 24),
1244
+ year,
1245
+ ]
1246
+ )
1247
+
1248
+ def __getitem__(self, index):
1249
+
1250
+ # Load target data
1251
+ hadisd_slice = self.hadisd_data[index + 4 * self.lead_time]
1252
+
1253
+ x_context = self.era5_x
1254
+ n_lon = x_context[0].shape[0]
1255
+ n_lat = x_context[1].shape[0]
1256
+
1257
+ # Load auxiliary time
1258
+ aux_time = torch.reshape(self.to_tensor(self.get_time_aux(index)), (-1, 1, 1))
1259
+
1260
+ # Load input
1261
+ y_context = self.norm_era5(self.Y_context[index, ..., self.lead_time])
1262
+ y_context = torch.cat(
1263
+ [
1264
+ self.to_tensor(y_context).permute(2, 1, 0),
1265
+ self.era5_elev.permute(0, 2, 1),
1266
+ aux_time.repeat(1, n_lon, n_lat),
1267
+ ]
1268
+ )
1269
+
1270
+ assert y_context.shape[1] == n_lon
1271
+ assert y_context.shape[2] == n_lat
1272
+
1273
+ # Handle region masking
1274
+ if self.region != "global":
1275
+ hadisd_slice["y"][self.mask] = np.nan
1276
+
1277
+ return {
1278
+ "x_target": hadisd_slice["x"],
1279
+ "alt_target": hadisd_slice["altitude"],
1280
+ "y_target": hadisd_slice["y"],
1281
+ "y_context": y_context,
1282
+ "x_context": x_context,
1283
+ "aux_time": aux_time,
1284
+ "lt": torch.Tensor([0]),
1285
+ }
1286
+
1287
+
1288
+ class ForecastLoader(Dataset):
1289
+ """
1290
+ Loader for finetuning the processor module
1291
+ """
1292
+
1293
+ def __init__(
1294
+ self,
1295
+ device,
1296
+ mode,
1297
+ lead_time,
1298
+ era5_mode="sfc",
1299
+ res=5,
1300
+ frequency=24,
1301
+ norm=True,
1302
+ diff=False,
1303
+ rollout=False,
1304
+ random_lt=False,
1305
+ u_only=False,
1306
+ ic_path=None,
1307
+ finetune_step=None,
1308
+ finetune_eval_every=100,
1309
+ eval_steps=False,
1310
+ ):
1311
+
1312
+ super().__init__()
1313
+
1314
+ # Setup
1315
+ self.device = device
1316
+ self.mode = mode
1317
+ self.data_path = "data_path/"
1318
+
1319
+ self.lead_time = lead_time
1320
+ self.era5_mode = era5_mode
1321
+ self.res = res
1322
+ self.frequency = frequency
1323
+ self.norm = norm
1324
+ self.diff = diff
1325
+ self.rollout = rollout
1326
+ self.random_lt = random_lt
1327
+ self.u_only = u_only
1328
+ self.ic_path = ic_path
1329
+
1330
+ self.finetune_step = finetune_step
1331
+ self.finetune_eval_every = finetune_eval_every
1332
+ self.eval_steps = eval_steps
1333
+
1334
+ if self.frequency == 6:
1335
+ self.lead_time = self.lead_time * 4
1336
+ freq = "6H"
1337
+
1338
+ else:
1339
+ freq = "1D"
1340
+
1341
+ if self.mode == "train":
1342
+ self.dates = pd.date_range("1979-01-01", "2017-12-31", freq=freq)
1343
+ elif self.mode == "tune":
1344
+ self.dates = pd.date_range("2018-01-01", "2018-12-31", freq=freq)
1345
+ elif self.mode == "test":
1346
+ self.dates = pd.date_range("2018-01-01", "2018-12-31", freq=freq)
1347
+ elif self.mode == "val":
1348
+ self.dates = pd.date_range("2019-01-01", "2019-12-31", freq=freq)
1349
+
1350
+ # Load the predictions from the previous leadtime to be the new context set
1351
+ if self.finetune_step is not None:
1352
+
1353
+ if self.mode == "train":
1354
+ self.dates = pd.date_range("2007-01-02", "2017-12-31", freq=freq)
1355
+ ic_shape = (
1356
+ len(self.dates) - max(0, (self.finetune_step - 1) * 4),
1357
+ 121,
1358
+ 240,
1359
+ 24,
1360
+ )
1361
+ elif self.mode == "val":
1362
+ self.dates = pd.date_range("2019-01-01", "2019-12-31", freq=freq)
1363
+ ic_shape = (
1364
+ len(self.dates) - max(0, (self.finetune_step - 1) * 4),
1365
+ 121,
1366
+ 240,
1367
+ 24,
1368
+ )
1369
+ elif self.mode == "test":
1370
+ self.dates = pd.date_range("2018-01-01", "2018-12-31", freq=freq)
1371
+ ic_shape = (
1372
+ len(self.dates) - max(0, (self.finetune_step - 1) * 4),
1373
+ 121,
1374
+ 240,
1375
+ 24,
1376
+ )
1377
+
1378
+ if self.finetune_step > 1:
1379
+ print(ic_shape)
1380
+ self.ic = np.memmap(
1381
+ self.ic_path
1382
+ + "ic_{}_{}.mmap".format(self.mode, self.finetune_step - 1),
1383
+ dtype="float32",
1384
+ mode="r",
1385
+ shape=ic_shape,
1386
+ )
1387
+ elif self.ic_path is not None:
1388
+
1389
+ self.ic = np.memmap(
1390
+ self.ic_path + "ic_{}.mmap".format(self.mode),
1391
+ dtype="float32",
1392
+ mode="r",
1393
+ shape=ic_shape,
1394
+ )
1395
+
1396
+ elif self.ic_path is not None:
1397
+ if self.mode == "train":
1398
+ self.dates = pd.date_range("2007-01-02", "2017-12-31", freq=freq)
1399
+ ic_shape = (len(self.dates), 121, 240, 24)
1400
+
1401
+ self.ic = np.memmap(
1402
+ self.ic_path + "/ic_{}.mmap".format(self.mode),
1403
+ dtype="float32",
1404
+ mode="r",
1405
+ shape=ic_shape,
1406
+ )
1407
+
1408
+ # Orography
1409
+ self.era5_elev = np.float32(
1410
+ np.load(self.data_path + "era5/elev_vars_{}.npy".format(res))
1411
+ )
1412
+ elev_mean = self.era5_elev.mean(axis=(1, 2))[:, np.newaxis, np.newaxis]
1413
+ elev_std = self.era5_elev.std(axis=(1, 2))[:, np.newaxis, np.newaxis]
1414
+ self.era5_elev = (self.era5_elev - elev_mean) / elev_std
1415
+
1416
+ # ERA5 ground truth data for training
1417
+ self.era5_sfc = [
1418
+ self.load_era5(year)
1419
+ for year in range(int(self.dates[0].year), int(self.dates[-1].year) + 1)
1420
+ ]
1421
+
1422
+ # Noramalisation factors
1423
+ self.means = (
1424
+ self.to_tensor(
1425
+ np.load(
1426
+ self.data_path
1427
+ + "norm_factors/mean_{}_{}.npy".format(self.era5_mode, self.res)
1428
+ )
1429
+ )
1430
+ .unsqueeze(1)
1431
+ .unsqueeze(1)
1432
+ )
1433
+ self.stds = (
1434
+ self.to_tensor(
1435
+ np.load(
1436
+ self.data_path
1437
+ + "norm_factors/std_{}_{}.npy".format(self.era5_mode, self.res)
1438
+ )
1439
+ )
1440
+ .unsqueeze(1)
1441
+ .unsqueeze(1)
1442
+ )
1443
+ self.diff_means = (
1444
+ self.to_tensor(
1445
+ np.load(
1446
+ self.data_path
1447
+ + "norm_factors/mean_diff_{}_{}.npy".format(
1448
+ self.era5_mode, self.res
1449
+ )
1450
+ )
1451
+ )
1452
+ .unsqueeze(0)
1453
+ .unsqueeze(0)
1454
+ )
1455
+ self.diff_stds = (
1456
+ self.to_tensor(
1457
+ np.load(
1458
+ self.data_path
1459
+ + "norm_factors/std_diff_{}_{}.npy".format(self.era5_mode, self.res)
1460
+ )
1461
+ )
1462
+ .unsqueeze(0)
1463
+ .unsqueeze(0)
1464
+ )
1465
+
1466
+ self.diff_means_1 = (
1467
+ self.to_tensor(
1468
+ np.load(
1469
+ self.data_path
1470
+ + "norm_factors/mean_diff_{}_{}_6h.npy".format(
1471
+ self.era5_mode, self.res
1472
+ )
1473
+ )
1474
+ )
1475
+ .unsqueeze(0)
1476
+ .unsqueeze(0)
1477
+ )
1478
+ self.diff_stds_1 = (
1479
+ self.to_tensor(
1480
+ np.load(
1481
+ self.data_path
1482
+ + "norm_factors/std_diff_{}_{}_6h.npy".format(
1483
+ self.era5_mode, self.res
1484
+ )
1485
+ )
1486
+ )
1487
+ .unsqueeze(0)
1488
+ .unsqueeze(0)
1489
+ )
1490
+
1491
+ self.diff_means_2 = (
1492
+ self.to_tensor(
1493
+ np.load(
1494
+ self.data_path
1495
+ + "norm_factors/mean_diff_{}_{}_12h.npy".format(
1496
+ self.era5_mode, self.res
1497
+ )
1498
+ )
1499
+ )
1500
+ .unsqueeze(0)
1501
+ .unsqueeze(0)
1502
+ )
1503
+ self.diff_stds_2 = (
1504
+ self.to_tensor(
1505
+ np.load(
1506
+ self.data_path
1507
+ + "norm_factors/std_diff_{}_{}_12h.npy".format(
1508
+ self.era5_mode, self.res
1509
+ )
1510
+ )
1511
+ )
1512
+ .unsqueeze(0)
1513
+ .unsqueeze(0)
1514
+ )
1515
+
1516
+ self.means_dict = {
1517
+ 0: self.diff_means,
1518
+ 2: self.diff_means_2,
1519
+ 3: self.diff_means_1,
1520
+ }
1521
+
1522
+ self.stds_dict = {0: self.diff_stds, 2: self.diff_stds_2, 3: self.diff_stds_1}
1523
+
1524
+ def __len__(self):
1525
+ if np.logical_and(self.eval_steps, self.mode == "train"):
1526
+ return self.finetune_eval_every * 12 * 4
1527
+
1528
+ return self.dates.shape[0] - self.lead_time
1529
+
1530
+ def to_tensor(self, arr):
1531
+
1532
+ return torch.from_numpy(arr).float().to(self.device)
1533
+
1534
+ def norm_era5(self, x):
1535
+ x = (x - self.means) / self.stds
1536
+ return x
1537
+
1538
+ def norm_era5_tendency(self, x, lt_offset):
1539
+
1540
+ x = (x - self.means_dict[lt_offset]) / self.stds_dict[lt_offset]
1541
+ return x
1542
+
1543
+ def unnorm_pred(self, x):
1544
+ x = x * self.diff_stds.unsqueeze(0) + self.diff_means.unsqueeze(0)
1545
+ return x
1546
+
1547
+ def unnorm_base_context(self, x):
1548
+ x = x * self.stds.unsqueeze(0) + self.means.unsqueeze(0)
1549
+ return x
1550
+
1551
+ def load_era5(self, year):
1552
+ """
1553
+ Load ERA5 data for training
1554
+ """
1555
+
1556
+ if year % 4 == 0:
1557
+ d = 366
1558
+ else:
1559
+ d = 365
1560
+
1561
+ if self.frequency == 6:
1562
+ d = d * 4
1563
+
1564
+ if self.era5_mode == "sfc":
1565
+ levels = 4
1566
+ elif self.era5_mode == "13u":
1567
+ levels = 69
1568
+ else:
1569
+ levels = 24
1570
+
1571
+ if self.res == 1:
1572
+ x = 240
1573
+ y = 121
1574
+ elif self.res == 5:
1575
+ x = 64
1576
+ y = 32
1577
+
1578
+ mmap = np.memmap(
1579
+ self.data_path
1580
+ + "era5/era5_{}_{}_{}_{}.memmap".format(
1581
+ self.era5_mode, self.res, self.frequency, year
1582
+ ),
1583
+ dtype="float32",
1584
+ mode="r",
1585
+ shape=(d, levels, x, y),
1586
+ )
1587
+ return mmap
1588
+
1589
+ def load_era5_time(self, index):
1590
+ """
1591
+ Load ERA5 data for training
1592
+ """
1593
+ date = self.dates[index]
1594
+ year = date.year
1595
+ doy = date.dayofyear - 1
1596
+ hour = date.hour
1597
+ if self.frequency == 6:
1598
+ era5 = self.era5_sfc[year - int(self.dates[0].year)][
1599
+ doy * 4 + hour // 6, ...
1600
+ ]
1601
+ else:
1602
+ era5 = self.era5_sfc[year - int(self.dates[0].year)][doy, ...]
1603
+
1604
+ return np.copy(era5)
1605
+
1606
+ def make_time_channels(self, index, x, y):
1607
+ """
1608
+ Make auxiliary time channels
1609
+ """
1610
+
1611
+ date = self.dates[index]
1612
+ hour = date.hour
1613
+ doy = date.dayofyear - 1
1614
+ if date.year % 4 == 0:
1615
+ n_days = 366
1616
+ else:
1617
+ n_days = 365
1618
+ hour_sin = np.sin(hour * np.pi / 12) * np.float32(np.ones((1, x, y)))
1619
+ hour_cos = np.cos(hour * np.pi / 12) * np.float32(np.ones((1, x, y)))
1620
+ doy_sin = np.sin(doy * 2 * np.pi / n_days) * np.float32(np.ones((1, x, y)))
1621
+ doy_cos = np.cos(doy * 2 * np.pi / n_days) * np.float32(np.ones((1, x, y)))
1622
+
1623
+ return np.concatenate([hour_sin, hour_cos, doy_sin, doy_cos])
1624
+
1625
+ def __getitem__(self, index):
1626
+
1627
+ # Option to offset to random leadtime
1628
+ lt_offset = 0
1629
+ if self.random_lt:
1630
+ lt_offset = np.random.choice([0, 2, 3])
1631
+
1632
+ # Load ground truth data
1633
+ y_target = self.to_tensor(
1634
+ self.load_era5_time(index + self.lead_time - lt_offset)
1635
+ )
1636
+
1637
+ # Load either initial condition or ERA5 depending on task
1638
+ if self.ic_path is not None:
1639
+ era5_ts0 = self.ic[index].copy().transpose(2, 1, 0)
1640
+
1641
+ else:
1642
+ era5_ts0 = self.load_era5_time(index)
1643
+
1644
+ # Auxiliary time
1645
+ time = self.make_time_channels(index, era5_ts0.shape[1], era5_ts0.shape[2])
1646
+ era5_ts0 = self.to_tensor(
1647
+ np.concatenate([era5_ts0, self.era5_elev, time], axis=0)
1648
+ )
1649
+ y_context = era5_ts0.permute(0, 2, 1)[:, ...]
1650
+
1651
+ # Normalisation
1652
+ if self.diff:
1653
+ y_target = (y_target - era5_ts0[:24, ...]).permute(2, 1, 0)
1654
+ y_target = self.norm_era5_tendency(y_target, lt_offset)
1655
+ y_context[:24, ...] = self.norm_era5(y_context[:24, ...])
1656
+
1657
+ else:
1658
+ if self.norm:
1659
+ y_context[:24, ...] = self.norm_era5(y_context[:24, ...], lt_offset)
1660
+ y_target = self.norm_era5(y_target, lt_offset)
1661
+ y_target = y_target.permute(2, 1, 0)
1662
+
1663
+ if self.rollout:
1664
+ # Option to return entire timeseries of target data
1665
+ targets = []
1666
+ for t in range(self.lead_time + 1):
1667
+ t = self.to_tensor(self.load_era5_time(index + t))
1668
+ targets.append(t.permute(2, 1, 0))
1669
+ targets = torch.stack(targets, dim=-1)[..., ::4]
1670
+
1671
+ return {
1672
+ "y_context": y_context.permute(0, 2, 1),
1673
+ "y_target": y_target,
1674
+ "targets": targets,
1675
+ "lt": self.to_tensor(np.array([lt_offset])),
1676
+ }
1677
+
1678
+ else:
1679
+ return {
1680
+ "y_context": y_context.permute(0, 2, 1),
1681
+ "y_target": y_target[..., :],
1682
+ "lt": self.to_tensor(np.array([lt_offset])),
1683
+ "target_index": self.to_tensor(np.array([index])),
1684
+ }
1685
+
1686
+
1687
+ class WeatherDatasetE2E(WeatherDataset):
1688
+ """
1689
+ Dataset for running Aardvark end-to-end
1690
+ """
1691
+
1692
+ def __init__(
1693
+ self,
1694
+ device,
1695
+ hadisd_mode,
1696
+ start_date,
1697
+ end_date,
1698
+ lead_time,
1699
+ mode,
1700
+ hadisd_var,
1701
+ max_steps_per_epoch=None,
1702
+ era5_mode="sfc",
1703
+ res=1,
1704
+ filter_dates=None,
1705
+ var_start=0,
1706
+ var_end=24,
1707
+ diff=False,
1708
+ two_frames=False,
1709
+ region="global",
1710
+ ):
1711
+
1712
+ super().__init__(
1713
+ device,
1714
+ hadisd_mode,
1715
+ start_date,
1716
+ end_date,
1717
+ lead_time,
1718
+ era5_mode,
1719
+ res=res,
1720
+ filter_dates=filter_dates,
1721
+ diff=diff,
1722
+ )
1723
+
1724
+ # Setup
1725
+ self.var_start = var_start
1726
+ self.var_end = var_end
1727
+ self.diff = diff
1728
+ self.two_frames = two_frames
1729
+ self.region = region
1730
+ self.lead_time = lead_time
1731
+ self.mode = mode
1732
+ self.max_steps_per_epoch = max_steps_per_epoch
1733
+
1734
+ # Initialise encoder dataset
1735
+ self.assimilation_dataset = WeatherDatasetAssimilation(
1736
+ device="cuda",
1737
+ hadisd_mode="train",
1738
+ start_date=start_date,
1739
+ end_date=end_date,
1740
+ lead_time=0,
1741
+ era5_mode="4u",
1742
+ res=1,
1743
+ var_start=0,
1744
+ var_end=24,
1745
+ diff=False,
1746
+ two_frames=False,
1747
+ )
1748
+
1749
+ # Initialise forecast dataset
1750
+ self.forecast_dataset = ForecastLoader(
1751
+ device="cuda",
1752
+ mode=mode,
1753
+ lead_time=lead_time,
1754
+ era5_mode=era5_mode,
1755
+ res=1,
1756
+ frequency=6,
1757
+ diff=True,
1758
+ u_only=False,
1759
+ random_lt=False,
1760
+ )
1761
+
1762
+ # Initialise downscaling dataset
1763
+ self.downscaling_dataset = ForecasterDatasetDownscaling(
1764
+ start_date=start_date,
1765
+ end_date=end_date,
1766
+ lead_time=lead_time,
1767
+ hadisd_var=hadisd_var,
1768
+ mode=mode,
1769
+ device=device,
1770
+ forecast_path=None,
1771
+ region=region,
1772
+ )
1773
+
1774
+ def __len__(self):
1775
+ if self.max_steps_per_epoch:
1776
+ return self.max_steps_per_epoch
1777
+ return len(self.downscaling_dataset) - 40 # Need 10 day offset at end of year
1778
+
1779
+ def __getitem__(self, index):
1780
+
1781
+ if self.max_steps_per_epoch:
1782
+ index = np.random.choice(
1783
+ np.arange(len(self.downscaling_dataset) - 40)
1784
+ ) # Need 10 day offset at end of year
1785
+
1786
+ # Get data for each of the three datasets
1787
+ assimilation = self.assimilation_dataset.__getitem__(index)
1788
+ forecast = self.forecast_dataset.__getitem__(index)
1789
+ downscaling = self.downscaling_dataset.__getitem__(index)
1790
+
1791
+ # Create task
1792
+ task = {
1793
+ "assimilation": assimilation,
1794
+ "forecast": forecast,
1795
+ "downscaling": downscaling,
1796
+ "index": torch.tensor(index),
1797
+ }
1798
+
1799
+ # Add y target to allow for end to end finetuning if needed
1800
+ task["y_target"] = task["downscaling"]["y_target"]
1801
+
1802
+ return task
1803
+
1804
+ def unnorm_pred(self, x):
1805
+
1806
+ dev = x.device
1807
+ x = x.detach().cpu().numpy()
1808
+
1809
+ x = (
1810
+ x
1811
+ * self.stds[np.newaxis, ...].transpose(0, 2, 3, 1)[
1812
+ ..., self.var_start : self.var_end
1813
+ ]
1814
+ + self.means[np.newaxis, ...].transpose(0, 2, 3, 1)[
1815
+ ..., self.var_start : self.var_end
1816
+ ]
1817
+ )
1818
+ if bool(self.diff):
1819
+ x = (
1820
+ x
1821
+ + self.era5_mean_spatial[np.newaxis, ...].transpose(0, 3, 2, 1)[
1822
+ ..., self.var_start : self.var_end
1823
+ ]
1824
+ )
1825
+ return torch.from_numpy(x).float().to(dev)
official-src/aardvark/loader_utils_new.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import pandas as pd
4
+
5
+ LATLON_SCALE_FACTOR = 360
6
+ DAYS_IN_YEAR = 366
7
+
8
+ DAILY_SCALE_FACTOR = {
9
+ "ERA5": 4,
10
+ "HADISD": 4,
11
+ "IR": 4,
12
+ "SOUNDER": 1,
13
+ "ICOADS": 1,
14
+ }
15
+
16
+ date_list = [
17
+ "1979-01-01",
18
+ "1999-01-01",
19
+ "1999-01-02",
20
+ "2002-01-02",
21
+ "2007-01-01",
22
+ "2007-01-02",
23
+ "2007-01-03",
24
+ "2013-01-02",
25
+ "2014-01-01",
26
+ "2017-01-02",
27
+ "2018-01-01",
28
+ "2019-01-01",
29
+ "2020-01-02",
30
+ "2020-01-01",
31
+ "2021-01-02",
32
+ "2021-01-01",
33
+ ]
34
+
35
+
36
+ def generate_offsets(date_list, dates):
37
+ offsets = {}
38
+ for d in date_list:
39
+ try:
40
+ offsets[d] = np.where(dates == d)[0][0]
41
+ except:
42
+ offsets[d] = -1
43
+ return offsets
44
+
45
+
46
+ IC_OFFSETS = generate_offsets(
47
+ date_list, pd.date_range("1999-01-02", "2021-12-31 18:00", freq="6H")
48
+ )
49
+
50
+ AMSUA_OFFSETS = generate_offsets(
51
+ date_list, pd.date_range("2007-01-01", "2021-12-31 18:00", freq="6H")
52
+ )
53
+
54
+ AMSUB_OFFSETS = generate_offsets(
55
+ date_list, pd.date_range("2007-01-01", "2021-12-31 18:00", freq="6H")
56
+ )
57
+
58
+ ASCAT_OFFSETS = generate_offsets(
59
+ date_list, pd.date_range("2007-01-01", "2021-12-31", freq="6H")
60
+ )
61
+
62
+ ATMS_OFFSETS = generate_offsets(
63
+ date_list, pd.date_range("2013-01-02", "2021-12-31", freq="1D")
64
+ )
65
+
66
+ ICOADS_OFFSETS = generate_offsets(
67
+ date_list, pd.date_range("1999-01-01 06:00", "2021-12-31", freq="6H")
68
+ )
69
+
70
+ IGRA_OFFSETS = generate_offsets(
71
+ date_list, pd.date_range("1999-01-01 00:00", "2021-12-31 18:00", freq="6H")
72
+ )
73
+
74
+ SAT_OFFSETS = generate_offsets(
75
+ date_list, pd.date_range("1990-01-01 00:00", "2021-12-31 18:00", freq="6H")
76
+ )
77
+
78
+ HADISD_OFFSETS = generate_offsets(
79
+ date_list, pd.date_range("1950-01-01 00:00", "2021-12-31 18:00", freq="6H")
80
+ )
81
+
82
+
83
+ def lon_to_0_360(x):
84
+ return (x + 360) % 360
85
+
86
+
87
+ def lat_to_m90_90(x):
88
+ return torch.flip(x, [-1])
official-src/aardvark/loss_functions.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+
5
+
6
+ class RmseLoss(nn.Module):
7
+ """
8
+ RMSE loss
9
+ """
10
+
11
+ def __init__(self, start_ind=0, end_ind=24):
12
+
13
+ super().__init__()
14
+ self.start_ind = start_ind
15
+ self.end_ind = end_ind
16
+
17
+ def forward(
18
+ self,
19
+ target,
20
+ output,
21
+ prev_step_output,
22
+ fix_sigma=False,
23
+ unwrap=False,
24
+ expand=False,
25
+ ):
26
+
27
+ squared_diff = ((target.to(output.device) - output) ** 2)[
28
+ ..., self.start_ind : self.end_ind
29
+ ]
30
+ return torch.mean(torch.sqrt(torch.nanmean(squared_diff, dim=(1, 2, 3))))
31
+
32
+
33
+ class PressureWeightedRmseLoss(nn.Module):
34
+ """
35
+ Latitude weighted pressure weighted RMSE loss used in training the processor
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ res,
41
+ era5_mode,
42
+ data_dir,
43
+ aux_data_dir,
44
+ weight_per_variable=False,
45
+ ):
46
+ super().__init__()
47
+
48
+ self.weights = torch.from_numpy(
49
+ np.load(aux_data_dir + "lat_weights/weights_lat_{}.npy".format(res)).T[
50
+ np.newaxis, ..., np.newaxis
51
+ ]
52
+ ).float()
53
+
54
+ self.weight_per_variable = weight_per_variable
55
+ self.variable_weights = torch.from_numpy(
56
+ np.load(aux_data_dir + "loss_weights.npy")[
57
+ np.newaxis, np.newaxis, np.newaxis, :
58
+ ]
59
+ ).float()
60
+
61
+ self.pressure_levels = (
62
+ torch.from_numpy(
63
+ np.load(
64
+ data_dir + "era5/era5_pressure_levels_{}.npy".format(era5_mode)
65
+ )[np.newaxis, np.newaxis, np.newaxis, :]
66
+ ).float()
67
+ / 1000
68
+ )
69
+
70
+ def forward(
71
+ self,
72
+ target,
73
+ output,
74
+ prev_step_output,
75
+ fix_sigma=False,
76
+ unwrap=False,
77
+ expand=False,
78
+ ):
79
+
80
+ squared_diff = (target.to(output.device) - output) ** 2
81
+
82
+ if not expand:
83
+ weighted_sqared_diff = (
84
+ squared_diff
85
+ * self.weights.to(target.device)
86
+ * self.pressure_levels.to(target.device)
87
+ )
88
+ return torch.mean(
89
+ torch.sqrt(torch.nanmean(weighted_sqared_diff, dim=(1, 2, 3)))
90
+ )
91
+
92
+ weighted_sqared_diff = squared_diff * self.weights.to(target.device)
93
+
94
+ if self.weight_per_variable:
95
+ weighted_sqared_diff = weighted_sqared_diff * self.variable_weights.to(
96
+ weighted_sqared_diff.device
97
+ )
98
+
99
+ return torch.mean(
100
+ torch.sqrt(torch.nanmean(weighted_sqared_diff, dim=(1, 2))), dim=0
101
+ )
102
+
103
+
104
+ class WeightedRmseLoss(nn.Module):
105
+ """
106
+ Latitude weighted RMSE loss
107
+ """
108
+
109
+ def __init__(
110
+ self,
111
+ res,
112
+ data_dir,
113
+ aux_data_dir,
114
+ weight_per_variable=False,
115
+ start_ind=0,
116
+ end_ind=24,
117
+ ):
118
+
119
+ super().__init__()
120
+ self.start_ind = start_ind
121
+ self.end_ind = end_ind
122
+
123
+ self.weights = torch.from_numpy(
124
+ np.load(aux_data_dir + "lat_weights/weights_lat_{}.npy".format(res)).T[
125
+ np.newaxis, ..., np.newaxis
126
+ ]
127
+ ).float()
128
+
129
+ self.weight_per_variable = weight_per_variable
130
+ self.variable_weights = torch.from_numpy(
131
+ np.load(aux_data_dir + "loss_weights.npy")[
132
+ np.newaxis, np.newaxis, np.newaxis, start_ind:end_ind
133
+ ]
134
+ ).float()
135
+
136
+ def forward(
137
+ self,
138
+ target,
139
+ output,
140
+ prev_step_output,
141
+ fix_sigma=False,
142
+ unwrap=False,
143
+ expand=False,
144
+ ):
145
+ squared_diff = (target.to(output.device) - output) ** 2
146
+
147
+ if not expand:
148
+ weighted_sqared_diff = squared_diff * self.weights.to(target.device)
149
+
150
+ if self.weight_per_variable:
151
+ weighted_sqared_diff = weighted_sqared_diff * self.variable_weights.to(
152
+ weighted_sqared_diff.device
153
+ )
154
+
155
+ x = torch.nanmean(
156
+ torch.sqrt(torch.nanmean(weighted_sqared_diff, dim=(1, 2, 3)))
157
+ )
158
+
159
+ return x
160
+
161
+ weighted_sqared_diff = squared_diff * self.weights.to(target.device)
162
+ x = torch.mean(
163
+ torch.sqrt(torch.nanmean(weighted_sqared_diff, dim=(1, 2))), dim=0
164
+ )
165
+
166
+ return x
official-src/aardvark/misc_downscaling_functionality.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pickle
2
+
3
+ import torch
4
+ import numpy as np
5
+ import torch.nn as nn
6
+
7
+ from set_convs import convDeepSet
8
+ from unet_wrap_padding import Unet
9
+ from vit import *
10
+ from models import *
11
+
12
+ hadisd_publisher_shifts = {
13
+ "tas": 273.15,
14
+ "u": 0.0,
15
+ "v": 0.0,
16
+ "psl": 0.0,
17
+ "ws": 0.0,
18
+ }
19
+
20
+ hadisd_publisher_scales = {
21
+ "tas": 10,
22
+ "u": 10,
23
+ "v": 10,
24
+ "psl": 100,
25
+ "ws": 10.0,
26
+ }
27
+
28
+
29
+ def hadisd_normalisation_factors(var: str):
30
+ path = "/home/azureuser/aux_data/norm_factors/"
31
+ return {
32
+ "mean": np.load(path + f"mean_hadisd_{var}.npy"),
33
+ "std": np.load(path + f"std_hadisd_{var}.npy"),
34
+ }
35
+
36
+
37
+ def unnormalise_hadisd_var(x, var):
38
+ factors = hadisd_normalisation_factors(var)
39
+ hadisd_shift = hadisd_publisher_shifts[var]
40
+ hadisd_scale = hadisd_publisher_scales[var]
41
+
42
+ return hadisd_shift + hadisd_scale * (factors["mean"] + factors["std"] * x)
43
+
44
+
45
+ class DownscalingRmseLoss(nn.Module):
46
+ def __init__(self):
47
+ super().__init__()
48
+
49
+ def forward(self, target, output, prev_step, fix_sigma=None, expand=True):
50
+
51
+ target = torch.flatten(target.to(output.device))
52
+ output = torch.flatten(output)
53
+
54
+ tmp = torch.isnan(target)
55
+ clean_target = target[~tmp]
56
+ clean_output = output[~tmp]
57
+
58
+ return torch.mean((clean_target - clean_output) ** 2)
59
+
60
+
61
+ class ConvCNPWeatherOnToOff(nn.Module):
62
+ """
63
+ ConvCNP for decoder
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ in_channels,
69
+ out_channels,
70
+ int_channels,
71
+ device,
72
+ res,
73
+ data_path="../data/",
74
+ mode="end_to_end",
75
+ decoder=None,
76
+ film=False,
77
+ ):
78
+
79
+ super().__init__()
80
+
81
+ # Setup
82
+ self.device = device
83
+
84
+ self.in_channels = in_channels
85
+ self.out_channels = out_channels
86
+ self.int_channels = int_channels
87
+ self.decoder = decoder
88
+ self.int_x = 256
89
+ self.int_y = 128
90
+ self.mode = mode
91
+ self.film = film
92
+
93
+ # Load lon-lat of internal discretisation
94
+ self.era5_x = (
95
+ torch.from_numpy(
96
+ np.load(data_path + "grid_lon_lat/era5_x_{}.npy".format(res))
97
+ ).float()
98
+ / 360
99
+ )
100
+ self.era5_y = (
101
+ torch.from_numpy(
102
+ np.load(data_path + "grid_lon_lat/era5_y_{}.npy".format(res))
103
+ ).float()
104
+ / 360
105
+ )
106
+
107
+ # Setup setconv
108
+ self.sc_out = convDeepSet(
109
+ 0.001, "OnToOff", density_channel=False, device=self.device
110
+ )
111
+
112
+ if self.mode not in ["downscaling", "end_to_end"]:
113
+ unet_out_channels = out_channels
114
+ else:
115
+ unet_out_channels = int_channels
116
+
117
+ # UNet backbone
118
+ if self.decoder == "base":
119
+ self.decoder_lr = Unet(
120
+ in_channels=in_channels,
121
+ out_channels=unet_out_channels,
122
+ div_factor=1,
123
+ film=film,
124
+ )
125
+
126
+ else:
127
+ raise Exception(f"Expected to use base decoder, but got {self.decoder}")
128
+
129
+ # Postprocessing MLP
130
+ self.mlp = DownscalingMLP(
131
+ in_channels=24 + 9,
132
+ out_channels=1,
133
+ h_channels=64,
134
+ h_layers=2,
135
+ )
136
+
137
+ def forward(self, task, film_index):
138
+
139
+ x = task["y_context"]
140
+ batch_size = x.shape[0]
141
+
142
+ # UNet backbone
143
+ x = self.decoder_lr(x, film_index=task["lt"])
144
+
145
+ # Transform to station predictions with setconv
146
+ num_channels = x.shape[3]
147
+ x = x.permute(0, 3, 1, 2)
148
+ assert list(x.shape) == [batch_size, num_channels, 240, 121]
149
+ x_target = task["x_target"]
150
+ num_stations = x_target.shape[2]
151
+
152
+ x = self.sc_out(
153
+ x_in=task["x_context"],
154
+ wt=x,
155
+ x_out=[x_target[:, 0, :], x_target[:, 1, :]],
156
+ )
157
+ assert x.shape[0] == batch_size
158
+ assert x.shape[2] == num_stations
159
+
160
+ # Concatenate auxiliary data at stations
161
+ alt_target = task["alt_target"]
162
+ assert torch.isnan(alt_target).sum() == 0
163
+ assert alt_target.shape[0] == batch_size
164
+ assert alt_target.shape[2] == num_stations
165
+
166
+ aux_time = task["aux_time"].squeeze(-1).repeat(1, 1, num_stations)
167
+ assert aux_time.shape[0] == batch_size
168
+ assert aux_time.shape[2] == num_stations
169
+
170
+ x = torch.cat([x, alt_target, x_target, aux_time], dim=1).permute(0, 2, 1)
171
+ assert x.shape[0] == batch_size
172
+ assert x.shape[1] == num_stations
173
+
174
+ tmp = self.mlp(x)
175
+ assert list(tmp.shape) == [batch_size, num_stations, 1]
176
+ y_hat = tmp.squeeze(-1)
177
+ assert list(y_hat.shape) == [batch_size, num_stations]
178
+ return y_hat
179
+
180
+
181
+ class ResidualBlock(nn.Module):
182
+ def __init__(self, n_channels):
183
+ super().__init__()
184
+ self.block = nn.Sequential(nn.Linear(n_channels, n_channels), nn.ReLU())
185
+
186
+ def forward(self, x):
187
+ return self.block(x) + x
188
+
189
+
190
+ class DownscalingMLP(nn.Module):
191
+ """
192
+ MLP for handling auxiliary data at station locations
193
+ """
194
+
195
+ def __init__(self, in_channels, out_channels, h_channels, h_layers):
196
+ super().__init__()
197
+
198
+ self.mlp = nn.Sequential(
199
+ nn.Linear(in_channels, h_channels),
200
+ *[ResidualBlock(h_channels) for _ in range(h_layers)],
201
+ nn.Linear(h_channels, out_channels),
202
+ )
203
+
204
+ def forward(self, x):
205
+ return self.mlp(x)
206
+
207
+
208
+ def find_epoch(results_dir: str) -> int:
209
+ losses = np.load(results_dir + "losses_0.npy")
210
+ epoch = np.argmin(losses)
211
+
212
+ return epoch
213
+
214
+
215
+ def load_config(results_dir: str) -> dict:
216
+ with open(results_dir + "config.pkl", "rb") as f:
217
+ return pickle.load(f)
218
+
219
+
220
+ def load_model(results_dir, device, config_dir=None, epoch=None):
221
+ """
222
+ Load the ConvCNPWeatherOnToOff to generate forecasts
223
+ """
224
+
225
+ config_dir = config_dir or results_dir
226
+ config = load_config(config_dir)
227
+ epoch = epoch or find_epoch(results_dir)
228
+
229
+ model = ConvCNPWeatherOnToOff(
230
+ in_channels=config["in_channels"],
231
+ out_channels=config["out_channels"],
232
+ int_channels=config["int_channels"],
233
+ device=device,
234
+ res=config["res"],
235
+ decoder=config["decoder"],
236
+ mode=config["mode"],
237
+ film=False,
238
+ )
239
+
240
+ full_state_dict = torch.load(results_dir + f"epoch_{epoch}", map_location=device)
241
+ state_dict = full_state_dict["model_state_dict"]
242
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
243
+ model.load_state_dict(state_dict)
244
+ model = model.to(device)
245
+ model.eval()
246
+
247
+ return model
248
+
249
+
250
+ def load_model_e2e(results_dir, lead_time, device):
251
+ """
252
+ Load the full E2E model to generate forecasts
253
+ """
254
+
255
+ config = load_config(results_dir)
256
+ epoch = find_epoch(results_dir)
257
+
258
+ model = ConvCNPWeatherE2E(
259
+ device="cuda",
260
+ lead_time=lead_time,
261
+ se_model_path="../results/assimilation/all_v4_vit_ps3",
262
+ forecast_model_path="../results/forecast/vit_fnl_randomlt_mlp",
263
+ sf_model_path="../results/downscaling_NEW",
264
+ )
265
+
266
+ full_state_dict = torch.load(results_dir + f"epoch_{epoch}", map_location=device)
267
+ state_dict = full_state_dict["model_state_dict"]
268
+ state_dict = {k[7:]: v for k, v in zip(state_dict.keys(), state_dict.values())}
269
+ model.load_state_dict(state_dict)
270
+ model = model.to(device)
271
+ model.eval()
272
+
273
+ return model
official-src/aardvark/models.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+
7
+ from architectures import MLP
8
+ from set_convs import convDeepSet
9
+ from unet_wrap_padding import *
10
+ from vit import *
11
+
12
+ sys.path.append("../")
13
+
14
+
15
+ class ConvCNPWeather(nn.Module):
16
+ """
17
+ ConvCNP class used for the encoder and processor modules
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ in_channels,
23
+ out_channels,
24
+ int_channels,
25
+ device,
26
+ res,
27
+ data_path="../data/",
28
+ gnp=False,
29
+ mode="assimilation",
30
+ decoder=None,
31
+ film=False,
32
+ two_frames=False,
33
+ ):
34
+
35
+ super().__init__()
36
+
37
+ self.device = device
38
+
39
+ self.in_channels = in_channels
40
+ self.out_channels = out_channels
41
+ self.int_channels = int_channels
42
+ self.decoder = decoder
43
+ self.int_x = 256
44
+ self.int_y = 128
45
+ self.data_path = data_path
46
+ self.mode = mode
47
+ self.film = film
48
+ self.two_frames = two_frames
49
+
50
+ N_SAT_VARS = 2
51
+ N_ICOADS_VARS = 5
52
+ N_HADISD_VARS = 5
53
+
54
+ # Load internal grid longitude-latitude locations
55
+ self.era5_x = (
56
+ torch.from_numpy(
57
+ np.load(self.data_path + "grid_lon_lat/era5_x_{}.npy".format(res))
58
+ ).float()
59
+ / 360
60
+ )
61
+ self.era5_y = (
62
+ torch.from_numpy(
63
+ np.load(self.data_path + "grid_lon_lat/era5_y_{}.npy".format(res))
64
+ ).float()
65
+ / 360
66
+ )
67
+
68
+ self.int_grid = [
69
+ (torch.linspace(0, 360, 240) / 360).float().cuda(),
70
+ (torch.linspace(-90, 90, 121) / 360).float().cuda(),
71
+ ]
72
+
73
+ self.int_grid = [self.int_grid[0].unsqueeze(0), self.int_grid[1].unsqueeze(0)]
74
+
75
+ # Create input setconvs for each data modality
76
+ self.ascat_setconvs = convDeepSet(
77
+ 0.001, "OnToOn", density_channel=True, device=self.device
78
+ )
79
+ self.amsua_setconvs = [
80
+ convDeepSet(0.001, "OnToOn", density_channel=True, device=self.device)
81
+ for _ in range(13)
82
+ ]
83
+ self.amsub_setconvs = [
84
+ convDeepSet(0.001, "OnToOn", density_channel=True, device=self.device)
85
+ for _ in range(12)
86
+ ]
87
+ self.hirs_setconvs = [
88
+ convDeepSet(0.001, "OnToOn", density_channel=True, device=self.device)
89
+ for _ in range(26)
90
+ ]
91
+
92
+ self.sat_setconvs = [
93
+ convDeepSet(0.001, "OnToOn", density_channel=True, device=self.device)
94
+ for _ in range(N_SAT_VARS)
95
+ ]
96
+ self.hadisd_setconvs = [
97
+ convDeepSet(0.001, "OffToOn", density_channel=True, device=self.device)
98
+ for _ in range(N_HADISD_VARS)
99
+ ]
100
+ self.icoads_setconvs = [
101
+ convDeepSet(0.001, "OffToOn", density_channel=True, device=self.device)
102
+ for _ in range(N_ICOADS_VARS)
103
+ ]
104
+ self.igra_setconvs = [
105
+ convDeepSet(0.001, "OffToOn", density_channel=True, device=self.device)
106
+ for _ in range(24)
107
+ ]
108
+
109
+ self.sc_out = convDeepSet(
110
+ 0.001, "OnToOff", density_channel=False, device=self.device
111
+ )
112
+
113
+ # Instantiate the decoder. Here decoder refers to decoder in a convCNP (i.e the ViT backbone)
114
+ if self.decoder == "vit":
115
+ self.decoder_lr = ViT(
116
+ in_channels=in_channels,
117
+ out_channels=out_channels,
118
+ h_channels=512,
119
+ depth=16,
120
+ patch_size=5,
121
+ per_var_embedding=True,
122
+ img_size=[240, 121],
123
+ )
124
+
125
+ elif self.decoder == "vit_assimilation":
126
+ self.decoder_lr = ViT(
127
+ in_channels=256,
128
+ out_channels=out_channels,
129
+ h_channels=512,
130
+ depth=8,
131
+ patch_size=3,
132
+ per_var_embedding=False,
133
+ img_size=[256, 128],
134
+ )
135
+
136
+ self.mlp = MLP(
137
+ in_channels=out_channels,
138
+ out_channels=out_channels,
139
+ h_channels=128,
140
+ h_layers=4,
141
+ )
142
+ self.break_next = False
143
+
144
+ def encoder_hadisd(self, task, prefix):
145
+ """
146
+ Data preprocessing for HadISD
147
+ """
148
+
149
+ encodings = []
150
+ for channel in range(4):
151
+ encodings.append(
152
+ self.hadisd_setconvs[channel](
153
+ x_in=[
154
+ task["x_context_hadisd_{}".format(prefix)][channel][:, 0, :],
155
+ task["x_context_hadisd_{}".format(prefix)][channel][:, 1, :],
156
+ ],
157
+ wt=task["y_context_hadisd_{}".format(prefix)][channel].unsqueeze(1),
158
+ x_out=self.int_grid,
159
+ )
160
+ )
161
+ encodings = torch.cat(encodings, dim=1)
162
+ return encodings
163
+
164
+ def encoder_sat(self, task, prefix):
165
+ """
166
+ Data preprocessing for Gridsat
167
+ """
168
+
169
+ encodings = []
170
+ for channel in range(task["sat_{}".format(prefix)].shape[1]):
171
+ encodings.append(
172
+ self.sat_setconvs[channel](
173
+ x_in=task["sat_x_{}".format(prefix)],
174
+ wt=task["sat_{}".format(prefix)][:, channel : channel + 1, ...],
175
+ x_out=self.int_grid,
176
+ )
177
+ )
178
+ encodings = torch.cat(encodings, dim=1)
179
+ return encodings
180
+
181
+ def encoder_icoads(self, task, prefix):
182
+ """
183
+ Data preprocessing for ICOADS
184
+ """
185
+
186
+ encodings = []
187
+ for channel in range(5):
188
+ encodings.append(
189
+ self.icoads_setconvs[channel](
190
+ x_in=task["icoads_x_{}".format(prefix)],
191
+ wt=task["icoads_{}".format(prefix)][:, channel, :].unsqueeze(1),
192
+ x_out=self.int_grid,
193
+ )
194
+ )
195
+ encodings = torch.cat(encodings, dim=1)
196
+
197
+ return encodings
198
+
199
+ def encoder_amsua(self, task, prefix):
200
+ """
201
+ Data preprocessing for AMSU-A
202
+ """
203
+
204
+ encodings = []
205
+ task["amsua_{}".format(prefix)][..., -1] = np.nan
206
+ task["amsua_{}".format(prefix)][task["amsua_{}".format(prefix)] == 0] = np.nan
207
+ for i in range(13):
208
+ encodings.append(
209
+ self.amsua_setconvs[i](
210
+ x_in=task["amsua_x_{}".format(prefix)],
211
+ wt=task["amsua_{}".format(prefix)].permute(0, 3, 2, 1)[
212
+ :, i : i + 1, ...
213
+ ],
214
+ x_out=self.int_grid,
215
+ )
216
+ )
217
+
218
+ encodings = torch.cat(encodings, dim=1)
219
+ return encodings
220
+
221
+ def encoder_amsub(self, task, prefix):
222
+ """
223
+ Data preprocessing for AMSU-B
224
+ """
225
+
226
+ encodings = []
227
+ task["amsub_{}".format(prefix)][task["amsub_{}".format(prefix)] == 0] = np.nan
228
+ for i in range(12):
229
+ encodings.append(
230
+ self.amsua_setconvs[i](
231
+ x_in=task["amsub_x_{}".format(prefix)],
232
+ wt=task["amsub_{}".format(prefix)].permute(0, 3, 1, 2)[
233
+ :, i : i + 1, ...
234
+ ],
235
+ x_out=self.int_grid,
236
+ )
237
+ )
238
+
239
+ encodings = torch.cat(encodings, dim=1)
240
+ return encodings
241
+
242
+ def encoder_hirs(self, task, prefix):
243
+ """
244
+ Data preprocessing for HIRS
245
+ """
246
+
247
+ encodings = []
248
+
249
+ task["hirs_{}".format(prefix)][task["hirs_{}".format(prefix)] == 0] = np.nan
250
+ for i in range(26):
251
+ encodings.append(
252
+ self.hirs_setconvs[i](
253
+ x_in=task["hirs_x_{}".format(prefix)],
254
+ wt=task["hirs_{}".format(prefix)].permute(0, 3, 1, 2)[
255
+ :, i : i + 1, ...
256
+ ],
257
+ x_out=self.int_grid,
258
+ )
259
+ )
260
+
261
+ encodings = torch.cat(encodings, dim=1)
262
+ return encodings
263
+
264
+ def encoder_igra(self, task, prefix):
265
+ """
266
+ Data preprocessing for IGRA
267
+ """
268
+
269
+ encodings = []
270
+ for channel in range(24):
271
+ encodings.append(
272
+ self.igra_setconvs[channel](
273
+ x_in=task["igra_x_{}".format(prefix)],
274
+ wt=task["igra_{}".format(prefix)][:, channel, :].unsqueeze(1),
275
+ x_out=self.int_grid,
276
+ )
277
+ )
278
+ encodings = torch.cat(encodings, dim=1)
279
+
280
+ return encodings
281
+
282
+ def encoder_ascat(self, task, prefix):
283
+ """
284
+ Data preprocessing for ASCAT
285
+ """
286
+
287
+ task["ascat_{}".format(prefix)][
288
+ torch.isnan(task["ascat_{}".format(prefix)])
289
+ ] = 0
290
+ e = nn.functional.interpolate(
291
+ task["ascat_{}".format(prefix)].permute(0, 3, 1, 2), size=(240, 121)
292
+ )
293
+ e = torch.flip(e, dims=[-1])
294
+ return e
295
+
296
+ def encoder_iasi(self, task, prefix):
297
+ """
298
+ Data preprocessing for IASI
299
+ """
300
+
301
+ task["iasi_{}".format(prefix)][torch.isnan(task["iasi_{}".format(prefix)])] = 0
302
+ e = nn.functional.interpolate(
303
+ task["iasi_{}".format(prefix)].permute(0, 3, 1, 2), size=(240, 121)
304
+ )
305
+ e = torch.flip(e, dims=[-1])
306
+ return e
307
+
308
+ def forward(self, task, film_index):
309
+
310
+ # Setup input
311
+ if self.mode == "assimilation":
312
+
313
+ self.int_grid = [i.to(task["y_target"].device) for i in self.int_grid]
314
+ elev = nn.functional.interpolate(
315
+ torch.flip(task["era5_elev_current"].permute(0, 1, 3, 2), dims=[2]),
316
+ size=(self.int_grid[0].shape[1], self.int_grid[1].shape[1]),
317
+ )
318
+ elev = torch.flip(task["era5_elev_current"].permute(0, 1, 3, 2), dims=[2])
319
+
320
+ if not self.two_frames:
321
+ encodings = [
322
+ self.encoder_iasi(task, "current"),
323
+ self.encoder_ascat(task, "current"),
324
+ self.encoder_hadisd(task, "current"),
325
+ self.encoder_icoads(task, "current"),
326
+ self.encoder_sat(task, "current"),
327
+ self.encoder_amsua(task, "current"),
328
+ self.encoder_amsub(task, "current"),
329
+ self.encoder_igra(task, "current"),
330
+ self.encoder_hirs(task, "current"),
331
+ elev,
332
+ task["climatology_current"],
333
+ torch.ones_like(elev[:, :5, ...])
334
+ * task["aux_time_current"].unsqueeze(-1).unsqueeze(-1),
335
+ ]
336
+ else:
337
+ # Option to pass two timesteps (t=-1 and t=0) as input
338
+ encodings = [
339
+ self.encoder_iasi(task, "current"),
340
+ self.encoder_ascat(task, "current"),
341
+ self.encoder_hadisd(task, "current"),
342
+ self.encoder_icoads(task, "current"),
343
+ self.encoder_sat(task, "current"),
344
+ self.encoder_amsua(task, "current"),
345
+ self.encoder_amsub(task, "current"),
346
+ self.encoder_igra(task, "current"),
347
+ self.encoder_hirs(task, "current"),
348
+ self.encoder_iasi(task, "prev"),
349
+ self.encoder_ascat(task, "prev"),
350
+ self.encoder_hadisd(task, "prev"),
351
+ self.encoder_icoads(task, "prev"),
352
+ self.encoder_sat(task, "prev"),
353
+ self.encoder_amsua(task, "prev"),
354
+ self.encoder_amsub(task, "prev"),
355
+ self.encoder_igra(task, "prev"),
356
+ self.encoder_hirs(task, "prev"),
357
+ elev,
358
+ task["climatology_current"],
359
+ torch.ones_like(elev[:, :5, ...])
360
+ * task["aux_time_current"].unsqueeze(-1).unsqueeze(-1),
361
+ ]
362
+ x = torch.cat(encodings, dim=1)
363
+
364
+ else:
365
+ x = task["y_context"]
366
+
367
+ if x.shape[-1] > x.shape[-2]:
368
+ x = x.permute(0, 1, 3, 2)
369
+
370
+ # Run ViT backbone
371
+ if self.decoder == "vit":
372
+ x = self.decoder_lr(x, lead_times=task["lt"])
373
+ x = x.permute(0, 3, 1, 2)
374
+ else:
375
+ x = nn.functional.interpolate(x, size=(256, 128))
376
+ x = self.decoder_lr(x, film_index=(task["lt"] * 0) + 1)
377
+
378
+ # Process outputs
379
+
380
+ if np.logical_and(
381
+ self.mode == "assimilation", self.decoder == "vit_assimilation"
382
+ ):
383
+ x = nn.functional.interpolate(x.permute(0, 3, 1, 2), size=(240, 121))
384
+ return x.permute(0, 3, 2, 1)
385
+
386
+ elif self.mode == "forecast":
387
+ x = nn.functional.interpolate(x, size=(240, 121)).permute(0, 2, 3, 1)
388
+ return x.permute(0, 2, 1, 3)
389
+
390
+ return x
official-src/aardvark/set_convs.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ class convDeepSet(nn.Module):
6
+ """
7
+ ConvDeepSet used to translate ungridded observations to a gridded representation and
8
+ vice versa.
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ init_ls,
14
+ mode,
15
+ device,
16
+ density_channel=True,
17
+ step=0.25,
18
+ grid=False,
19
+ ):
20
+ super().__init__()
21
+ self.init_ls = torch.nn.Parameter(torch.tensor([init_ls]))
22
+ self.grid = grid
23
+ self.step = step
24
+ self.density_channel = density_channel
25
+ self.mode = mode
26
+
27
+ self.init_ls.requires_grad = True
28
+ self.device = device
29
+
30
+ def compute_weights(self, x1, x2):
31
+
32
+ dists2 = self.pw_dists2(x1.unsqueeze(-1), x2.unsqueeze(-1))
33
+
34
+ d = torch.exp((-0.5 * dists2) / (self.init_ls.to(x1.device)) ** 2)
35
+ return d
36
+
37
+ def pw_dists2(self, a, b):
38
+
39
+ norms_a = torch.sum(a**2, axis=-1)[..., :, None]
40
+ norms_b = torch.sum(b**2, axis=-1)[..., None, :]
41
+
42
+ return norms_a + norms_b - 2 * torch.matmul(a, b.permute(0, 2, 1))
43
+
44
+ def forward(self, x_in, wt, x_out):
45
+
46
+ # Add a density channel
47
+ density_channel = torch.ones_like(wt[:, 0:1, ...])
48
+ density_channel[torch.isnan(wt[:, 0:1, ...])] = 0
49
+
50
+ wt = torch.cat([density_channel, wt], dim=1)
51
+ wt[torch.isnan(wt)] = 0
52
+
53
+ if self.mode == "OffToOn":
54
+ # Case 1: converting off-the-grid data to a gridded representation
55
+
56
+ in_lon_mask = ~torch.isnan(x_in[0])
57
+ in_lat_mask = ~torch.isnan(x_in[1])
58
+
59
+ x_in[0][~in_lon_mask] = 0
60
+ x_in[1][~in_lat_mask] = 0
61
+
62
+ ws = [self.compute_weights(xzi, xi) for xzi, xi in zip(x_in, x_out)]
63
+
64
+ ws[0] = ws[0] * in_lon_mask.unsqueeze(-1).int()
65
+ ws[1] = ws[1] * in_lat_mask.unsqueeze(-1).int()
66
+
67
+ ee = torch.einsum("...cw,...wx,...wy->...cxy", wt, ws[0], ws[1])
68
+
69
+ elif self.mode == "OnToOn":
70
+ # Case 2: converting between gridded representations
71
+
72
+ ws = [self.compute_weights(xzi, xi) for xzi, xi in zip(x_in, x_out)]
73
+ ee = torch.einsum("...cwh,...wx,...hy->...cxy", wt, ws[0], ws[1])
74
+
75
+ elif self.mode == "OnToOff":
76
+
77
+ # Case 3: converting a gridded representation to off-the-grid predictions
78
+ out_lon_mask = ~torch.isnan(x_out[0])
79
+ out_lat_mask = ~torch.isnan(x_out[1])
80
+ x_out[0][~out_lon_mask] = 0
81
+ x_out[1][~out_lat_mask] = 0
82
+
83
+ ws = [self.compute_weights(xzi, xi) for xzi, xi in zip(x_in, x_out)]
84
+
85
+ ws[0] = ws[0] * out_lon_mask.unsqueeze(-2).int()
86
+ ws[1] = ws[1] * out_lat_mask.unsqueeze(-2).int()
87
+
88
+ ee = torch.einsum("...cwh,...wx,...hx->...cx", wt, ws[0], ws[1])
89
+ if self.density_channel:
90
+ ee = torch.cat(
91
+ [
92
+ ee[:, 0:1, ...],
93
+ ee[:, 1:, ...] / torch.clamp(ee[:, 0:1, ...], min=1e-6, max=1e5),
94
+ ],
95
+ dim=1,
96
+ )
97
+
98
+ return ee
99
+ else:
100
+ ee = ee[:, 1:, ...] / torch.clamp(ee[:, 0:1, ...], min=1e-6, max=1e5)
101
+ return ee
official-src/aardvark/train_module.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NB: this script is for illustration purposes only and is not runnable as our
3
+ full dataset is not provided as part of the submission, due to size constraints.
4
+ Many of the relevant paths to the data have been thus replaced by dummy paths.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import pickle
10
+ import argparse
11
+
12
+ import torch
13
+ import torch.multiprocessing as mp
14
+ from torch.utils.data import DataLoader
15
+ import torch.utils.data.distributed
16
+ from torch.utils.data.distributed import DistributedSampler
17
+ from torch.distributed import init_process_group, destroy_process_group
18
+
19
+
20
+ from trainer import DDPTrainer
21
+ from loss_functions import WeightedRmseLoss, PressureWeightedRmseLoss, RmseLoss
22
+ from misc_downscaling_functionality import ConvCNPWeatherOnToOff, DownscalingRmseLoss
23
+ from loader import *
24
+ from models import *
25
+ from unet_wrap_padding import *
26
+
27
+
28
+ sys.path.append("../npw/data")
29
+ torch.set_float32_matmul_precision("medium")
30
+
31
+
32
+ def ddp_setup(rank, world_size, master_port):
33
+ """
34
+ Args:
35
+ rank: Unique identifier of each process
36
+ world_size: Total number of processes
37
+ """
38
+
39
+ os.environ["MASTER_ADDR"] = "localhost"
40
+ os.environ["MASTER_PORT"] = master_port
41
+ init_process_group(backend="nccl", rank=rank, world_size=world_size)
42
+ torch.cuda.set_device(rank)
43
+
44
+
45
+ def start_date(name):
46
+ if name == "train":
47
+ return "2007-01-02"
48
+ elif name == "val":
49
+ return "2019-01-01"
50
+ elif name == "test":
51
+ return "2018-01-01"
52
+ else:
53
+ raise Exception(f"Unrecognised split name {name}")
54
+
55
+
56
+ def end_date(name):
57
+ if name == "train":
58
+ return "2017-12-31"
59
+ elif name == "val":
60
+ return "2019-11-01"
61
+ elif name == "test":
62
+ return "2018-12-21"
63
+ else:
64
+ raise Exception(f"Unrecognised split name {name}")
65
+
66
+
67
+ def main(rank, world_size, output_dir, args):
68
+ """
69
+ Primary training script for the encoder, processor and decoder modules.
70
+ """
71
+
72
+ master_port = args.master_port
73
+ lead_time = args.lead_time
74
+ era5_mode = args.era5_mode
75
+ weights_dir = args.weights_dir
76
+ ddp_setup(rank, world_size, master_port)
77
+
78
+ # Instantiate loss function
79
+ if args.loss == "lw_rmse":
80
+ lf = WeightedRmseLoss(
81
+ args.res,
82
+ start_ind=args.start_ind,
83
+ end_ind=args.end_ind,
84
+ weight_per_variable=bool(args.weight_per_variable),
85
+ )
86
+ elif args.loss == "lw_rmse_pressure_weighted":
87
+ lf = PressureWeightedRmseLoss(args.res, era5_mode)
88
+ elif args.loss == "rmse":
89
+ lf = RmseLoss()
90
+ elif args.loss == "downscaling_rmse":
91
+ lf = DownscalingRmseLoss()
92
+
93
+ # Setup datasets
94
+
95
+ # Case 1: training encoder
96
+ if args.mode == "assimilation":
97
+ train_dataset = WeatherDatasetAssimilation(
98
+ device="cuda",
99
+ hadisd_mode="train",
100
+ start_date="2007-01-02",
101
+ end_date="2017-12-31",
102
+ lead_time=0,
103
+ era5_mode="4u",
104
+ res=args.res,
105
+ var_start=args.start_ind,
106
+ var_end=args.end_ind,
107
+ diff=bool(args.diff),
108
+ two_frames=bool(args.two_frames),
109
+ )
110
+ val_dataset = WeatherDatasetAssimilation(
111
+ device="cuda",
112
+ hadisd_mode="train",
113
+ start_date="2019-01-01",
114
+ end_date="2019-12-31",
115
+ lead_time=0,
116
+ era5_mode="4u",
117
+ res=args.res,
118
+ var_start=args.start_ind,
119
+ var_end=args.end_ind,
120
+ diff=bool(args.diff),
121
+ two_frames=bool(args.two_frames),
122
+ )
123
+
124
+ # Case 2: training processor
125
+ elif args.mode == "forecast":
126
+ if args.ic == "aardvark":
127
+ train_dataset = FineTuneForecastLoaderNew(
128
+ device="cuda",
129
+ mode="train",
130
+ lead_time=lead_time,
131
+ era5_mode=era5_mode,
132
+ res=args.res,
133
+ frequency=args.frequency,
134
+ diff=bool(args.diff),
135
+ aardvark_ic_path=args.aardvark_ic_path,
136
+ random_lt=True,
137
+ )
138
+ val_dataset = FineTuneForecastLoaderNew(
139
+ device="cuda",
140
+ mode="val",
141
+ lead_time=lead_time,
142
+ era5_mode=era5_mode,
143
+ res=args.res,
144
+ frequency=args.frequency,
145
+ diff=bool(args.diff),
146
+ aardvark_ic_path=args.aardvark_ic_path,
147
+ )
148
+ else:
149
+ train_dataset = ForecastLoader(
150
+ device="cuda",
151
+ mode="train",
152
+ lead_time=lead_time,
153
+ era5_mode=era5_mode,
154
+ res=args.res,
155
+ frequency=args.frequency,
156
+ diff=bool(args.diff),
157
+ u_only=False,
158
+ random_lt=False,
159
+ )
160
+ val_dataset = ForecastLoader(
161
+ device="cuda",
162
+ mode="val",
163
+ lead_time=lead_time,
164
+ era5_mode=era5_mode,
165
+ res=args.res,
166
+ frequency=args.frequency,
167
+ diff=bool(args.diff),
168
+ u_only=False,
169
+ random_lt=False,
170
+ )
171
+
172
+ # Case 3: training decoder
173
+ elif args.mode == "downscaling":
174
+
175
+ train_dataset = ForecasterDatasetDownscaling(
176
+ start_date="2007-01-02",
177
+ end_date="2017-12-31",
178
+ lead_time=args.lead_time,
179
+ hadisd_var=args.var,
180
+ mode="train",
181
+ device="cuda",
182
+ forecast_path=None,
183
+ )
184
+
185
+ val_dataset = ForecasterDatasetDownscaling(
186
+ start_date="2019-01-01",
187
+ end_date="2019-12-21",
188
+ lead_time=args.lead_time,
189
+ hadisd_var=args.var,
190
+ mode="train",
191
+ device="cuda",
192
+ forecast_path=None,
193
+ )
194
+
195
+ try:
196
+ os.mkdir(f"{output_dir}lt_{args.lead_time}")
197
+ except FileExistsError:
198
+ pass
199
+
200
+ output_dir = f"{output_dir}lt_{args.lead_time}/"
201
+
202
+ # Instantiate model
203
+
204
+ if args.mode == "downscaling":
205
+ model = ConvCNPWeatherOnToOff(
206
+ in_channels=args.in_channels,
207
+ out_channels=args.end_ind - args.start_ind,
208
+ int_channels=args.int_channels,
209
+ device="cuda",
210
+ res=args.res,
211
+ decoder=args.decoder,
212
+ mode=args.mode,
213
+ film=bool(args.film),
214
+ )
215
+ else:
216
+ model = ConvCNPWeather(
217
+ in_channels=args.in_channels,
218
+ out_channels=args.end_ind - args.start_ind,
219
+ int_channels=args.int_channels,
220
+ device="cuda",
221
+ res=args.res,
222
+ gnp=bool(0),
223
+ decoder=args.decoder,
224
+ mode=args.mode,
225
+ film=bool(args.film),
226
+ two_frames=bool(args.two_frames),
227
+ )
228
+
229
+ # Instantiate loaders
230
+ train_sampler = DistributedSampler(train_dataset)
231
+ val_sampler = DistributedSampler(val_dataset)
232
+
233
+ train_loader = DataLoader(
234
+ train_dataset,
235
+ batch_size=args.batch_size,
236
+ shuffle=False,
237
+ sampler=train_sampler,
238
+ )
239
+
240
+ val_loader = DataLoader(
241
+ val_dataset,
242
+ batch_size=args.batch_size,
243
+ shuffle=False,
244
+ sampler=val_sampler,
245
+ )
246
+
247
+ # Instantiate trainer
248
+
249
+ trainer = DDPTrainer(
250
+ model,
251
+ rank,
252
+ train_loader,
253
+ val_loader,
254
+ lf,
255
+ output_dir,
256
+ args.lr,
257
+ train_sampler,
258
+ weight_decay=args.weight_decay,
259
+ weights_path=weights_dir,
260
+ tune_film=args.film,
261
+ )
262
+
263
+ # Train model
264
+
265
+ trainer.train(n_epochs=args.epoch)
266
+ destroy_process_group()
267
+
268
+
269
+ if __name__ == "__main__":
270
+ parser = argparse.ArgumentParser()
271
+ parser.add_argument("--output_dir")
272
+ parser.add_argument("--mode")
273
+ parser.add_argument("--weights_dir")
274
+ parser.add_argument("--in_channels", type=int)
275
+ parser.add_argument("--out_channels", type=int)
276
+ parser.add_argument("--int_channels", type=int)
277
+ parser.add_argument("--loss")
278
+ parser.add_argument("--ic")
279
+ parser.add_argument("--decoder")
280
+ parser.add_argument("--film")
281
+ parser.add_argument("--aardvark_ic_path")
282
+ parser.add_argument("--two_frames", type=int, default=0)
283
+ parser.add_argument("--weight_per_variable", type=int, default=0)
284
+ parser.add_argument("--batch_size", type=int, default=128)
285
+ parser.add_argument("--epoch", type=int, default=50)
286
+ parser.add_argument("--master_port", default="12345")
287
+ parser.add_argument("--lr", type=float, default=5e-4)
288
+ parser.add_argument("--lead_time", type=int)
289
+ parser.add_argument("--era5_mode", default="4u")
290
+ parser.add_argument("--weight_decay", type=float, default=1e-6)
291
+ parser.add_argument("--res", type=int, default=1)
292
+ parser.add_argument("--frequency", type=int, default=6)
293
+ parser.add_argument("--diff", type=int, default=1)
294
+ parser.add_argument("--start_ind", type=int, default=0)
295
+ parser.add_argument("--end_ind", type=int, default=24)
296
+ parser.add_argument("--downscaling_train_start_date", default="1979-01-01")
297
+ parser.add_argument("--downscaling_train_end_date", default="2017-12-31")
298
+ parser.add_argument("--downscaling_context", default="era5")
299
+ parser.add_argument("--downscaling_lead_time", type=int)
300
+ parser.add_argument("--var", default=None)
301
+ args = parser.parse_args()
302
+
303
+ torch.device("cuda")
304
+
305
+ # Create results directory
306
+ output_dir = args.output_dir
307
+ if not os.path.exists(output_dir):
308
+ os.mkdir(output_dir)
309
+
310
+ # Save config
311
+ with open(output_dir + "/config.pkl", "wb") as f:
312
+ pickle.dump(vars(args), f)
313
+
314
+ world_size = torch.cuda.device_count()
315
+ mp.spawn(main, args=[world_size, output_dir, args], nprocs=world_size)
official-src/aardvark/trainer.py ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import subprocess
3
+
4
+ import numpy as np
5
+ import torch
6
+ from torch.nn.parallel import DistributedDataParallel as DDP
7
+ from tqdm import tqdm
8
+
9
+ from misc_downscaling_functionality import *
10
+ from models import *
11
+ from loss_functions import *
12
+
13
+ sys.path.append("..")
14
+
15
+
16
+ class DDPTrainer:
17
+ """
18
+ Main class for training models using DDP
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ model,
24
+ rank,
25
+ train_loader,
26
+ val_loader,
27
+ loss_function,
28
+ save_path,
29
+ learning_rate,
30
+ sampler,
31
+ weight_decay,
32
+ test_loader=None,
33
+ weights_path=None,
34
+ tune_film=False,
35
+ ):
36
+ self.rank = rank
37
+ self.model = model
38
+ self.sampler = sampler
39
+ self.tune_film = tune_film
40
+ self.train_loader = train_loader
41
+ self.val_loader = val_loader
42
+ self.save_path = save_path
43
+ self.loss_function = loss_function
44
+ self.best_loss = 1000
45
+ self.test_loader = test_loader
46
+
47
+ self.model = self.model.to(rank)
48
+ self.model = DDP(self.model, device_ids=[rank], find_unused_parameters=True)
49
+
50
+ if self.model.module.decoder == "vit":
51
+ self.opt = torch.optim.AdamW(
52
+ model.parameters(),
53
+ lr=learning_rate,
54
+ betas=(0.9, 0.95),
55
+ weight_decay=1e-5,
56
+ )
57
+ self.scheduler = optim.lr_scheduler.CosineAnnealingLR(self.opt, 891 * 80)
58
+
59
+ else:
60
+ self.opt = torch.optim.Adam(
61
+ model.parameters(), lr=learning_rate, weight_decay=weight_decay
62
+ )
63
+
64
+ self.losses = []
65
+ self.train_losses = []
66
+ self.maes = []
67
+
68
+ def _unravel_to_numpy(self, x):
69
+ return x.view(-1).detach().cpu().numpy()
70
+
71
+ def eval_epoch(self, fix_sigma, epoch):
72
+
73
+ self.model.eval()
74
+
75
+ mae = []
76
+ lf = []
77
+ lf_unnorm = []
78
+
79
+ with torch.no_grad():
80
+ for count, task in enumerate(self.val_loader):
81
+
82
+ out = self.model(task, film_index=0)
83
+
84
+ prev_step = None
85
+
86
+ mae.append(
87
+ np.nanmean(
88
+ np.abs(
89
+ task["y_target"].detach().cpu()
90
+ - out[..., : task["y_target"].shape[-1]].detach().cpu()
91
+ )
92
+ )
93
+ )
94
+ l = (
95
+ self.loss_function(
96
+ task["y_target"], out, prev_step, fix_sigma=fix_sigma
97
+ )
98
+ .detach()
99
+ .item()
100
+ )
101
+ lf.append(l)
102
+
103
+ try:
104
+ ic = self.train_loader.dataset.unnorm_base_context(
105
+ task["y_context"][:, :-11, ...]
106
+ ).permute(0, 3, 2, 1)
107
+ unnorm_pred = self.train_loader.dataset.unnorm_pred(out)
108
+ unnorm_target = self.train_loader.dataset.unnorm_pred(
109
+ task["y_target"]
110
+ )
111
+
112
+ unnorm_pred = unnorm_pred + ic
113
+ unnorm_target = unnorm_target + ic
114
+
115
+ lu = (
116
+ self.loss_function(
117
+ unnorm_target,
118
+ unnorm_pred,
119
+ prev_step,
120
+ fix_sigma=fix_sigma,
121
+ expand=True,
122
+ )
123
+ .detach()
124
+ .cpu()
125
+ .numpy()
126
+ )
127
+
128
+ lf_unnorm.append(lu)
129
+
130
+ except:
131
+ pass
132
+
133
+ if self.test_loader is not None:
134
+ forecasts = []
135
+ targets = []
136
+ stations = []
137
+
138
+ for count, task in enumerate(self.test_loader):
139
+ out = self.model(task, film_index=0)
140
+ forecasts.append(unnormalise_hadisd_tas(out.detach().cpu().numpy()))
141
+ targets.append(
142
+ unnormalise_hadisd_tas(task["y_target"].detach().cpu().numpy())
143
+ )
144
+ stations.append(
145
+ task["downscaling"]["x_target"].detach().cpu().numpy() * 360
146
+ )
147
+
148
+ # Save the test outputs
149
+ np.save(
150
+ self.save_path + f"forecast_{self.rank}_{self.epoch}.npy",
151
+ np.concatenate(forecasts, axis=0),
152
+ )
153
+ np.save(
154
+ self.save_path + f"targets_{self.rank}_{self.epoch}.npy",
155
+ np.concatenate(targets, axis=0),
156
+ )
157
+ np.save(
158
+ self.save_path + f"stations_{self.rank}_{self.epoch}.npy",
159
+ np.concatenate(stations, axis=0),
160
+ )
161
+
162
+ log_loss = np.nanmean(np.array(lf))
163
+
164
+ if log_loss < self.best_loss:
165
+ np.save(
166
+ self.save_path + "unnorm_preds.npy",
167
+ self.train_loader.dataset.unnorm_pred(out).detach().cpu().numpy(),
168
+ )
169
+ np.save(
170
+ self.save_path + "unnorm_targets.npy",
171
+ self.train_loader.dataset.unnorm_pred(task["y_target"])
172
+ .detach()
173
+ .cpu()
174
+ .numpy(),
175
+ )
176
+ log_loss_unnorm = np.nanmean(np.stack(lf_unnorm), axis=0)
177
+
178
+ if np.logical_and(self.rank == 0, self.epoch % 5 == 0):
179
+
180
+ np.save(self.save_path + "preds_eval.npy", out.cpu().numpy())
181
+ np.save(
182
+ self.save_path + "y_target_eval.npy", task["y_target"].cpu().numpy()
183
+ )
184
+
185
+ return log_loss, log_loss_unnorm
186
+
187
+ def train(self, n_epochs=100):
188
+
189
+ torch.cuda.set_device(self.rank)
190
+ subprocess.run(["cp", "reproduce_training.sh", f"{self.save_path}"])
191
+
192
+ train_loss = []
193
+ ll = []
194
+
195
+ fix_sigma = False
196
+ prev_step = None
197
+
198
+ self.epoch = 0
199
+ epoch_loss, log_loss_unnorm = self.eval_epoch(fix_sigma, 0)
200
+ train_loss = np.mean(train_loss)
201
+
202
+ for epoch in range(n_epochs):
203
+ self.epoch = epoch
204
+
205
+ self.sampler.set_epoch(epoch)
206
+
207
+ self.model.train()
208
+ train_loss = []
209
+ with tqdm(self.train_loader, unit="batch") as tepoch:
210
+ for count, task in enumerate(tepoch):
211
+
212
+ out = self.model(task, film_index=0)
213
+
214
+ loss = self.loss_function(
215
+ task["y_target"], out, prev_step, fix_sigma=fix_sigma
216
+ )
217
+
218
+ loss.backward()
219
+ tepoch.set_postfix(loss=loss.item())
220
+ prev_step = out
221
+
222
+ self.opt.step()
223
+ self.opt.zero_grad()
224
+ train_loss.append(loss.item())
225
+ try:
226
+ if self.model.module.decoder == "vit":
227
+ if epoch > 20:
228
+ self.scheduler.step()
229
+ except:
230
+ pass
231
+
232
+ epoch_loss, log_loss_unnorm = self.eval_epoch(fix_sigma, epoch)
233
+ train_loss = np.mean(train_loss)
234
+ ll.append(log_loss_unnorm)
235
+
236
+ self.losses.append(epoch_loss)
237
+ self.train_losses.append(train_loss)
238
+ np.save(
239
+ self.save_path + "losses_{}.npy".format(self.rank),
240
+ np.array(self.losses),
241
+ )
242
+ np.save(
243
+ self.save_path + "train_losses_{}.npy".format(self.rank),
244
+ np.array(self.train_losses),
245
+ )
246
+ np.save(self.save_path + "rmse_{}.npy".format(self.rank), np.array(ll))
247
+
248
+ if epoch_loss < self.best_loss:
249
+ self.best_loss = epoch_loss
250
+
251
+ if self.rank == 0:
252
+ if self.model.module.decoder == "vit":
253
+ torch.save(
254
+ {
255
+ "epoch": epoch,
256
+ "model_state_dict": self.model.state_dict(),
257
+ "optimizer_state_dict": self.opt.state_dict(),
258
+ "scheduler_state_dict": self.scheduler.state_dict(),
259
+ "loss": epoch_loss,
260
+ },
261
+ self.save_path + "epoch_{}".format(epoch),
262
+ )
263
+ else:
264
+ torch.save(
265
+ {
266
+ "epoch": epoch,
267
+ "model_state_dict": self.model.state_dict(),
268
+ "optimizer_state_dict": self.opt.state_dict(),
269
+ "loss": epoch_loss,
270
+ },
271
+ self.save_path + "epoch_{}".format(epoch),
272
+ )
273
+
274
+ try:
275
+ np.save(
276
+ self.save_path + "preds_train.npy".format(epoch),
277
+ out.detach().cpu().numpy(),
278
+ )
279
+ np.save(
280
+ self.save_path + "y_target_train.npy".format(epoch),
281
+ task["y_target"].detach().cpu().numpy(),
282
+ )
283
+ except:
284
+ pass
285
+
286
+
287
+ class DDPTrainerE2E:
288
+ """
289
+ Training class for E2E
290
+ """
291
+
292
+ def __init__(
293
+ self,
294
+ model,
295
+ rank,
296
+ train_loader,
297
+ val_loader,
298
+ loss_function,
299
+ save_path,
300
+ learning_rate,
301
+ sampler,
302
+ weight_decay,
303
+ hadisd_variable_name,
304
+ test_loader=None,
305
+ weights_path=None,
306
+ tune_film=False,
307
+ ):
308
+
309
+ self.rank = rank
310
+ self.model = model
311
+ self.sampler = sampler
312
+ self.tune_film = tune_film
313
+ self.train_loader = train_loader
314
+ self.val_loader = val_loader
315
+ self.save_path = save_path
316
+ self.loss_function = loss_function
317
+ self.best_loss = 1000
318
+ self.test_loader = test_loader
319
+ self.hadisd_variable_name = hadisd_variable_name
320
+
321
+ self.model = self.model.to(rank)
322
+ self.model = DDP(self.model, device_ids=[rank], find_unused_parameters=True)
323
+
324
+ if self.model.module.decoder == "vit":
325
+ self.opt = torch.optim.AdamW(
326
+ model.parameters(),
327
+ lr=learning_rate,
328
+ betas=(0.9, 0.95),
329
+ weight_decay=1e-5,
330
+ )
331
+ self.scheduler = optim.lr_scheduler.CosineAnnealingLR(self.opt, 891 * 80)
332
+ else:
333
+ self.opt = torch.optim.Adam(
334
+ model.parameters(), lr=learning_rate, weight_decay=weight_decay
335
+ )
336
+
337
+ self.losses = []
338
+ self.train_losses = []
339
+
340
+ self.maes = []
341
+
342
+ def _unravel_to_numpy(self, x):
343
+ return x.view(-1).detach().cpu().numpy()
344
+
345
+ def eval_epoch(self, fix_sigma, epoch):
346
+
347
+ self.model.eval()
348
+
349
+ mae = []
350
+ lf = []
351
+ lf_unnorm = []
352
+
353
+ with torch.no_grad():
354
+ forecasts = []
355
+ targets = []
356
+ stations = []
357
+ indices = []
358
+ for count, task in tqdm(enumerate(self.val_loader)):
359
+
360
+ out = self.model(task, film_index=0)
361
+ forecasts.append(
362
+ unnormalise_hadisd_var(
363
+ out.detach().cpu().numpy(), self.hadisd_variable_name
364
+ )
365
+ )
366
+ targets.append(
367
+ unnormalise_hadisd_var(
368
+ task["y_target"].detach().cpu().numpy(),
369
+ self.hadisd_variable_name,
370
+ )
371
+ )
372
+ stations.append(
373
+ task["downscaling"]["x_target"].detach().cpu().numpy() * 360
374
+ )
375
+ indices.append(task["index"])
376
+
377
+ prev_step = None
378
+
379
+ mae.append(
380
+ np.nanmean(
381
+ np.abs(
382
+ task["y_target"].detach().cpu()
383
+ - out[..., : task["y_target"].shape[-1]].detach().cpu()
384
+ )
385
+ )
386
+ )
387
+ l = (
388
+ self.loss_function(
389
+ task["y_target"], out, prev_step, fix_sigma=fix_sigma
390
+ )
391
+ .detach()
392
+ .item()
393
+ )
394
+ lf.append(l)
395
+
396
+ try:
397
+ ic = self.train_loader.dataset.unnorm_base_context(
398
+ task["y_context"][:, :-11, ...]
399
+ ).permute(0, 3, 2, 1)
400
+ unnorm_pred = self.train_loader.dataset.unnorm_pred(out)
401
+ unnorm_target = self.train_loader.dataset.unnorm_pred(
402
+ task["y_target"]
403
+ )
404
+
405
+ unnorm_pred = unnorm_pred + ic
406
+ unnorm_target = unnorm_target + ic
407
+
408
+ lu = (
409
+ self.loss_function(
410
+ unnorm_target,
411
+ unnorm_pred,
412
+ prev_step,
413
+ fix_sigma=fix_sigma,
414
+ expand=True,
415
+ )
416
+ .detach()
417
+ .cpu()
418
+ .numpy()
419
+ )
420
+
421
+ lf_unnorm.append(lu)
422
+
423
+ except:
424
+ pass
425
+
426
+ # Save the test outputs
427
+ np.save(
428
+ self.save_path + f"val_forecast_{self.rank}_{self.epoch}.npy",
429
+ np.concatenate(forecasts, axis=0),
430
+ )
431
+ np.save(
432
+ self.save_path + f"val_targets_{self.rank}_{self.epoch}.npy",
433
+ np.concatenate(targets, axis=0),
434
+ )
435
+ np.save(
436
+ self.save_path + f"val_stations_{self.rank}_{self.epoch}.npy",
437
+ np.concatenate(stations, axis=0),
438
+ )
439
+ np.save(
440
+ self.save_path + f"val_indices_{self.rank}_{self.epoch}.npy",
441
+ np.concatenate(indices, axis=0),
442
+ )
443
+
444
+ if self.test_loader is not None:
445
+ forecasts = []
446
+ targets = []
447
+ stations = []
448
+ indices = []
449
+
450
+ for count, task in enumerate(self.test_loader):
451
+ out = self.model(task, film_index=0)
452
+ forecasts.append(
453
+ unnormalise_hadisd_var(
454
+ out.detach().cpu().numpy(), self.hadisd_variable_name
455
+ )
456
+ )
457
+ targets.append(
458
+ unnormalise_hadisd_var(
459
+ task["y_target"].detach().cpu().numpy(),
460
+ self.hadisd_variable_name,
461
+ )
462
+ )
463
+ stations.append(
464
+ task["downscaling"]["x_target"].detach().cpu().numpy() * 360
465
+ )
466
+ indices.append(task["index"])
467
+
468
+ np.save(
469
+ self.save_path + f"test_forecast_{self.rank}_{self.epoch}.npy",
470
+ np.concatenate(forecasts, axis=0),
471
+ )
472
+ np.save(
473
+ self.save_path + f"test_targets_{self.rank}_{self.epoch}.npy",
474
+ np.concatenate(targets, axis=0),
475
+ )
476
+ np.save(
477
+ self.save_path + f"test_stations_{self.rank}_{self.epoch}.npy",
478
+ np.concatenate(stations, axis=0),
479
+ )
480
+ np.save(
481
+ self.save_path + f"test_indices_{self.rank}_{self.epoch}.npy",
482
+ np.concatenate(indices, axis=0),
483
+ )
484
+
485
+ log_loss = np.nanmean(np.array(lf))
486
+
487
+ if log_loss < self.best_loss:
488
+ np.save(
489
+ self.save_path + "unnorm_preds.npy",
490
+ self.train_loader.dataset.unnorm_pred(out).detach().cpu().numpy(),
491
+ )
492
+ np.save(
493
+ self.save_path + "unnorm_targets.npy",
494
+ self.train_loader.dataset.unnorm_pred(task["y_target"])
495
+ .detach()
496
+ .cpu()
497
+ .numpy(),
498
+ )
499
+ log_loss_unnorm = np.nanmean(np.stack(lf_unnorm), axis=0)
500
+
501
+ if np.logical_and(self.rank == 0, self.epoch % 5 == 0):
502
+
503
+ np.save(self.save_path + "preds_eval.npy", out.cpu().numpy())
504
+ np.save(
505
+ self.save_path + "y_target_eval.npy", task["y_target"].cpu().numpy()
506
+ )
507
+
508
+ return log_loss, log_loss_unnorm
509
+
510
+ def train(self, n_epochs=100):
511
+
512
+ torch.cuda.set_device(self.rank)
513
+ subprocess.run(["cp", "reproduce_training.sh", f"{self.save_path}"])
514
+
515
+ train_loss = []
516
+ ll = []
517
+
518
+ fix_sigma = False
519
+ prev_step = None
520
+
521
+ self.epoch = 0
522
+ epoch_loss, log_loss_unnorm = self.eval_epoch(fix_sigma, 0)
523
+ train_loss = np.mean(train_loss)
524
+
525
+ for epoch in range(n_epochs):
526
+ self.epoch = epoch
527
+ self.sampler.set_epoch(epoch)
528
+
529
+ epoch_loss, log_loss_unnorm = self.eval_epoch(fix_sigma, epoch)
530
+ train_loss = np.mean(train_loss)
531
+ ll.append(log_loss_unnorm)
532
+
533
+ self.losses.append(epoch_loss)
534
+ self.train_losses.append(train_loss)
535
+ np.save(
536
+ self.save_path + "losses_{}.npy".format(self.rank),
537
+ np.array(self.losses),
538
+ )
539
+ np.save(
540
+ self.save_path + "train_losses_{}.npy".format(self.rank),
541
+ np.array(self.train_losses),
542
+ )
543
+ np.save(self.save_path + "rmse_{}.npy".format(self.rank), np.array(ll))
544
+
545
+ if epoch_loss < self.best_loss:
546
+ self.best_loss = epoch_loss
547
+
548
+ if self.rank == 0:
549
+ if self.model.module.decoder == "vit":
550
+ torch.save(
551
+ {
552
+ "epoch": epoch,
553
+ "model_state_dict": self.model.state_dict(),
554
+ "optimizer_state_dict": self.opt.state_dict(),
555
+ "scheduler_state_dict": self.scheduler.state_dict(),
556
+ "loss": epoch_loss,
557
+ },
558
+ self.save_path + "epoch_{}".format(epoch),
559
+ )
560
+ else:
561
+ torch.save(
562
+ {
563
+ "epoch": epoch,
564
+ "model_state_dict": self.model.state_dict(),
565
+ "optimizer_state_dict": self.opt.state_dict(),
566
+ "loss": epoch_loss,
567
+ },
568
+ self.save_path + "epoch_{}".format(epoch),
569
+ )
570
+
571
+ try:
572
+ np.save(
573
+ self.save_path + "preds_train.npy".format(epoch),
574
+ out.detach().cpu().numpy(),
575
+ )
576
+ np.save(
577
+ self.save_path + "y_target_train.npy".format(epoch),
578
+ task["y_target"].detach().cpu().numpy(),
579
+ )
580
+ except:
581
+ pass
582
+
583
+ self.model.train()
584
+ train_loss = []
585
+ with tqdm(self.train_loader, unit="batch") as tepoch:
586
+ for count, task in tqdm(enumerate(tepoch)):
587
+ out = self.model(task, film_index=0)
588
+
589
+ loss = self.loss_function(
590
+ task["y_target"], out, prev_step, fix_sigma=fix_sigma
591
+ )
592
+
593
+ loss.backward()
594
+ tepoch.set_postfix(loss=loss.item())
595
+ prev_step = out
596
+
597
+ self.opt.step()
598
+ self.opt.zero_grad()
599
+ train_loss.append(loss.item())
600
+ if self.model.module.decoder == "vit":
601
+ if epoch > 20:
602
+ self.scheduler.step()
official-src/aardvark/unet_wrap_padding.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+
7
+ def cylindrical_conv_pad(x, w_pad):
8
+ return torch.cat([x[..., -w_pad:], x, x[..., :w_pad]], axis=-1)
9
+
10
+
11
+ class CylindricalConv2D(nn.Conv2d):
12
+ """
13
+ UNet with cylinderical boundary conditions
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ in_channels: int,
19
+ out_channels: int,
20
+ kernel_size: int,
21
+ stride: int,
22
+ ):
23
+ super().__init__(
24
+ in_channels=in_channels,
25
+ out_channels=out_channels,
26
+ kernel_size=kernel_size,
27
+ stride=stride,
28
+ )
29
+
30
+ assert self.kernel_size[0] % 2 == 1
31
+ assert self.kernel_size[1] % 2 == 1
32
+
33
+ self.h_pad = self.kernel_size[0] // 2
34
+ self.w_pad = self.kernel_size[1] // 2
35
+
36
+ def forward(self, x: torch.Tensor):
37
+ x = nn.functional.pad(x, (0, 0, self.h_pad, self.h_pad))
38
+ return super().forward(cylindrical_conv_pad(x, self.w_pad))
39
+
40
+
41
+ class CylindricalConvTranspose2D(nn.ConvTranspose2d):
42
+ def __init__(
43
+ self,
44
+ in_channels: int,
45
+ out_channels: int,
46
+ kernel_size: int,
47
+ stride: int,
48
+ ):
49
+ super().__init__(
50
+ in_channels=in_channels,
51
+ out_channels=out_channels,
52
+ kernel_size=kernel_size,
53
+ stride=stride,
54
+ )
55
+
56
+ assert self.kernel_size[0] % 2 == 1
57
+ assert self.kernel_size[1] % 2 == 1
58
+
59
+ self.sh, self.sw = self.stride
60
+ self.kh, self.kw = self.kernel_size
61
+
62
+ self.h_pad = math.ceil(((self.sh - 1) + 2 * (self.kh // 2)) / self.sh)
63
+ self.w_pad = math.ceil(((self.sw - 1) + 2 * (self.kw // 2)) / self.sw)
64
+
65
+ self.h0 = self.sh * self.h_pad - (self.sh - 1) + (self.kh // 2)
66
+ self.w0 = self.sw * self.w_pad - (self.sw - 1) + (self.kw // 2)
67
+
68
+ self._bias = nn.Parameter(10**-3 * torch.randn(out_channels))
69
+
70
+ def forward(self, x: torch.Tensor):
71
+
72
+ Nh = x.shape[2] * self.sh
73
+ Nw = x.shape[3] * self.sw
74
+
75
+ x = cylindrical_conv_pad(x, self.w_pad)
76
+ x = nn.functional.pad(x, (0, 0, self.h_pad, self.h_pad))
77
+
78
+ x = super().forward(x)
79
+
80
+ return x[:, :, self.h0 : self.h0 + Nh, self.w0 : self.w0 + Nw]
81
+
82
+
83
+ class Down(nn.Module):
84
+
85
+ def __init__(
86
+ self,
87
+ in_channels,
88
+ out_channels,
89
+ p=0,
90
+ film=False,
91
+ down=True,
92
+ attn=False,
93
+ ):
94
+
95
+ super().__init__()
96
+
97
+ self.film = film
98
+ self.attn = attn
99
+
100
+ self.conv_1 = CylindricalConv2D(
101
+ in_channels, out_channels, kernel_size=3, stride=1
102
+ )
103
+ if down:
104
+ self.conv_2 = CylindricalConv2D(
105
+ out_channels, out_channels, kernel_size=3, stride=2
106
+ )
107
+ else:
108
+ self.conv_2 = CylindricalConv2D(
109
+ out_channels, out_channels, kernel_size=3, stride=1
110
+ )
111
+
112
+ self.bn_1 = nn.BatchNorm2d(out_channels)
113
+ self.bn_2 = nn.BatchNorm2d(out_channels)
114
+
115
+ self.activation = nn.GELU()
116
+
117
+ if film:
118
+
119
+ self.gamma_1 = torch.nn.Parameter(
120
+ torch.ones(10, out_channels, 1, 1),
121
+ )
122
+ self.gamma_2 = torch.nn.Parameter(
123
+ torch.ones(10, out_channels, 1, 1),
124
+ )
125
+ self.beta_1 = torch.nn.Parameter(
126
+ torch.zeros(10, out_channels, 1, 1),
127
+ )
128
+ self.beta_2 = torch.nn.Parameter(
129
+ torch.zeros(10, out_channels, 1, 1),
130
+ )
131
+
132
+ if self.attn:
133
+ self.mha = AttentionBlock(n_channels=out_channels, n_heads=8)
134
+
135
+ def forward(self, xi, film_index=None):
136
+
137
+ film_index = film_index[:, 0].int()
138
+
139
+ x = self.conv_1(xi)
140
+ x = self.bn_1(x)
141
+ if self.film:
142
+ g1 = torch.index_select(self.gamma_1, 0, film_index)
143
+ b1 = torch.index_select(self.beta_1, 0, film_index)
144
+
145
+ x = g1 * x + b1
146
+
147
+ x = self.activation(x)
148
+ x = self.conv_2(x)
149
+ x = self.bn_2(x)
150
+ if self.film:
151
+ g2 = torch.index_select(self.gamma_2, 0, film_index)
152
+ b2 = torch.index_select(self.beta_2, 0, film_index)
153
+ x = g2 * x + b2
154
+ x = self.activation(x)
155
+
156
+ if self.attn:
157
+ x = self.mha(x)
158
+
159
+ return x
160
+
161
+
162
+ class Up(nn.Module):
163
+
164
+ def __init__(
165
+ self,
166
+ in_channels,
167
+ out_channels,
168
+ p,
169
+ bilinear=False,
170
+ film=False,
171
+ stride=2,
172
+ attn=False,
173
+ ):
174
+ super().__init__()
175
+
176
+ self.film = film
177
+
178
+ if bilinear:
179
+ self.up = nn.Upsample(
180
+ scale_factor=2,
181
+ mode="bilinear",
182
+ align_corners=True,
183
+ )
184
+ else:
185
+ self.up = CylindricalConvTranspose2D(
186
+ in_channels, out_channels, kernel_size=3, stride=stride
187
+ )
188
+
189
+ self.conv = Down(
190
+ out_channels,
191
+ out_channels,
192
+ p=0,
193
+ film=film,
194
+ down=False,
195
+ attn=attn,
196
+ )
197
+
198
+ def forward(self, x1, x2, film_index=None):
199
+ x1 = self.up(x1)
200
+ x1 = self.conv(x1, film_index=film_index)
201
+
202
+ if x1.shape[-1] != x2.shape[-1]:
203
+ x1 = x1[..., :, :-1]
204
+ if x1.shape[-2] != x2.shape[-2]:
205
+ x1 = x1[..., :-1, :]
206
+
207
+ return torch.cat([x2, x1], dim=1)
208
+
209
+
210
+ class Unet(nn.Module):
211
+
212
+ def __init__(
213
+ self,
214
+ in_channels,
215
+ out_channels,
216
+ div_factor=1,
217
+ p=0.0,
218
+ context=True,
219
+ film=False,
220
+ film_base=True,
221
+ ):
222
+ super(Unet, self).__init__()
223
+
224
+ self.n_channels = in_channels
225
+ self.bilinear = True
226
+ self.fp = nn.Softplus()
227
+ self.variances = nn.Parameter(torch.zeros([out_channels]))
228
+ self.context = context
229
+ self.film = film
230
+
231
+ m = 1
232
+
233
+ self.down1 = Down(
234
+ self.n_channels,
235
+ m * 128 // div_factor,
236
+ p=0,
237
+ film=film,
238
+ attn=False,
239
+ )
240
+ self.down2 = Down(
241
+ m * 128 // div_factor,
242
+ m * 256 // div_factor,
243
+ p=0,
244
+ film=film,
245
+ attn=False,
246
+ )
247
+ self.down3 = Down(
248
+ m * 256 // div_factor,
249
+ m * 512 // div_factor,
250
+ p=0,
251
+ film=film,
252
+ attn=False,
253
+ )
254
+ self.down4 = Down(
255
+ m * 512 // div_factor,
256
+ m * 512 // div_factor,
257
+ p=0,
258
+ film=film,
259
+ attn=False,
260
+ )
261
+ self.up1 = Up(
262
+ m * 512 // div_factor,
263
+ m * 512 // div_factor,
264
+ p=0,
265
+ film=film,
266
+ attn=False,
267
+ )
268
+ self.up2 = Up(
269
+ m * 1024 // div_factor,
270
+ m * 256 // div_factor,
271
+ p=0,
272
+ film=film,
273
+ attn=False,
274
+ )
275
+ self.up3 = Up(
276
+ m * 512 // div_factor,
277
+ m * 128 // div_factor,
278
+ p=0,
279
+ film=film,
280
+ attn=False,
281
+ )
282
+ self.up4 = Up(
283
+ m * 256 // div_factor,
284
+ m * 64 // div_factor,
285
+ p=0,
286
+ film=film,
287
+ attn=False,
288
+ )
289
+
290
+ self.out = nn.Conv2d(
291
+ m * 64 // div_factor + in_channels,
292
+ out_channels,
293
+ kernel_size=1,
294
+ bias=False,
295
+ )
296
+
297
+ def forward(self, x, film_index=None):
298
+
299
+ x1 = x.contiguous()
300
+ x2 = self.down1(x1, film_index=film_index)
301
+ x3 = self.down2(x2, film_index=film_index)
302
+ x4 = self.down3(x3, film_index=film_index)
303
+ x5 = self.down4(x4, film_index=film_index)
304
+ x = self.up1(x5, x4, film_index=film_index)
305
+ x = self.up2(x, x3, film_index=film_index)
306
+ x = self.up3(x, x2, film_index=film_index)
307
+ x = self.up4(x, x1, film_index=film_index)
308
+ x = self.out(x)
309
+
310
+ return x.permute(0, 2, 3, 1)
official-src/aardvark/utils.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ import numpy as np
3
+ from torch.nn.utils.rnn import pad_sequence
4
+
5
+
6
+ def channels_to_2nd_dim(x):
7
+ return x.permute(*([0, x.dim() - 1] + list(range(1, x.dim() - 1))))
8
+
9
+
10
+ def channels_to_final_dim(x):
11
+ return x.permute(*([0] + list(range(2, x.dim())) + [1]))
12
+
13
+
14
+ def collate(tensor_list):
15
+ out_dict = defaultdict()
16
+ for k in tensor_list[0].keys():
17
+ out_dict[k] = [t[k] for t in tensor_list]
18
+ out_dict[k] = pad_sequence(
19
+ out_dict[k],
20
+ padding_value=np.nan,
21
+ batch_first=True,
22
+ )
23
+ return out_dict
official-src/aardvark/vit.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This file is adapted from Nguyen, Tung, et al. "ClimaX: A foundation model
3
+ for weather and climate." arXiv preprint arXiv:2301.10343 (2023).
4
+ Code from this project is available at https://github.com/microsoft/ClimaX
5
+ """
6
+
7
+ from functools import lru_cache
8
+
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn as nn
12
+ from timm.models.vision_transformer import Block, PatchEmbed, trunc_normal_
13
+
14
+ from architectures import MLP
15
+
16
+
17
+ def get_2d_sincos_pos_embed(embed_dim, grid_size_h, grid_size_w, cls_token=False):
18
+
19
+ grid_h = np.arange(grid_size_h, dtype=np.float32)
20
+ grid_w = np.arange(grid_size_w, dtype=np.float32)
21
+ grid = np.meshgrid(grid_w, grid_h)
22
+ grid = np.stack(grid, axis=0)
23
+
24
+ grid = grid.reshape([2, 1, grid_size_h, grid_size_w])
25
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
26
+ if cls_token:
27
+ pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
28
+ return pos_embed
29
+
30
+
31
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
32
+ assert embed_dim % 2 == 0
33
+
34
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0])
35
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1])
36
+
37
+ emb = np.concatenate([emb_h, emb_w], axis=1)
38
+ return emb
39
+
40
+
41
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
42
+ assert embed_dim % 2 == 0
43
+ omega = np.arange(embed_dim // 2, dtype=np.float32)
44
+ omega /= embed_dim / 2.0
45
+ omega = 1.0 / 10000**omega
46
+
47
+ pos = pos.reshape(-1)
48
+ out = np.einsum("m,d->md", pos, omega)
49
+
50
+ emb_sin = np.sin(out)
51
+ emb_cos = np.cos(out)
52
+
53
+ emb = np.concatenate([emb_sin, emb_cos], axis=1)
54
+ return emb
55
+
56
+
57
+ def interpolate_pos_embed(model, checkpoint_model, new_size=(64, 128)):
58
+ if "net.pos_embed" in checkpoint_model:
59
+ pos_embed_checkpoint = checkpoint_model["net.pos_embed"]
60
+ embedding_size = pos_embed_checkpoint.shape[-1]
61
+ orig_num_patches = pos_embed_checkpoint.shape[-2]
62
+ patch_size = model.patch_size
63
+ w_h_ratio = 2
64
+ orig_h = int((orig_num_patches // w_h_ratio) ** 0.5)
65
+ orig_w = w_h_ratio * orig_h
66
+ orig_size = (orig_h, orig_w)
67
+ new_size = (new_size[0] // patch_size, new_size[1] // patch_size)
68
+
69
+ if orig_size[0] != new_size[0]:
70
+ print(
71
+ "Interpolate PEs from %dx%d to %dx%d"
72
+ % (orig_size[0], orig_size[1], new_size[0], new_size[1])
73
+ )
74
+ pos_tokens = pos_embed_checkpoint.reshape(
75
+ -1, orig_size[0], orig_size[1], embedding_size
76
+ ).permute(0, 3, 1, 2)
77
+ new_pos_tokens = torch.nn.functional.interpolate(
78
+ pos_tokens,
79
+ size=(new_size[0], new_size[1]),
80
+ mode="bicubic",
81
+ align_corners=False,
82
+ )
83
+ new_pos_tokens = new_pos_tokens.permute(0, 2, 3, 1).flatten(1, 2)
84
+ checkpoint_model["net.pos_embed"] = new_pos_tokens
85
+
86
+
87
+ def interpolate_channel_embed(checkpoint_model, new_len):
88
+ if "net.channel_embed" in checkpoint_model:
89
+ channel_embed_checkpoint = checkpoint_model["net.channel_embed"]
90
+ old_len = channel_embed_checkpoint.shape[1]
91
+ if new_len <= old_len:
92
+ checkpoint_model["net.channel_embed"] = channel_embed_checkpoint[
93
+ :, :new_len
94
+ ]
95
+
96
+
97
+ class ViT(nn.Module):
98
+
99
+ def __init__(
100
+ self,
101
+ in_channels,
102
+ out_channels,
103
+ h_channels,
104
+ img_size=[256, 128],
105
+ patch_size=8,
106
+ depth=24,
107
+ decoder_depth=4,
108
+ num_heads=16,
109
+ mlp_ratio=4.0,
110
+ drop_path=0.0,
111
+ drop_rate=0.0,
112
+ per_var_embedding=True,
113
+ ):
114
+ super().__init__()
115
+
116
+ self.img_size = img_size
117
+ self.patch_size = patch_size
118
+ default_vars = [str(i) for i in range(in_channels)]
119
+ self.default_vars = default_vars
120
+ embed_dim = h_channels
121
+ self.per_var_embedding = per_var_embedding
122
+
123
+ if self.per_var_embedding:
124
+ self.token_embeds = nn.ModuleList(
125
+ [
126
+ PatchEmbed(img_size, patch_size, 1, embed_dim)
127
+ for i in range(len(default_vars))
128
+ ]
129
+ )
130
+ else:
131
+ self.token_embeds = nn.ModuleList(
132
+ [PatchEmbed(img_size, patch_size, in_channels, embed_dim)]
133
+ )
134
+ self.num_patches = self.token_embeds[0].num_patches
135
+
136
+ self.var_embed, self.var_map = self.create_var_embedding(embed_dim)
137
+ self.var_query = nn.Parameter(torch.zeros(1, 1, embed_dim), requires_grad=True)
138
+ self.var_agg = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
139
+
140
+ self.pos_embed = nn.Parameter(
141
+ torch.zeros(1, self.num_patches, embed_dim), requires_grad=True
142
+ )
143
+ self.lead_time_embed = nn.Linear(1, embed_dim)
144
+
145
+ self.out_dim = out_channels
146
+ self.pos_drop = nn.Dropout(p=drop_rate)
147
+ dpr = [x.item() for x in torch.linspace(0, drop_path, depth)]
148
+ self.blocks = nn.ModuleList(
149
+ [
150
+ Block(
151
+ embed_dim,
152
+ num_heads,
153
+ mlp_ratio,
154
+ qkv_bias=True,
155
+ drop_path=dpr[i],
156
+ norm_layer=nn.LayerNorm,
157
+ drop=drop_rate,
158
+ )
159
+ for i in range(depth)
160
+ ]
161
+ )
162
+ self.norm = nn.LayerNorm(embed_dim)
163
+
164
+ self.head = nn.ModuleList()
165
+ for _ in range(decoder_depth):
166
+ self.head.append(nn.Linear(embed_dim, embed_dim))
167
+ self.head.append(nn.GELU())
168
+ self.head.append(nn.Linear(embed_dim, self.out_dim * patch_size**2))
169
+ self.head = nn.Sequential(*self.head)
170
+
171
+ self.initialize_weights()
172
+ if not self.per_var_embedding:
173
+ self.mlp = MLP(in_channels=277, out_channels=256)
174
+
175
+ def initialize_weights(self):
176
+ pos_embed = get_2d_sincos_pos_embed(
177
+ self.pos_embed.shape[-1],
178
+ int(self.img_size[0] / self.patch_size),
179
+ int(self.img_size[1] / self.patch_size),
180
+ cls_token=False,
181
+ )
182
+ self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
183
+
184
+ var_embed = get_1d_sincos_pos_embed_from_grid(
185
+ self.var_embed.shape[-1], np.arange(len(self.default_vars))
186
+ )
187
+ self.var_embed.data.copy_(torch.from_numpy(var_embed).float().unsqueeze(0))
188
+
189
+ for i in range(len(self.token_embeds)):
190
+ w = self.token_embeds[i].proj.weight.data
191
+ trunc_normal_(w.view([w.shape[0], -1]), std=0.02)
192
+
193
+ self.apply(self._init_weights)
194
+
195
+ def _init_weights(self, m):
196
+ if isinstance(m, nn.Linear):
197
+ trunc_normal_(m.weight, std=0.02)
198
+ if m.bias is not None:
199
+ nn.init.constant_(m.bias, 0)
200
+ elif isinstance(m, nn.LayerNorm):
201
+ nn.init.constant_(m.bias, 0)
202
+ nn.init.constant_(m.weight, 1.0)
203
+
204
+ def create_var_embedding(self, dim):
205
+ var_embed = nn.Parameter(
206
+ torch.zeros(1, len(self.default_vars), dim), requires_grad=True
207
+ )
208
+ var_map = {}
209
+ idx = 0
210
+ for var in self.default_vars:
211
+ var_map[var] = idx
212
+ idx += 1
213
+ return var_embed, var_map
214
+
215
+ @lru_cache(maxsize=None)
216
+ def get_var_ids(self, vars, device):
217
+ ids = np.array([self.var_map[var] for var in vars])
218
+ return torch.from_numpy(ids).to(device)
219
+
220
+ def get_var_emb(self, var_emb, vars):
221
+ ids = self.get_var_ids(vars, var_emb.device)
222
+ return var_emb[:, ids, :]
223
+
224
+ def unpatchify(self, x: torch.Tensor, h=None, w=None):
225
+ p = self.patch_size
226
+ c = self.out_dim
227
+ h = self.img_size[0] // p if h is None else h // p
228
+ w = self.img_size[1] // p if w is None else w // p
229
+ assert h * w == x.shape[1]
230
+
231
+ x = x.reshape(shape=(x.shape[0], h, w, p, p, c))
232
+ x = torch.einsum("nhwpqc->nchpwq", x)
233
+ imgs = x.reshape(shape=(x.shape[0], c, h * p, w * p))
234
+ return imgs
235
+
236
+ def aggregate_variables(self, x: torch.Tensor):
237
+ b, _, l, _ = x.shape
238
+ x = torch.einsum("bvld->blvd", x)
239
+ x = x.flatten(0, 1)
240
+
241
+ var_query = self.var_query.repeat_interleave(x.shape[0], dim=0)
242
+ x, _ = self.var_agg(var_query, x, x)
243
+ x = x.squeeze()
244
+
245
+ x = x.unflatten(dim=0, sizes=(b, l))
246
+ return x
247
+
248
+ def mlp_embedding(self, x):
249
+
250
+ return
251
+
252
+ def forward_encoder(self, x, lead_times, variables):
253
+
254
+ if isinstance(variables, list):
255
+ variables = tuple(variables)
256
+
257
+ if self.per_var_embedding:
258
+ embeds = []
259
+ var_ids = self.get_var_ids(variables, x.device)
260
+ for i in range(len(var_ids)):
261
+ id = var_ids[i]
262
+ embeds.append(self.token_embeds[id](x[:, i : i + 1]))
263
+ x = torch.stack(embeds, dim=1)
264
+
265
+ var_embed = self.get_var_emb(self.var_embed, variables)
266
+ x = x + var_embed.unsqueeze(2)
267
+
268
+ x = self.aggregate_variables(x)
269
+
270
+ else:
271
+ x = self.mlp(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
272
+ x = self.token_embeds[0](x)
273
+
274
+ x = x + self.pos_embed
275
+
276
+ lead_time_emb = self.lead_time_embed(lead_times.unsqueeze(-1))
277
+ lead_time_emb = lead_time_emb.unsqueeze(1)
278
+
279
+ x = x + lead_time_emb
280
+
281
+ x = self.pos_drop(x)
282
+
283
+ for blk in self.blocks:
284
+ x = blk(x)
285
+ x = self.norm(x)
286
+
287
+ return x
288
+
289
+ def forward(self, x, lead_times=None, film_index=None):
290
+
291
+ if lead_times is None:
292
+ lead_times = torch.ones(x.shape[0]).float().cuda().unsqueeze(-1)
293
+
294
+ out_transformers = self.forward_encoder(x, lead_times[:, 0], self.default_vars)
295
+ preds = self.head(out_transformers)
296
+ preds = self.unpatchify(preds)
297
+
298
+ return preds.permute(0, 2, 3, 1)
official-src/data/grid_lon_lat/era5_x_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00907116398e8f25089efe261536bdc76937c3d17aa592914cb71852c18c92d9
3
+ size 2048
official-src/data/grid_lon_lat/era5_y_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e09ac41c355484d2c361c8b4ff1dff34b104efcb7b6f1532a75ff422bd5dce35
3
+ size 1096
official-src/data/norm_factors/mean_4u_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:40376a60ad7d920f9d1c2ab08f4f783aee2f2aa07f5c6ad6e00fadce5c134be1
3
+ size 224
official-src/data/norm_factors/mean_diff_4u_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79c0582e23a3a54afbc13c4249d5af7256c0b10f34da6bf8fd9e1c2daf2c2d06
3
+ size 224
official-src/data/norm_factors/mean_hadisd_tas.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8048980d8fe7ec5f4ba74ae5c6bbadb59d2e846f4be12226e9b958e8a4b3a24e
3
+ size 132
official-src/data/norm_factors/mean_hadisd_ws.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c97ebf30fa59a35582fda96a16a4b98833aae0e8df7fe36db641944398cbe092
3
+ size 136
official-src/data/norm_factors/std_4u_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:acad322c1b4ecdcf71d63f48272aaee0d4ab3a89b4ea490e968721b10621ca6d
3
+ size 224
official-src/data/norm_factors/std_diff_4u_1.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:50ee4e57a7d460f03b21b1261743fe1667a254e3019f3b3f8c829012d22d873a
3
+ size 224
official-src/data/norm_factors/std_hadisd_tas.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ae2e4568519a0da1bcfdf795dbdccdca6f22feec1a83a18b6665ac1f9a045bc
3
+ size 132
official-src/data/norm_factors/std_hadisd_ws.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9181e9fdaff3adf74ccc2a3816690f367435dc945287ebea2728f0c2b8820bfe
3
+ size 136
official-src/data/sample_data_final.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:536459e078220d7a8e5f6d3caa3846c930c6b759dbb21660df73473cbaf01d0c
3
+ size 53759995
official-src/environment.yml ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: npw
2
+ channels:
3
+ - pytorch
4
+ - nvidia
5
+ - conda-forge
6
+ - defaults
7
+ dependencies:
8
+ - _libgcc_mutex=0.1=conda_forge
9
+ - _openmp_mutex=4.5=2_gnu
10
+ - anyio=4.2.0=py38h06a4308_0
11
+ - argon2-cffi=21.3.0=pyhd3eb1b0_0
12
+ - argon2-cffi-bindings=21.2.0=py38h7f8727e_0
13
+ - asttokens=2.0.5=pyhd3eb1b0_0
14
+ - async-lru=2.0.4=py38h06a4308_0
15
+ - attrs=23.1.0=py38h06a4308_0
16
+ - babel=2.11.0=py38h06a4308_0
17
+ - backcall=0.2.0=pyhd3eb1b0_0
18
+ - beautifulsoup4=4.12.2=py38h06a4308_0
19
+ - blas=1.0=mkl
20
+ - bleach=4.1.0=pyhd3eb1b0_0
21
+ - blosc=1.21.5=hc2324a3_1
22
+ - bottleneck=1.3.7=py38ha9d4c09_0
23
+ - brotli=1.0.9=h5eee18b_7
24
+ - brotli-bin=1.0.9=h5eee18b_7
25
+ - brotli-python=1.0.9=py38h6a678d5_7
26
+ - bzip2=1.0.8=h5eee18b_5
27
+ - c-ares=1.19.1=h5eee18b_0
28
+ - ca-certificates=2024.6.2=hbcca054_0
29
+ - cartopy=0.21.1=py38h1176785_0
30
+ - certifi=2024.6.2=pyhd8ed1ab_0
31
+ - cffi=1.16.0=py38h5eee18b_0
32
+ - charset-normalizer=2.0.4=pyhd3eb1b0_0
33
+ - comm=0.2.1=py38h06a4308_0
34
+ - contourpy=1.0.5=py38hdb19cb5_0
35
+ - cuda-cudart=12.1.105=0
36
+ - cuda-cupti=12.1.105=0
37
+ - cuda-libraries=12.1.0=0
38
+ - cuda-nvrtc=12.1.105=0
39
+ - cuda-nvtx=12.1.105=0
40
+ - cuda-opencl=12.4.99=0
41
+ - cuda-runtime=12.1.0=0
42
+ - cycler=0.11.0=pyhd3eb1b0_0
43
+ - cyrus-sasl=2.1.28=h52b45da_1
44
+ - dbus=1.13.18=hb2f20db_0
45
+ - debugpy=1.6.7=py38h6a678d5_0
46
+ - decorator=5.1.1=pyhd3eb1b0_0
47
+ - defusedxml=0.7.1=pyhd3eb1b0_0
48
+ - eccodes=2.35.0=he84ddb8_0
49
+ - exceptiongroup=1.2.0=py38h06a4308_0
50
+ - executing=0.8.3=pyhd3eb1b0_0
51
+ - expat=2.5.0=h6a678d5_0
52
+ - ffmpeg=4.3=hf484d3e_0
53
+ - filelock=3.13.1=py38h06a4308_0
54
+ - findlibs=0.0.5=pyhd8ed1ab_0
55
+ - fontconfig=2.14.2=h14ed4e7_0
56
+ - fonttools=4.25.0=pyhd3eb1b0_0
57
+ - freeglut=3.4.0=h6a678d5_0
58
+ - freetype=2.12.1=h4a9f257_0
59
+ - geos=3.8.0=he6710b0_0
60
+ - glib=2.78.4=h6a678d5_0
61
+ - glib-tools=2.78.4=h6a678d5_0
62
+ - gmp=6.2.1=h295c915_3
63
+ - gmpy2=2.1.2=py38heeb90bb_0
64
+ - gnutls=3.6.15=he1e5248_0
65
+ - gst-plugins-base=1.14.1=h6a678d5_1
66
+ - gstreamer=1.14.1=h5eee18b_1
67
+ - hdf4=4.2.15=h9772cbc_5
68
+ - hdf5=1.14.3=nompi_h4f84152_100
69
+ - icu=73.2=h59595ed_0
70
+ - idna=3.4=py38h06a4308_0
71
+ - importlib-metadata=7.0.1=py38h06a4308_0
72
+ - importlib_metadata=7.0.1=hd3eb1b0_0
73
+ - importlib_resources=6.1.1=py38h06a4308_1
74
+ - intel-openmp=2023.1.0=hdb19cb5_46306
75
+ - ipykernel=6.28.0=py38h06a4308_0
76
+ - ipython=8.12.2=py38h06a4308_0
77
+ - jasper=4.2.4=he283c48_0
78
+ - jedi=0.18.1=py38h06a4308_1
79
+ - jinja2=3.1.3=py38h06a4308_0
80
+ - jpeg=9e=h5eee18b_1
81
+ - json5=0.9.6=pyhd3eb1b0_0
82
+ - jsonschema=4.19.2=py38h06a4308_0
83
+ - jsonschema-specifications=2023.7.1=py38h06a4308_0
84
+ - jupyter-lsp=2.2.0=py38h06a4308_0
85
+ - jupyter_client=8.6.0=py38h06a4308_0
86
+ - jupyter_core=5.5.0=py38h06a4308_0
87
+ - jupyter_events=0.8.0=py38h06a4308_0
88
+ - jupyter_server=2.10.0=py38h06a4308_0
89
+ - jupyter_server_terminals=0.4.4=py38h06a4308_1
90
+ - jupyterlab=4.0.11=py38h06a4308_0
91
+ - jupyterlab_pygments=0.1.2=py_0
92
+ - jupyterlab_server=2.25.1=py38h06a4308_0
93
+ - kiwisolver=1.4.4=py38h6a678d5_0
94
+ - krb5=1.20.1=h143b758_1
95
+ - lame=3.100=h7b6447c_0
96
+ - lcms2=2.12=h3be6417_0
97
+ - ld_impl_linux-64=2.38=h1181459_1
98
+ - lerc=3.0=h295c915_0
99
+ - libaec=1.1.3=h59595ed_0
100
+ - libbrotlicommon=1.0.9=h5eee18b_7
101
+ - libbrotlidec=1.0.9=h5eee18b_7
102
+ - libbrotlienc=1.0.9=h5eee18b_7
103
+ - libclang=14.0.6=default_hc6dbbc7_1
104
+ - libclang13=14.0.6=default_he11475f_1
105
+ - libcublas=12.1.0.26=0
106
+ - libcufft=11.0.2.4=0
107
+ - libcufile=1.9.0.20=0
108
+ - libcups=2.4.2=h2d74bed_1
109
+ - libcurand=10.3.5.119=0
110
+ - libcurl=8.5.0=h251f7ec_0
111
+ - libcusolver=11.4.4.55=0
112
+ - libcusparse=12.0.2.55=0
113
+ - libdeflate=1.17=h5eee18b_1
114
+ - libedit=3.1.20230828=h5eee18b_0
115
+ - libev=4.33=h7f8727e_1
116
+ - libffi=3.4.4=h6a678d5_0
117
+ - libgcc-ng=13.2.0=h77fa898_7
118
+ - libgfortran-ng=13.2.0=h69a702a_7
119
+ - libgfortran5=13.2.0=hca663fb_7
120
+ - libglib=2.78.4=hdc74915_0
121
+ - libglu=9.0.0=hac7e632_1003
122
+ - libgomp=13.2.0=h77fa898_7
123
+ - libiconv=1.17=hd590300_2
124
+ - libidn2=2.3.4=h5eee18b_0
125
+ - libjpeg-turbo=2.0.0=h9bf148f_0
126
+ - libllvm14=14.0.6=hdb19cb5_3
127
+ - libnetcdf=4.9.2=nompi_h9612171_113
128
+ - libnghttp2=1.57.0=h2d74bed_0
129
+ - libnpp=12.0.2.50=0
130
+ - libnvjitlink=12.1.105=0
131
+ - libnvjpeg=12.1.1.14=0
132
+ - libpng=1.6.43=h2797004_0
133
+ - libpq=12.17=hdbd6064_0
134
+ - libsodium=1.0.18=h7b6447c_0
135
+ - libssh2=1.10.0=hdbd6064_2
136
+ - libstdcxx-ng=13.2.0=hc0a3c3a_7
137
+ - libtasn1=4.19.0=h5eee18b_0
138
+ - libtiff=4.5.1=h6a678d5_0
139
+ - libunistring=0.9.10=h27cfd23_0
140
+ - libuuid=2.38.1=h0b41bf4_0
141
+ - libwebp-base=1.3.2=h5eee18b_0
142
+ - libxcb=1.15=h7f8727e_0
143
+ - libxkbcommon=1.7.0=h662e7e4_0
144
+ - libxml2=2.12.7=hc051c1a_1
145
+ - libzip=1.10.1=h2629f0a_3
146
+ - libzlib=1.2.13=h4ab18f5_6
147
+ - llvm-openmp=14.0.6=h9e868ea_0
148
+ - lz4-c=1.9.4=h6a678d5_0
149
+ - markupsafe=2.1.3=py38h5eee18b_0
150
+ - matplotlib=3.7.2=py38h06a4308_0
151
+ - matplotlib-base=3.7.2=py38h1128e8f_0
152
+ - matplotlib-inline=0.1.6=py38h06a4308_0
153
+ - mistune=2.0.4=py38h06a4308_0
154
+ - mkl=2023.1.0=h213fc3f_46344
155
+ - mkl-service=2.4.0=py38h5eee18b_1
156
+ - mkl_fft=1.3.8=py38h5eee18b_0
157
+ - mkl_random=1.2.4=py38hdb19cb5_0
158
+ - mpc=1.1.0=h10f8cd9_1
159
+ - mpfr=4.0.2=hb69a4c5_1
160
+ - mpmath=1.3.0=py38h06a4308_0
161
+ - munkres=1.1.4=py_0
162
+ - mysql=5.7.24=h721c034_2
163
+ - nbclient=0.8.0=py38h06a4308_0
164
+ - nbconvert=7.10.0=py38h06a4308_0
165
+ - nbformat=5.9.2=py38h06a4308_0
166
+ - ncurses=6.4=h6a678d5_0
167
+ - nest-asyncio=1.6.0=py38h06a4308_0
168
+ - nettle=3.7.3=hbbd107a_1
169
+ - networkx=3.1=py38h06a4308_0
170
+ - notebook-shim=0.2.3=py38h06a4308_0
171
+ - numexpr=2.8.4=py38hc78ab66_1
172
+ - numpy=1.24.3=py38hf6e8229_1
173
+ - numpy-base=1.24.3=py38h060ed82_1
174
+ - openh264=2.1.1=h4ff587b_0
175
+ - openjpeg=2.4.0=h3ad879b_0
176
+ - openssl=3.3.1=h4ab18f5_0
177
+ - overrides=7.4.0=py38h06a4308_0
178
+ - packaging=23.1=py38h06a4308_0
179
+ - pandas=2.0.3=py38h1128e8f_0
180
+ - pandocfilters=1.5.0=pyhd3eb1b0_0
181
+ - parso=0.8.3=pyhd3eb1b0_0
182
+ - pcre2=10.42=hebb0a14_0
183
+ - pexpect=4.8.0=pyhd3eb1b0_3
184
+ - pickleshare=0.7.5=pyhd3eb1b0_1003
185
+ - pillow=10.2.0=py38h5eee18b_0
186
+ - pip=23.3.1=py38h06a4308_0
187
+ - pkgutil-resolve-name=1.3.10=py38h06a4308_1
188
+ - platformdirs=3.10.0=py38h06a4308_0
189
+ - ply=3.11=py38_0
190
+ - pooch=1.7.0=py38h06a4308_0
191
+ - proj=8.2.1=ha227179_0
192
+ - prometheus_client=0.14.1=py38h06a4308_0
193
+ - prompt-toolkit=3.0.43=py38h06a4308_0
194
+ - psutil=5.9.0=py38h5eee18b_0
195
+ - ptyprocess=0.7.0=pyhd3eb1b0_2
196
+ - pure_eval=0.2.2=pyhd3eb1b0_0
197
+ - pycparser=2.21=pyhd3eb1b0_0
198
+ - pygments=2.15.1=py38h06a4308_1
199
+ - pyparsing=3.0.9=py38h06a4308_0
200
+ - pyproj=3.4.1=py38hb83c990_0
201
+ - pyqt=5.15.10=py38h6a678d5_0
202
+ - pyqt5-sip=12.13.0=py38h5eee18b_0
203
+ - pyshp=2.3.1=py38h06a4308_0
204
+ - pysocks=1.7.1=py38h06a4308_0
205
+ - python=3.8.18=h955ad1f_0
206
+ - python-dateutil=2.8.2=pyhd3eb1b0_0
207
+ - python-eccodes=1.7.0=py38he82f83a_2
208
+ - python-fastjsonschema=2.16.2=py38h06a4308_0
209
+ - python-json-logger=2.0.7=py38h06a4308_0
210
+ - python-tzdata=2023.3=pyhd3eb1b0_0
211
+ - python_abi=3.8=2_cp38
212
+ - pytorch=2.3.0=py3.8_cuda12.1_cudnn8.9.2_0
213
+ - pytorch-cuda=12.1=ha16c6d3_5
214
+ - pytorch-mutex=1.0=cuda
215
+ - pytz=2023.3.post1=py38h06a4308_0
216
+ - pyyaml=6.0.1=py38h5eee18b_0
217
+ - pyzmq=25.1.2=py38h6a678d5_0
218
+ - qt-main=5.15.2=h53bd1ea_10
219
+ - readline=8.2=h5eee18b_0
220
+ - referencing=0.30.2=py38h06a4308_0
221
+ - requests=2.31.0=py38h06a4308_1
222
+ - rfc3339-validator=0.1.4=py38h06a4308_0
223
+ - rfc3986-validator=0.1.1=py38h06a4308_0
224
+ - rpds-py=0.10.6=py38hb02cf49_0
225
+ - scipy=1.10.1=py38hf6e8229_1
226
+ - send2trash=1.8.2=py38h06a4308_0
227
+ - setuptools=68.2.2=py38h06a4308_0
228
+ - shapely=2.0.1=py38h0fbb895_0
229
+ - sip=6.7.12=py38h6a678d5_0
230
+ - six=1.16.0=pyhd3eb1b0_1
231
+ - snappy=1.2.0=hdb0a2a9_1
232
+ - sniffio=1.3.0=py38h06a4308_0
233
+ - soupsieve=2.5=py38h06a4308_0
234
+ - sqlite=3.41.2=h5eee18b_0
235
+ - stack_data=0.2.0=pyhd3eb1b0_0
236
+ - sympy=1.12=py38h06a4308_0
237
+ - tbb=2021.8.0=hdb19cb5_0
238
+ - terminado=0.17.1=py38h06a4308_0
239
+ - tinycss2=1.2.1=py38h06a4308_0
240
+ - tk=8.6.12=h1ccaba5_0
241
+ - tomli=2.0.1=py38h06a4308_0
242
+ - torchaudio=2.3.0=py38_cu121
243
+ - torchtriton=2.3.0=py38
244
+ - torchvision=0.18.0=py38_cu121
245
+ - tornado=6.3.3=py38h5eee18b_0
246
+ - tqdm=4.65.0=py38hb070fc8_0
247
+ - traitlets=5.7.1=py38h06a4308_0
248
+ - typing-extensions=4.9.0=py38h06a4308_1
249
+ - typing_extensions=4.9.0=py38h06a4308_1
250
+ - urllib3=2.1.0=py38h06a4308_1
251
+ - wcwidth=0.2.5=pyhd3eb1b0_0
252
+ - webencodings=0.5.1=py38_1
253
+ - websocket-client=0.58.0=py38h06a4308_4
254
+ - wheel=0.41.2=py38h06a4308_0
255
+ - xarray=2022.11.0=py38h06a4308_0
256
+ - xkeyboard-config=2.42=h4ab18f5_0
257
+ - xorg-kbproto=1.0.7=h7f98852_1002
258
+ - xorg-libx11=1.8.9=h8ee46fc_0
259
+ - xorg-libxau=1.0.11=hd590300_0
260
+ - xorg-libxext=1.3.4=h0b41bf4_2
261
+ - xorg-xextproto=7.3.0=h0b41bf4_1003
262
+ - xorg-xproto=7.0.31=h7f98852_1007
263
+ - xz=5.4.6=h5eee18b_0
264
+ - yaml=0.2.5=h7b6447c_0
265
+ - zeromq=4.3.5=h6a678d5_0
266
+ - zipp=3.17.0=py38h06a4308_0
267
+ - zlib=1.2.13=h4ab18f5_6
268
+ - zstd=1.5.5=hc292b87_0
269
+ - pip:
270
+ - aiohttp==3.9.3
271
+ - aiosignal==1.3.1
272
+ - appdirs==1.4.4
273
+ - asciitree==0.3.3
274
+ - async-timeout==4.0.3
275
+ - black==24.4.2
276
+ - cachetools==5.3.3
277
+ - cfgrib==0.9.12.0
278
+ - cftime==1.6.3
279
+ - click==8.1.7
280
+ - click-plugins==1.1.1
281
+ - cligj==0.7.2
282
+ - docker-pycreds==0.4.0
283
+ - eumdac==2.2.2
284
+ - fasteners==0.19
285
+ - fiona==1.9.6
286
+ - frozenlist==1.4.1
287
+ - fsspec==2024.3.1
288
+ - gcsfs==2024.3.1
289
+ - geographiclib==2.0
290
+ - geopandas==0.13.2
291
+ - geopy==2.4.1
292
+ - gitdb==4.0.11
293
+ - gitpython==3.1.42
294
+ - google-api-core==2.17.1
295
+ - google-auth==2.28.2
296
+ - google-auth-oauthlib==1.2.0
297
+ - google-cloud-core==2.4.1
298
+ - google-cloud-storage==2.16.0
299
+ - google-crc32c==1.5.0
300
+ - google-resumable-media==2.7.0
301
+ - googleapis-common-protos==1.63.0
302
+ - h5py==3.10.0
303
+ - huggingface-hub==0.22.2
304
+ - multidict==6.0.5
305
+ - mypy-extensions==1.0.0
306
+ - netcdf4==1.6.5
307
+ - numcodecs==0.12.1
308
+ - oauthlib==3.2.2
309
+ - pathspec==0.12.1
310
+ - plotly==5.20.0
311
+ - protobuf==4.25.3
312
+ - pyasn1==0.5.1
313
+ - pyasn1-modules==0.3.0
314
+ - ratelimiter==1.2.0.post0
315
+ - requests-oauthlib==1.4.0
316
+ - rsa==4.9
317
+ - safetensors==0.4.3
318
+ - scienceplots==2.1.1
319
+ - sentry-sdk==1.42.0
320
+ - setproctitle==1.3.3
321
+ - smmap==5.0.1
322
+ - tenacity==8.2.3
323
+ - timm==0.6.13
324
+ - tokenize-rt==5.2.0
325
+ - wandb==0.16.4
326
+ - yarl==1.9.4
327
+ - zarr==2.16.1
328
+ prefix: /home/azureuser/miniconda3/envs/npw
official-src/notebooks/data_demo.ipynb ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "f329e4f1-6dd9-4f2e-b5b3-0e8454832eee",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Dataset\n",
9
+ "This notebook provides an example of the data utilised to generate a forecast using Aardvark Weather. We explore a single timeslice containing all the observations required to generate a forecast. This sample data is the output of the loader `WeatherDatasetE2E` in `../aardvark/loaders.py`."
10
+ ]
11
+ },
12
+ {
13
+ "cell_type": "code",
14
+ "execution_count": 1,
15
+ "id": "c17595b2-a624-440b-a2be-6af8d3802f87",
16
+ "metadata": {},
17
+ "outputs": [],
18
+ "source": [
19
+ "import numpy as np\n",
20
+ "import pickle"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "execution_count": 2,
26
+ "id": "bc8b869d-33d4-45e5-b3ef-15d4e521e3d5",
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": [
30
+ "import matplotlib\n",
31
+ "from matplotlib import pyplot as plt\n",
32
+ "\n",
33
+ "matplotlib.rcParams['mathtext.fontset'] = 'stix'\n",
34
+ "matplotlib.rcParams['font.family'] = 'STIXGeneral'\n"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "markdown",
39
+ "id": "6de4a11f-4677-4552-8630-6b395c27012e",
40
+ "metadata": {},
41
+ "source": [
42
+ "## Data\n",
43
+ "Open a sample of data required to produce a forecast."
44
+ ]
45
+ },
46
+ {
47
+ "cell_type": "code",
48
+ "execution_count": 3,
49
+ "id": "0483c01d-2e08-469c-b914-42532cc3099b",
50
+ "metadata": {},
51
+ "outputs": [],
52
+ "source": [
53
+ "with open('../data/sample_data_final.pkl', 'rb') as fp:\n",
54
+ " data = pickle.load(fp)"
55
+ ]
56
+ },
57
+ {
58
+ "cell_type": "markdown",
59
+ "id": "83b7c966-fab5-47ce-a91c-d5419acaee40",
60
+ "metadata": {},
61
+ "source": [
62
+ "Multiple different datasets are utilised as input to create a forecast, each with multiple channels including observations and metadata. Example channels for each of these are plotted below. The plot_channel variable in each cell can be adjusted to visualise different channels. "
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": 4,
68
+ "id": "693aa212-c3ab-4eda-b07a-a6b262da627d",
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "# Set up longitude and latitude for plotting\n",
73
+ "lon = np.linspace(0,359,360)\n",
74
+ "lat = np.linspace(90,-90,181)\n",
75
+ "\n",
76
+ "lon_1p5 = np.linspace(0,359,240)\n",
77
+ "lat_1p5 = np.linspace(90,-90,121)"
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "markdown",
82
+ "id": "42a2524f-108a-4ed7-bc89-2fb91638a7aa",
83
+ "metadata": {},
84
+ "source": [
85
+ "## Satellite\n",
86
+ "First visualise the satellite data from HIRS, AMSU-A, AMSU-B, IASI and ASCAT"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "id": "1243f422-351a-458c-aaa9-4bf72a959678",
92
+ "metadata": {},
93
+ "source": [
94
+ "### HIRS"
95
+ ]
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "execution_count": null,
100
+ "id": "0d750dcc-cfef-4ca5-8f14-a7bae45980f4",
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": [
104
+ "fig = plt.figure()\n",
105
+ "plot_channel = 11\n",
106
+ "p = plt.contourf(\n",
107
+ " lon,\n",
108
+ " lat,\n",
109
+ " data[\"assimilation\"][\"hirs_current\"][0,...,plot_channel].cpu().T, \n",
110
+ " levels=100,\n",
111
+ " cmap=\"magma\")\n",
112
+ "cbar = fig.colorbar(p)\n",
113
+ "cbar.set_label('Normalised radiance') \n",
114
+ "plt.xlabel(\"Longitude\")\n",
115
+ "plt.ylabel(\"Latitude\")\n",
116
+ "plt.title(f\"HIRS channel {plot_channel}\")\n",
117
+ "plt.show()"
118
+ ]
119
+ },
120
+ {
121
+ "cell_type": "markdown",
122
+ "id": "d5c5e48e-3bab-461f-8425-9efa10c415e0",
123
+ "metadata": {},
124
+ "source": [
125
+ "### AMSU-A"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": null,
131
+ "id": "79deb284-7ed8-47d2-9006-a13a06d2b835",
132
+ "metadata": {},
133
+ "outputs": [],
134
+ "source": [
135
+ "fig = plt.figure()\n",
136
+ "plot_channel = 8\n",
137
+ "p = plt.contourf(\n",
138
+ " lon, \n",
139
+ " lat[:-1],\n",
140
+ " data[\"assimilation\"][\"amsua_current\"][0,...,plot_channel].cpu(), \n",
141
+ " levels=100,\n",
142
+ " cmap=\"magma\")\n",
143
+ "cbar = fig.colorbar(p)\n",
144
+ "cbar.set_label('Normalised radiance') \n",
145
+ "plt.xlabel(\"Longitude\")\n",
146
+ "plt.ylabel(\"Latitude\")\n",
147
+ "plt.title(f\"AMSU-A channel {plot_channel}\")\n",
148
+ "plt.show()"
149
+ ]
150
+ },
151
+ {
152
+ "cell_type": "markdown",
153
+ "id": "9b481a5c-97ad-4016-826d-93771531ad2d",
154
+ "metadata": {},
155
+ "source": [
156
+ "### AMSU-B"
157
+ ]
158
+ },
159
+ {
160
+ "cell_type": "code",
161
+ "execution_count": null,
162
+ "id": "466434e7-7296-4454-b14c-6ab40bfbe0d1",
163
+ "metadata": {},
164
+ "outputs": [],
165
+ "source": [
166
+ "fig = plt.figure()\n",
167
+ "plot_channel = 10\n",
168
+ "p = plt.contourf(\n",
169
+ " lon,\n",
170
+ " lat,\n",
171
+ " data[\"assimilation\"][\"amsub_current\"][0,...,plot_channel].T.cpu(), \n",
172
+ " levels=100,\n",
173
+ " cmap=\"magma\")\n",
174
+ "cbar = fig.colorbar(p)\n",
175
+ "cbar.set_label('Normalised radiance') \n",
176
+ "plt.xlabel(\"Longitude\")\n",
177
+ "plt.ylabel(\"Latitude\")\n",
178
+ "plt.title(f\"AMSU-A channel {plot_channel}\")\n",
179
+ "plt.show()"
180
+ ]
181
+ },
182
+ {
183
+ "cell_type": "markdown",
184
+ "id": "44de7c3f-6a25-47eb-bcfe-ab7bc82a1aac",
185
+ "metadata": {},
186
+ "source": [
187
+ "### IASI"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "code",
192
+ "execution_count": null,
193
+ "id": "d5a5caf9-ecc4-403d-97ff-6c96dbc18f13",
194
+ "metadata": {},
195
+ "outputs": [],
196
+ "source": [
197
+ "fig = plt.figure()\n",
198
+ "plot_channel = 10\n",
199
+ "p = plt.contourf(\n",
200
+ " lon,\n",
201
+ " lat,\n",
202
+ " data[\"assimilation\"][\"iasi_current\"][0,...,plot_channel].T.cpu(), \n",
203
+ " levels=100,\n",
204
+ " cmap=\"magma\")\n",
205
+ "cbar = fig.colorbar(p)\n",
206
+ "cbar.set_label('Normalised radiance') \n",
207
+ "plt.xlabel(\"Longitude\")\n",
208
+ "plt.ylabel(\"Latitude\")\n",
209
+ "plt.title(f\"IASI channel {plot_channel}\")\n",
210
+ "plt.show()"
211
+ ]
212
+ },
213
+ {
214
+ "cell_type": "markdown",
215
+ "id": "ea0ac772-4150-4bfd-96de-004ac7b8b6b4",
216
+ "metadata": {},
217
+ "source": [
218
+ "### ASCAT"
219
+ ]
220
+ },
221
+ {
222
+ "cell_type": "code",
223
+ "execution_count": null,
224
+ "id": "9981b9c6-2d88-47a1-b135-f70f1335df78",
225
+ "metadata": {},
226
+ "outputs": [],
227
+ "source": [
228
+ "fig = plt.figure()\n",
229
+ "plot_channel = 5\n",
230
+ "p = plt.contourf(\n",
231
+ " lon,\n",
232
+ " lat,\n",
233
+ " data[\"assimilation\"][\"ascat_current\"][0,...,plot_channel].T.cpu(), \n",
234
+ " levels=100,\n",
235
+ " cmap=\"magma\")\n",
236
+ "cbar = fig.colorbar(p)\n",
237
+ "cbar.set_label('Normalised radiance') \n",
238
+ "plt.xlabel(\"Longitude\")\n",
239
+ "plt.ylabel(\"Latitude\")\n",
240
+ "plt.title(f\"ASCAT channel {plot_channel}\")\n",
241
+ "plt.show()"
242
+ ]
243
+ },
244
+ {
245
+ "cell_type": "markdown",
246
+ "id": "1b6849d1-c6fd-413f-a72e-3de111228a78",
247
+ "metadata": {},
248
+ "source": [
249
+ "## SYNOPS\n",
250
+ "We next visualise the SYNOPS data from land stations, marine platforms and radiosonde profiles"
251
+ ]
252
+ },
253
+ {
254
+ "cell_type": "markdown",
255
+ "id": "c392b951-b446-4fde-aacb-02be7759bc97",
256
+ "metadata": {},
257
+ "source": [
258
+ "### HadISD"
259
+ ]
260
+ },
261
+ {
262
+ "cell_type": "code",
263
+ "execution_count": null,
264
+ "id": "611eed9e-8c4a-4ac8-82f4-9969a6ef8eb7",
265
+ "metadata": {},
266
+ "outputs": [],
267
+ "source": [
268
+ "fig = plt.figure()\n",
269
+ "plot_channel = 0\n",
270
+ "p = plt.scatter(\n",
271
+ " data[\"assimilation\"][\"x_context_hadisd_current\"][plot_channel][0,0,:].cpu(),\n",
272
+ " data[\"assimilation\"][\"x_context_hadisd_current\"][plot_channel][0,1,:].cpu(),\n",
273
+ " c = data[\"assimilation\"][\"y_context_hadisd_current\"][plot_channel][0].T.cpu(), \n",
274
+ " cmap=\"magma\")\n",
275
+ "cbar = fig.colorbar(p)\n",
276
+ "cbar.set_label('Normalised value') \n",
277
+ "plt.xlabel(\"Longitude\")\n",
278
+ "plt.ylabel(\"Latitude\")\n",
279
+ "plt.title(f\"HadISD channel {plot_channel}\")\n",
280
+ "plt.show()"
281
+ ]
282
+ },
283
+ {
284
+ "cell_type": "markdown",
285
+ "id": "b2ae8c40-28bd-49ad-8a2f-b9f7df706834",
286
+ "metadata": {},
287
+ "source": [
288
+ "### ICOADS"
289
+ ]
290
+ },
291
+ {
292
+ "cell_type": "code",
293
+ "execution_count": null,
294
+ "id": "1e425818-5a30-4641-abc7-08cfd4c6b4fe",
295
+ "metadata": {},
296
+ "outputs": [],
297
+ "source": [
298
+ "plot_channel = 1\n",
299
+ "fig = plt.figure()\n",
300
+ "p = plt.scatter(\n",
301
+ " data[\"assimilation\"][\"icoads_x_current\"][0][0,:].cpu(),\n",
302
+ " data[\"assimilation\"][\"icoads_x_current\"][1][0,:].cpu(),\n",
303
+ " c = data[\"assimilation\"][\"icoads_current\"][0,plot_channel,:].T.cpu(), \n",
304
+ " cmap=\"magma\")\n",
305
+ "cbar = fig.colorbar(p)\n",
306
+ "cbar.set_label('Normalised value') \n",
307
+ "plt.xlabel(\"Longitude\")\n",
308
+ "plt.ylabel(\"Latitude\")\n",
309
+ "plt.title(f\"ICOADS channel {plot_channel}\")\n",
310
+ "plt.show()"
311
+ ]
312
+ },
313
+ {
314
+ "cell_type": "markdown",
315
+ "id": "14d103a9-053c-4576-b17f-35ebae3fa44a",
316
+ "metadata": {},
317
+ "source": [
318
+ "### IGRA"
319
+ ]
320
+ },
321
+ {
322
+ "cell_type": "code",
323
+ "execution_count": null,
324
+ "id": "11d5f5de-22ea-4d6e-a590-c1ce0a842a53",
325
+ "metadata": {},
326
+ "outputs": [],
327
+ "source": [
328
+ "plot_channel = 1\n",
329
+ "fig = plt.figure()\n",
330
+ "p = plt.scatter(\n",
331
+ " data[\"assimilation\"][\"igra_x_current\"][0][0,:].cpu(),\n",
332
+ " data[\"assimilation\"][\"igra_x_current\"][1][0,:].cpu(),\n",
333
+ " c = data[\"assimilation\"][\"igra_current\"][0,plot_channel,:].T.cpu(), \n",
334
+ " cmap=\"magma\")\n",
335
+ "cbar = fig.colorbar(p)\n",
336
+ "cbar.set_label('Normalised value') \n",
337
+ "plt.xlabel(\"Longitude\")\n",
338
+ "plt.ylabel(\"Latitude\")\n",
339
+ "plt.title(f\"IGRA channel {plot_channel}\")\n",
340
+ "plt.show()"
341
+ ]
342
+ },
343
+ {
344
+ "cell_type": "code",
345
+ "execution_count": null,
346
+ "id": "2b893a77-4c30-4f18-a7fb-b007a676d849",
347
+ "metadata": {},
348
+ "outputs": [],
349
+ "source": []
350
+ },
351
+ {
352
+ "cell_type": "code",
353
+ "execution_count": null,
354
+ "id": "521c93b6-babf-41fe-8dc6-c9a6eaef5b6f",
355
+ "metadata": {},
356
+ "outputs": [],
357
+ "source": []
358
+ }
359
+ ],
360
+ "metadata": {
361
+ "kernelspec": {
362
+ "display_name": "Python 3 (ipykernel)",
363
+ "language": "python",
364
+ "name": "python3"
365
+ },
366
+ "language_info": {
367
+ "codemirror_mode": {
368
+ "name": "ipython",
369
+ "version": 3
370
+ },
371
+ "file_extension": ".py",
372
+ "mimetype": "text/x-python",
373
+ "name": "python",
374
+ "nbconvert_exporter": "python",
375
+ "pygments_lexer": "ipython3",
376
+ "version": "3.8.18"
377
+ },
378
+ "vscode": {
379
+ "interpreter": {
380
+ "hash": "ff379d8b57ea10bc9abbe40475203c855480f57b5b4821ba344a2e4e1a8a455a"
381
+ }
382
+ }
383
+ },
384
+ "nbformat": 4,
385
+ "nbformat_minor": 5
386
+ }
official-src/notebooks/e2e_finetune_demo.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
official-src/notebooks/forecast_demo.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
official-src/training/downscaling_run_pipeline.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ python3 train_module.py \
4
+ --output_dir $3 \
5
+ --master_port 22348 \
6
+ --decoder base \
7
+ --loss downscaling_rmse \
8
+ --diff 0 \
9
+ --in_channels 36 \
10
+ --out_channels 1 \
11
+ --int_channels 24 \
12
+ --mode downscaling \
13
+ --lr 5e-4 \
14
+ --batch_size 64 \
15
+ --start_ind 0 \
16
+ --end_ind 24 \
17
+ --epoch 20 \
18
+ --weight_decay 1e-6 \
19
+ --downscaling_context aardvark \
20
+ --downscaling_train_start_date 2007-01-02 \
21
+ --downscaling_train_end_date 2017-12-31 \
22
+ --lead_time $1 \
23
+ --var $2
official-src/training/finetune.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ python3 ../aardvark/generate_initial_condition_single.py \
2
+ --assimilation_model_path ENCODER_PATH
3
+
4
+ python3 ../aardvark/finetune.py \
5
+ --assimilation_model_path ENCODER_PATH \
6
+ --forecast_model_path FORECAST_PATH \
7
+ --output_dir FINETUNE_PATH \
8
+ --lr 5e-5 \
9
+ --finetune_epochs 1
official-src/training/test/config.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:565f5a5ba9b564b2a63bcb30d20bd2f74045ef537ebf12a594a49eed577a5ace
3
+ size 387
official-src/training/train_decoder.sh ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python3 ../aardvark/train_module.py \
2
+ --output_dir DECODER/TAS \
3
+ --master_port 22348 \
4
+ --decoder base \
5
+ --loss downscaling_rmse \
6
+ --diff 0 \
7
+ --in_channels 36 \
8
+ --out_channels 1 \
9
+ --int_channels 24 \
10
+ --mode downscaling \
11
+ --lr 5e-4 \
12
+ --batch_size 64 \
13
+ --start_ind 0 \
14
+ --end_ind 24 \
15
+ --epoch 20 \
16
+ --weight_decay 1e-6 \
17
+ --downscaling_context aardvark \
18
+ --downscaling_train_start_date 2007-01-02 \
19
+ --downscaling_train_end_date 2017-12-31 \
20
+ --lead_time 4 \
21
+ --var tas
22
+
23
+ python3 ../aardvark/train_module.py \
24
+ --output_dir DECODER/WS \
25
+ --master_port 22348 \
26
+ --decoder base \
27
+ --loss downscaling_rmse \
28
+ --diff 0 \
29
+ --in_channels 36 \
30
+ --out_channels 1 \
31
+ --int_channels 24 \
32
+ --mode downscaling \
33
+ --lr 5e-4 \
34
+ --batch_size 64 \
35
+ --start_ind 0 \
36
+ --end_ind 24 \
37
+ --epoch 20 \
38
+ --weight_decay 1e-6 \
39
+ --downscaling_context aardvark \
40
+ --downscaling_train_start_date 2007-01-02 \
41
+ --downscaling_train_end_date 2017-12-31 \
42
+ --lead_time 4 \
43
+ --var ws
official-src/training/train_e2e.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Command for training with wind speed variable (ws)
2
+ python3 ../aardvark/e2e_train.py \
3
+ --output_dir E2E/WS \
4
+ --loss downscaling_rmse \
5
+ --region global \
6
+ --lead_time 1 \
7
+ --sf_model_path DECODER_PATH \
8
+ --se_model_path ENCODER_PATH \
9
+ --forecast_model_path PROCESSOR_PATH \
10
+ --var ws
11
+
12
+ # Command for training with temperature variable (tas)
13
+ python3 ../aardvark/e2e_train.py \
14
+ --output_dir E2E/TAS \
15
+ --loss downscaling_rmse \
16
+ --region global \
17
+ --lead_time 1 \
18
+ --sf_model_path DECODER_PATH \
19
+ --se_model_path ENCODER_PATH \
20
+ --forecast_model_path PROCESSOR_PATH \
21
+ --var tas
official-src/training/train_encoder.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python3 ../aardvark/train_module.py \
2
+ --output_dir ENCODER_PATH \
3
+ --master_port 12348 \
4
+ --decoder vit_assimilation \
5
+ --loss lw_rmse \
6
+ --diff 0 \
7
+ --in_channels 277 \
8
+ --out_channels 24 \
9
+ --int_channels 24 \
10
+ --mode assimilation \
11
+ --lr 5e-4 \
12
+ --batch_size 6 \
13
+ --start_ind 0 \
14
+ --end_ind 24 \
15
+ --epoch 100 \
16
+ --weight_per_variable 1
official-src/training/train_processor.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python3 ../aardvark/train_module.py \
2
+ --output_dir FORECAST_PATH/ \
3
+ --lead_time 1 \
4
+ --loss lw_rmse_pressure_weighted \
5
+ --diff 1 \
6
+ --mode forecast \
7
+ --in_channels 35 \
8
+ --out_channels 24 \
9
+ --int_channels 24 \
10
+ --decoder vit \
11
+ --ic era5 \
12
+ --film 0 \
13
+ --epoch 200 \
14
+ --weight_per_variable 0 \
15
+ --batch_size 24 \
16
+ --lr 5e-4 \
17
+ --era5_mode 4u
scripts/inference.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Run the one-day tas forecast through the official Aardvark modules."""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(ROOT))
11
+
12
+ import copy
13
+ import torch
14
+
15
+ from model.aardvark_adapter import build_one_day_model, load_sample
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser()
20
+ parser.add_argument("--root", type=Path, default=ROOT)
21
+ parser.add_argument("--device", default="cuda")
22
+ parser.add_argument("--checkpoint", type=Path, help="Optional checkpoint produced by scripts/train.py")
23
+ parser.add_argument("--output", type=Path, default=ROOT / "result" / "inference_one_day.json")
24
+ args = parser.parse_args()
25
+ root = args.root.resolve()
26
+ sample_path = root / "weights/sample_data/sample_data_final.pkl"
27
+ sample = load_sample(sample_path)
28
+ model = build_one_day_model(root / "weights", root / "official-src", args.device)
29
+ if args.checkpoint:
30
+ tuned = args.checkpoint.resolve()
31
+ payload = torch.load(tuned, map_location=args.device, weights_only=False)
32
+ target_model = model if payload["train_modules"] == "all" else model.sf_model
33
+ target_model.load_state_dict(payload["model"])
34
+ model.eval()
35
+ target = sample["y_target"].cpu()
36
+ with torch.inference_mode():
37
+ station, global_forecast, initial_state = model(copy.deepcopy(sample))
38
+ result_dir = root / "result"
39
+ result_dir.mkdir(parents=True, exist_ok=True)
40
+ torch.save(station.cpu(), result_dir / "prediction.pt")
41
+ torch.save(target, result_dir / "target.pt")
42
+ report = {
43
+ "device": args.device,
44
+ "lead_time_days": 1,
45
+ "station_tas_shape": list(station.shape),
46
+ "global_forecast_shape": list(global_forecast.shape),
47
+ "initial_state_shape": list(initial_state.shape),
48
+ "finite_outputs": bool(torch.isfinite(station).all()),
49
+ "weights": str(tuned) if args.checkpoint else "official",
50
+ }
51
+ args.output.parent.mkdir(parents=True, exist_ok=True)
52
+ args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
53
+ print(json.dumps(report, indent=2, sort_keys=True))
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()