Upload folder using huggingface_hub
Browse files- README.md +151 -0
- conf/config.yaml +76 -0
- configuration.json +1 -0
- model/common.py +161 -0
- model/data_loader.py +359 -0
- model/gencast.py +194 -0
- model/graphcast/autoregressive.py +312 -0
- model/graphcast/casting.py +205 -0
- model/graphcast/checkpoint.py +170 -0
- model/graphcast/data_utils.py +362 -0
- model/graphcast/deep_typed_graph_net.py +461 -0
- model/graphcast/denoiser.py +852 -0
- model/graphcast/denoisers_base.py +53 -0
- model/graphcast/dpm_solver_plus_plus_2s.py +187 -0
- model/graphcast/gencast.py +284 -0
- model/graphcast/graphcast.py +796 -0
- model/graphcast/grid_mesh_connectivity.py +133 -0
- model/graphcast/icosahedral_mesh.py +285 -0
- model/graphcast/losses.py +179 -0
- model/graphcast/mlp.py +45 -0
- model/graphcast/model_utils.py +807 -0
- model/graphcast/nan_cleaning.py +125 -0
- model/graphcast/normalization.py +196 -0
- model/graphcast/predictor_base.py +170 -0
- model/graphcast/rollout.py +498 -0
- model/graphcast/samplers_base.py +47 -0
- model/graphcast/samplers_utils.py +436 -0
- model/graphcast/solar_radiation.py +605 -0
- model/graphcast/sparse_transformer.py +577 -0
- model/graphcast/sparse_transformer_utils.py +76 -0
- model/graphcast/transformer.py +124 -0
- model/graphcast/typed_graph.py +97 -0
- model/graphcast/typed_graph_net.py +317 -0
- model/graphcast/xarray_jax.py +1080 -0
- model/graphcast/xarray_tree.py +70 -0
- scripts/fake_data.py +115 -0
- scripts/inference.py +158 -0
- scripts/result.py +87 -0
- scripts/train.py +325 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: JAX
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
license: apache-2.0
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth Science
|
| 9 |
+
- Weather Forecasting
|
| 10 |
+
- Ensemble Forecasting
|
| 11 |
+
- ERA5
|
| 12 |
+
tasks: []
|
| 13 |
+
datasets:
|
| 14 |
+
- OneScience/ERA5
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">GenCast</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Overview
|
| 24 |
+
|
| 25 |
+
GenCast is a probabilistic global weather forecasting model developed by Google DeepMind. Its paper appeared as the cover article of the leading scientific journal *Nature* on December 4, 2024.
|
| 26 |
+
|
| 27 |
+
Paper: *GenCast: Diffusion-Based Ensemble Forecasting for Medium-Range Weather*
|
| 28 |
+
|
| 29 |
+
https://arxiv.org/abs/2312.15796
|
| 30 |
+
|
| 31 |
+
# Model Description
|
| 32 |
+
|
| 33 |
+
GenCast is an ensemble forecasting model built with graph neural networks and diffusion models. Across a comprehensive set of evaluations, it outperformed ENS, the European Centre for Medium-Range Weather Forecasts' (ECMWF) leading ensemble forecasting system.
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Use Cases
|
| 37 |
+
|
| 38 |
+
| Use Case | Description |
|
| 39 |
+
| :---: | :--- |
|
| 40 |
+
| Weather forecasting training | Train the model on ERA5 data in HDF5 format that conforms to the GenCast data protocol. |
|
| 41 |
+
| Quick local validation | Use synthetic data to validate data loading, model training and inference, and visualization of inference results. |
|
| 42 |
+
| ModelScope/OneCode execution | Download the standalone model package, install its dependencies, and run the included scripts directly. |
|
| 43 |
+
| Multi-GPU training | Use JAX `pmap` for data-parallel training across multiple GPUs or accelerators on a single host. |
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# Usage
|
| 47 |
+
|
| 48 |
+
## 1. Using OneCode
|
| 49 |
+
|
| 50 |
+
Use the OneCode online environment for an intelligent, one-click AI4S development experience:
|
| 51 |
+
|
| 52 |
+
[Try one-click AI4S development with OneCode](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 53 |
+
|
| 54 |
+
## 2. Manual Setup
|
| 55 |
+
|
| 56 |
+
**Hardware Requirements**
|
| 57 |
+
|
| 58 |
+
- A GPU or DCU is recommended.
|
| 59 |
+
- A CPU can be used for import checks and connectivity validation with a minimal configuration, but full training and inference will be slow.
|
| 60 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or later is recommended; alternatively, use the OneScience-recommended version compatible with your cluster.
|
| 61 |
+
|
| 62 |
+
### Download the Model Package
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
hf download --model OneScience-Group/GenCast --local-dir ./GenCast
|
| 66 |
+
cd GenCast
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
### Set Up the Runtime Environment
|
| 70 |
+
|
| 71 |
+
**DCU Environment**
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# Activate DTK and conda first.
|
| 75 |
+
conda create -n onescience311 python=3.11 -y
|
| 76 |
+
conda activate onescience311
|
| 77 |
+
# Installation with uv is also supported.
|
| 78 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
**GPU Environment**
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
# Activate conda first.
|
| 85 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 86 |
+
conda activate onescience311
|
| 87 |
+
# Installation with uv is also supported.
|
| 88 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Training Data
|
| 92 |
+
|
| 93 |
+
The OneScience community provides ERA5 data for training. Because of file-size constraints, the repository currently contains a self-contained data slice. Download the data with the following command and ensure that the data path in `conf/config.yaml` is configured correctly:
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
hf download --dataset OneScience-Group/ERA5 --local-dir ./data
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
### Training
|
| 101 |
+
|
| 102 |
+
Single GPU:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# If real data is unavailable, first run `python scripts/fake_data.py` to generate synthetic data.
|
| 106 |
+
python scripts/train.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Multiple GPUs:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
CUDA_VISIBLE_DEVICES=0,1 python scripts/train.py --config conf/config.yaml --parallel-mode pmap --num-devices 2 --global-batch-size 2
|
| 113 |
+
# CUDA_VISIBLE_DEVICES specifies the GPU indices to expose.
|
| 114 |
+
# --num-devices specifies the number of GPUs to use.
|
| 115 |
+
# --global-batch-size specifies the batch size and must be divisible by the number of GPUs.
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
After training, the weights are saved to `data/checkpoints/model_bak.npz`.
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
### Pre-trained Weights
|
| 122 |
+
|
| 123 |
+
This repository will provide weights trained on ERA5 reanalysis data in the `weights/` directory. The weight files are being prepared and will be uploaded soon.
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
### Inference
|
| 127 |
+
|
| 128 |
+
By default, inference loads `data/checkpoints/model_bak.npz`:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
python scripts/inference.py
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### Evaluation and Visualization
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
python scripts/result.py
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# Official OneScience Resources
|
| 142 |
+
|
| 143 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 144 |
+
| --- | --- | --- |
|
| 145 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 146 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# Citation and License
|
| 150 |
+
|
| 151 |
+
- This repository is a reproduction of the original GenCast paper.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
runtime:
|
| 2 |
+
platform: "auto"
|
| 3 |
+
|
| 4 |
+
data:
|
| 5 |
+
data_dir: "./data"
|
| 6 |
+
static_dir: "./data/static"
|
| 7 |
+
stats_dir: "./data/stats"
|
| 8 |
+
train_years: [2000, 2001]
|
| 9 |
+
test_years: [2003]
|
| 10 |
+
train_stride: 1
|
| 11 |
+
test_stride: 1
|
| 12 |
+
reintroduce_sst_nans: true
|
| 13 |
+
# Source total_precipitation must be accumulated over each source interval,
|
| 14 |
+
# in the same units as the official GenCast statistics.
|
| 15 |
+
precipitation_interval_hours: 6
|
| 16 |
+
|
| 17 |
+
# Full-scale random initialization. For released weights, inference ignores
|
| 18 |
+
# this section and loads every architecture field from the official checkpoint.
|
| 19 |
+
model:
|
| 20 |
+
mesh_size: 4 # GenCast Mini (2562 nodes); 6=full (40962 nodes, TPU-scale)
|
| 21 |
+
latent_size: 512
|
| 22 |
+
hidden_layers: 1
|
| 23 |
+
radius_query_fraction_edge_length: 0.6
|
| 24 |
+
attention_k_hop: 16
|
| 25 |
+
attention_type: "triblockdiag_mha" # GPU/CPU; use "splash_mha" only for TPU
|
| 26 |
+
mask_type: "lazy"
|
| 27 |
+
num_layers: 16
|
| 28 |
+
num_heads: 4
|
| 29 |
+
ffw_hidden: 2048
|
| 30 |
+
|
| 31 |
+
sampler:
|
| 32 |
+
max_noise_level: 80.0
|
| 33 |
+
min_noise_level: 0.03
|
| 34 |
+
num_noise_levels: 20
|
| 35 |
+
rho: 7.0
|
| 36 |
+
stochastic_churn_rate: 2.5
|
| 37 |
+
churn_min_noise_level: 0.75
|
| 38 |
+
churn_max_noise_level: .inf
|
| 39 |
+
noise_level_inflation_factor: 1.05
|
| 40 |
+
|
| 41 |
+
training:
|
| 42 |
+
max_steps: 10
|
| 43 |
+
learning_rate: 0.0001
|
| 44 |
+
betas: [0.9, 0.999]
|
| 45 |
+
epsilon: 1.0e-8
|
| 46 |
+
seed: 42
|
| 47 |
+
save_interval: 1000
|
| 48 |
+
|
| 49 |
+
parallel:
|
| 50 |
+
mode: "pmap"
|
| 51 |
+
num_devices: 1
|
| 52 |
+
global_batch_size: 1
|
| 53 |
+
axis_name: "devices"
|
| 54 |
+
|
| 55 |
+
inference:
|
| 56 |
+
official_checkpoint: null
|
| 57 |
+
# Officially documented GPU substitution for TPU splash attention.
|
| 58 |
+
attention_type_override: null
|
| 59 |
+
prediction_steps: 30
|
| 60 |
+
num_members: 4
|
| 61 |
+
seed: 42
|
| 62 |
+
# Keep full-resolution ensemble memory bounded by writing each lead/member.
|
| 63 |
+
stream_chunks: true
|
| 64 |
+
|
| 65 |
+
checkpoint:
|
| 66 |
+
trainer: "./data/checkpoints/model_bak.npz"
|
| 67 |
+
resume: null
|
| 68 |
+
|
| 69 |
+
output:
|
| 70 |
+
prediction: "./result/prediction.nc"
|
| 71 |
+
plot: "./result/gencast_forecast.png"
|
| 72 |
+
|
| 73 |
+
fake_data:
|
| 74 |
+
height: 9
|
| 75 |
+
width: 16
|
| 76 |
+
timesteps: 8
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/common.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import xarray
|
| 10 |
+
import yaml
|
| 11 |
+
|
| 12 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def load_config(path: str | Path) -> dict[str, Any]:
|
| 16 |
+
with Path(path).open(encoding="utf-8") as source:
|
| 17 |
+
return yaml.safe_load(source)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def resolve_path(path: str | Path) -> Path:
|
| 21 |
+
candidate = Path(path).expanduser()
|
| 22 |
+
return candidate if candidate.is_absolute() else PROJECT_ROOT / candidate
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def configure_jax(platform: str) -> None:
|
| 26 |
+
if platform != "auto" and "JAX_PLATFORM_NAME" not in os.environ:
|
| 27 |
+
os.environ["JAX_PLATFORM_NAME"] = platform
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def load_stats(stats_dir: str | Path) -> dict[str, xarray.Dataset]:
|
| 31 |
+
directory = resolve_path(stats_dir)
|
| 32 |
+
names = (
|
| 33 |
+
"diffs_stddev_by_level",
|
| 34 |
+
"mean_by_level",
|
| 35 |
+
"stddev_by_level",
|
| 36 |
+
"min_by_level",
|
| 37 |
+
)
|
| 38 |
+
stats = {}
|
| 39 |
+
for name in names:
|
| 40 |
+
path = directory / f"{name}.nc"
|
| 41 |
+
if not path.exists():
|
| 42 |
+
raise FileNotFoundError(f"Missing GenCast statistic: {path}")
|
| 43 |
+
stats[name] = xarray.load_dataset(path).compute()
|
| 44 |
+
from model.graphcast import gencast, graphcast
|
| 45 |
+
|
| 46 |
+
inputs = set(gencast.TASK.input_variables) - set(graphcast.GENERATED_FORCING_VARS)
|
| 47 |
+
targets = set(gencast.TASK.target_variables)
|
| 48 |
+
required_by_stat = {
|
| 49 |
+
"mean_by_level": inputs | (targets - inputs),
|
| 50 |
+
"stddev_by_level": inputs | (targets - inputs),
|
| 51 |
+
"diffs_stddev_by_level": targets & inputs,
|
| 52 |
+
"min_by_level": {"sea_surface_temperature"},
|
| 53 |
+
}
|
| 54 |
+
for stat_name, dataset in stats.items():
|
| 55 |
+
missing = sorted(required_by_stat[stat_name] - set(dataset.data_vars))
|
| 56 |
+
if missing:
|
| 57 |
+
raise ValueError(f"{stat_name} is missing GenCast variables: {missing}")
|
| 58 |
+
for name, values in dataset.data_vars.items():
|
| 59 |
+
array = np.asarray(values)
|
| 60 |
+
if not np.all(np.isfinite(array)):
|
| 61 |
+
raise ValueError(f"{stat_name}.{name} contains non-finite values")
|
| 62 |
+
if "level" in values.dims and tuple(values.level.values) != tuple(
|
| 63 |
+
gencast.TASK.pressure_levels
|
| 64 |
+
):
|
| 65 |
+
raise ValueError(f"{stat_name}.{name} does not use GenCast WB13 order")
|
| 66 |
+
if stat_name in ("stddev_by_level", "diffs_stddev_by_level") and np.any(array <= 0):
|
| 67 |
+
raise ValueError(f"{stat_name}.{name} must be strictly positive")
|
| 68 |
+
return stats
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def save_trainer_checkpoint(
|
| 72 |
+
path: str | Path,
|
| 73 |
+
*,
|
| 74 |
+
params: Any,
|
| 75 |
+
state: Any,
|
| 76 |
+
optimizer_state: Any,
|
| 77 |
+
step: int,
|
| 78 |
+
config: dict[str, Any],
|
| 79 |
+
) -> None:
|
| 80 |
+
import jax
|
| 81 |
+
|
| 82 |
+
destination = resolve_path(path)
|
| 83 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 84 |
+
leaves, treedef = jax.tree_util.tree_flatten(
|
| 85 |
+
{"params": params, "state": state, "optimizer_state": optimizer_state}
|
| 86 |
+
)
|
| 87 |
+
arrays = {f"leaf_{i}": np.asarray(value) for i, value in enumerate(leaves)}
|
| 88 |
+
arrays["treedef"] = np.array([treedef], dtype=object)
|
| 89 |
+
arrays["step"] = np.asarray(step, dtype=np.int64)
|
| 90 |
+
arrays["config_json"] = np.asarray(json.dumps(config, sort_keys=True))
|
| 91 |
+
temporary = destination.with_suffix(destination.suffix + ".tmp")
|
| 92 |
+
with temporary.open("wb") as output:
|
| 93 |
+
np.savez(output, **arrays)
|
| 94 |
+
os.replace(temporary, destination)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def load_trainer_checkpoint(
|
| 98 |
+
path: str | Path,
|
| 99 |
+
) -> tuple[Any, Any, Any, int, dict[str, Any]]:
|
| 100 |
+
import jax
|
| 101 |
+
|
| 102 |
+
source_path = resolve_path(path)
|
| 103 |
+
with np.load(source_path, allow_pickle=True) as source:
|
| 104 |
+
treedef = source["treedef"].item()
|
| 105 |
+
leaves = [source[f"leaf_{i}"] for i in range(len(source.files) - 3)]
|
| 106 |
+
tree = jax.tree_util.tree_unflatten(treedef, leaves)
|
| 107 |
+
saved_config = json.loads(str(source["config_json"]))
|
| 108 |
+
return (
|
| 109 |
+
tree["params"], tree["state"], tree["optimizer_state"],
|
| 110 |
+
int(source["step"]), saved_config,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def validate_checkpoint_config(
|
| 115 |
+
current: dict[str, Any],
|
| 116 |
+
saved: dict[str, Any],
|
| 117 |
+
*,
|
| 118 |
+
scope: str = "resume",
|
| 119 |
+
) -> None:
|
| 120 |
+
"""Validate checkpoint compatibility for training resume or inference."""
|
| 121 |
+
if scope not in ("resume", "inference"):
|
| 122 |
+
raise ValueError("scope must be 'resume' or 'inference'")
|
| 123 |
+
|
| 124 |
+
inference_paths = (
|
| 125 |
+
("model",), ("sampler",), ("data", "stats_dir"),
|
| 126 |
+
("data", "static_dir"), ("data", "precipitation_interval_hours"),
|
| 127 |
+
)
|
| 128 |
+
resume_only_paths = (
|
| 129 |
+
("training", "learning_rate"),
|
| 130 |
+
("training", "betas"), ("training", "epsilon"),
|
| 131 |
+
("training", "seed"), ("data", "data_dir"),
|
| 132 |
+
("data", "train_years"), ("data", "train_stride"),
|
| 133 |
+
("parallel", "mode"), ("parallel", "num_devices"),
|
| 134 |
+
("parallel", "global_batch_size"), ("parallel", "axis_name"),
|
| 135 |
+
)
|
| 136 |
+
if scope == "resume":
|
| 137 |
+
if "parallel" not in saved:
|
| 138 |
+
saved = dict(saved)
|
| 139 |
+
saved["parallel"] = {
|
| 140 |
+
"mode": "single",
|
| 141 |
+
"num_devices": 1,
|
| 142 |
+
"global_batch_size": 1,
|
| 143 |
+
"axis_name": "devices",
|
| 144 |
+
}
|
| 145 |
+
paths = inference_paths + resume_only_paths
|
| 146 |
+
else:
|
| 147 |
+
paths = inference_paths
|
| 148 |
+
|
| 149 |
+
for path in paths:
|
| 150 |
+
current_value: Any = current
|
| 151 |
+
saved_value: Any = saved
|
| 152 |
+
for key in path:
|
| 153 |
+
current_value = current_value[key]
|
| 154 |
+
saved_value = saved_value[key]
|
| 155 |
+
if current_value != saved_value:
|
| 156 |
+
name = ".".join(path)
|
| 157 |
+
raise ValueError(
|
| 158 |
+
f"Trainer checkpoint configuration mismatch for {name} "
|
| 159 |
+
f"during {scope}: "
|
| 160 |
+
f"saved={saved_value!r}, current={current_value!r}"
|
| 161 |
+
)
|
model/data_loader.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""将 ERA5 HDF5 严格适配为官方 GenCast xarray 数据协议。"""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import bisect
|
| 6 |
+
import datetime as dt
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Iterator
|
| 9 |
+
|
| 10 |
+
import h5py
|
| 11 |
+
import numpy as np
|
| 12 |
+
import xarray
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from onescience.datapipes.climate import ERA5Dataset as _ERA5Dataset
|
| 16 |
+
except ModuleNotFoundError as error:
|
| 17 |
+
if error.name not in ("torch", "onescience"):
|
| 18 |
+
raise
|
| 19 |
+
|
| 20 |
+
class _ERA5Dataset:
|
| 21 |
+
"""Minimal discovery fallback for JAX-only OneScience environments."""
|
| 22 |
+
|
| 23 |
+
def __init__(self, dataset_dir, used_years, used_variables, **_):
|
| 24 |
+
self.dataset_dir = dataset_dir
|
| 25 |
+
self.used_years = used_years
|
| 26 |
+
self.used_variables = used_variables
|
| 27 |
+
self._init_avail_samples()
|
| 28 |
+
self._init_normalized_files()
|
| 29 |
+
|
| 30 |
+
def _init_avail_samples(self):
|
| 31 |
+
data_dir = Path(self.dataset_dir) / "data"
|
| 32 |
+
available = {int(path.stem): path for path in data_dir.glob("*.h5")}
|
| 33 |
+
missing_years = sorted(set(self.used_years) - set(available))
|
| 34 |
+
if missing_years:
|
| 35 |
+
raise ValueError(f"Years not found in dataset: {missing_years}")
|
| 36 |
+
first = available[self.used_years[0]]
|
| 37 |
+
with h5py.File(first, "r") as source:
|
| 38 |
+
fields = source["fields"]
|
| 39 |
+
variables = [
|
| 40 |
+
value.decode() if isinstance(value, bytes) else str(value)
|
| 41 |
+
for value in fields.attrs["variables"]
|
| 42 |
+
]
|
| 43 |
+
self.T, self.C, self.H, self.W = fields.shape
|
| 44 |
+
self.time_step = int(fields.attrs["time_step"])
|
| 45 |
+
missing_variables = sorted(set(self.used_variables) - set(variables))
|
| 46 |
+
if missing_variables:
|
| 47 |
+
raise ValueError(f"Variables not found in dataset: {missing_variables}")
|
| 48 |
+
self.file_map = {year: str(available[year]) for year in self.used_years}
|
| 49 |
+
|
| 50 |
+
def _init_normalized_files(self):
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
from model.graphcast import data_utils
|
| 54 |
+
from model.graphcast import gencast
|
| 55 |
+
from model.graphcast import graphcast
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
PRESSURE_LEVELS = tuple(graphcast.PRESSURE_LEVELS_WEATHERBENCH_13)
|
| 59 |
+
SURFACE_VARIABLES = tuple(gencast.TARGET_SURFACE_NO_PRECIP_VARS)
|
| 60 |
+
ATMOSPHERIC_VARIABLES = tuple(graphcast.TARGET_ATMOSPHERIC_VARS)
|
| 61 |
+
STATIC_VARIABLES = tuple(graphcast.STATIC_VARS)
|
| 62 |
+
RAW_PRECIPITATION = "total_precipitation"
|
| 63 |
+
TARGET_PRECIPITATION = "total_precipitation_12hr"
|
| 64 |
+
MODEL_TARGET_CHANNELS = 6 + 6 * len(PRESSURE_LEVELS)
|
| 65 |
+
ERA5_VARIABLES = (
|
| 66 |
+
*SURFACE_VARIABLES,
|
| 67 |
+
RAW_PRECIPITATION,
|
| 68 |
+
*(f"{name}_{level}" for name in ATMOSPHERIC_VARIABLES for level in PRESSURE_LEVELS),
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def expected_target_channel_names() -> tuple[str, ...]:
|
| 73 |
+
channels: list[str] = []
|
| 74 |
+
atmospheric = set(ATMOSPHERIC_VARIABLES)
|
| 75 |
+
for name in sorted(gencast.TASK.target_variables):
|
| 76 |
+
if name in atmospheric:
|
| 77 |
+
channels.extend(f"{name}_{level}" for level in PRESSURE_LEVELS)
|
| 78 |
+
else:
|
| 79 |
+
channels.append(name)
|
| 80 |
+
return tuple(channels)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class GenCastERA5Dataset(_ERA5Dataset):
|
| 84 |
+
"""Reuse ERA5Dataset discovery while enforcing GenCast's named protocol."""
|
| 85 |
+
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
dataset_dir: str | Path,
|
| 89 |
+
used_years: list[int],
|
| 90 |
+
*,
|
| 91 |
+
static_dir: str | Path | None = None,
|
| 92 |
+
prediction_steps: int = 1,
|
| 93 |
+
stride: int = 1,
|
| 94 |
+
task_config: Any = gencast.TASK,
|
| 95 |
+
precipitation_interval_hours: int = 6,
|
| 96 |
+
load_future_targets: bool = True,
|
| 97 |
+
) -> None:
|
| 98 |
+
super().__init__(
|
| 99 |
+
dataset_dir=str(dataset_dir),
|
| 100 |
+
used_years=used_years,
|
| 101 |
+
used_variables=list(ERA5_VARIABLES),
|
| 102 |
+
input_steps=1,
|
| 103 |
+
output_steps=1,
|
| 104 |
+
normalize=False,
|
| 105 |
+
)
|
| 106 |
+
self.static_dir = Path(static_dir or Path(dataset_dir) / "static")
|
| 107 |
+
self.prediction_steps = int(prediction_steps)
|
| 108 |
+
self.stride = int(stride)
|
| 109 |
+
self.task_config = task_config
|
| 110 |
+
self.precipitation_interval_hours = int(precipitation_interval_hours)
|
| 111 |
+
self.load_future_targets = bool(load_future_targets)
|
| 112 |
+
self._validate_task_config()
|
| 113 |
+
if self.prediction_steps < 1 or self.stride < 1:
|
| 114 |
+
raise ValueError("prediction_steps and stride must be positive")
|
| 115 |
+
self._inspect_years()
|
| 116 |
+
|
| 117 |
+
def _init_normalized_files(self) -> None:
|
| 118 |
+
# GenCast uses named by-level NetCDF statistics in the model wrapper.
|
| 119 |
+
self.mu = self.sd = None
|
| 120 |
+
|
| 121 |
+
def _inspect_years(self) -> None:
|
| 122 |
+
self._year_meta: list[dict[str, Any]] = []
|
| 123 |
+
self._cumulative: list[int] = []
|
| 124 |
+
total = 0
|
| 125 |
+
for year in self.used_years:
|
| 126 |
+
path = Path(self.file_map[year])
|
| 127 |
+
with h5py.File(path, "r") as source:
|
| 128 |
+
fields = source["fields"]
|
| 129 |
+
variables = [
|
| 130 |
+
value.decode() if isinstance(value, bytes) else str(value)
|
| 131 |
+
for value in fields.attrs["variables"]
|
| 132 |
+
]
|
| 133 |
+
time_step = int(fields.attrs["time_step"])
|
| 134 |
+
shape = tuple(fields.shape)
|
| 135 |
+
if time_step not in (6, 12):
|
| 136 |
+
raise ValueError(f"{path}: GenCast requires 6h or 12h ERA5, got {time_step}h")
|
| 137 |
+
if self.precipitation_interval_hours != time_step:
|
| 138 |
+
raise ValueError(
|
| 139 |
+
f"{path}: total_precipitation must be an accumulation over each "
|
| 140 |
+
f"{time_step}h source interval; configured "
|
| 141 |
+
f"{self.precipitation_interval_hours}h"
|
| 142 |
+
)
|
| 143 |
+
missing = sorted(set(ERA5_VARIABLES) - set(variables))
|
| 144 |
+
if missing:
|
| 145 |
+
raise ValueError(f"{path}: missing GenCast ERA5 variables: {missing}")
|
| 146 |
+
frame_stride = 12 // time_step
|
| 147 |
+
# The -12h input also needs a complete 12h precipitation window.
|
| 148 |
+
first_reference = 2 * frame_stride - 1
|
| 149 |
+
last_reference = (
|
| 150 |
+
shape[0] - frame_stride * self.prediction_steps - 1
|
| 151 |
+
if self.load_future_targets else shape[0] - 1
|
| 152 |
+
)
|
| 153 |
+
references = list(range(first_reference, last_reference + 1, self.stride))
|
| 154 |
+
meta = {
|
| 155 |
+
"year": year,
|
| 156 |
+
"path": path,
|
| 157 |
+
"shape": shape,
|
| 158 |
+
"time_step": time_step,
|
| 159 |
+
"frame_stride": frame_stride,
|
| 160 |
+
"variables": variables,
|
| 161 |
+
"references": references,
|
| 162 |
+
}
|
| 163 |
+
self._year_meta.append(meta)
|
| 164 |
+
total += len(references)
|
| 165 |
+
self._cumulative.append(total)
|
| 166 |
+
self.total_samples = total
|
| 167 |
+
if not total:
|
| 168 |
+
raise ValueError("No complete GenCast samples are available")
|
| 169 |
+
|
| 170 |
+
def __len__(self) -> int:
|
| 171 |
+
return self.total_samples
|
| 172 |
+
|
| 173 |
+
def __getitem__(self, index: int):
|
| 174 |
+
if index < 0:
|
| 175 |
+
index += len(self)
|
| 176 |
+
if index < 0 or index >= len(self):
|
| 177 |
+
raise IndexError(index)
|
| 178 |
+
year_index = bisect.bisect_right(self._cumulative, index)
|
| 179 |
+
start = 0 if year_index == 0 else self._cumulative[year_index - 1]
|
| 180 |
+
meta = self._year_meta[year_index]
|
| 181 |
+
reference_index = meta["references"][index - start]
|
| 182 |
+
dataset = self._read_dataset(meta, reference_index)
|
| 183 |
+
return data_utils.extract_inputs_targets_forcings(
|
| 184 |
+
dataset,
|
| 185 |
+
target_lead_times=slice("12h", f"{12 * self.prediction_steps}h"),
|
| 186 |
+
input_variables=self.task_config.input_variables,
|
| 187 |
+
target_variables=self.task_config.target_variables,
|
| 188 |
+
forcing_variables=self.task_config.forcing_variables,
|
| 189 |
+
pressure_levels=self.task_config.pressure_levels,
|
| 190 |
+
input_duration=self.task_config.input_duration,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
def _read_dataset(self, meta: dict[str, Any], reference_index: int) -> xarray.Dataset:
|
| 194 |
+
frame_stride = meta["frame_stride"]
|
| 195 |
+
frame_indices = [
|
| 196 |
+
reference_index - frame_stride,
|
| 197 |
+
reference_index,
|
| 198 |
+
*(reference_index + frame_stride * step for step in range(1, self.prediction_steps + 1)),
|
| 199 |
+
]
|
| 200 |
+
variable_index = {name: i for i, name in enumerate(meta["variables"])}
|
| 201 |
+
selected_names = list(SURFACE_VARIABLES) + [
|
| 202 |
+
f"{name}_{level}"
|
| 203 |
+
for name in ATMOSPHERIC_VARIABLES
|
| 204 |
+
for level in PRESSURE_LEVELS
|
| 205 |
+
]
|
| 206 |
+
selected_indices = [variable_index[name] for name in selected_names]
|
| 207 |
+
order = np.argsort(selected_indices)
|
| 208 |
+
inverse = np.empty(len(order), dtype=np.int64)
|
| 209 |
+
inverse[order] = np.arange(len(order))
|
| 210 |
+
read_count = len(frame_indices) if self.load_future_targets else 2
|
| 211 |
+
with h5py.File(meta["path"], "r") as source:
|
| 212 |
+
fields = source["fields"]
|
| 213 |
+
loaded = np.stack([
|
| 214 |
+
fields[t, np.asarray(selected_indices)[order], :, :][inverse]
|
| 215 |
+
for t in frame_indices[:read_count]
|
| 216 |
+
]).astype(np.float32)
|
| 217 |
+
values = np.full(
|
| 218 |
+
(len(frame_indices), *loaded.shape[1:]), np.nan, dtype=np.float32
|
| 219 |
+
)
|
| 220 |
+
values[:read_count] = loaded
|
| 221 |
+
precipitation = np.full(
|
| 222 |
+
(len(frame_indices), *loaded.shape[-2:]), np.nan, dtype=np.float32
|
| 223 |
+
)
|
| 224 |
+
if self.load_future_targets:
|
| 225 |
+
precipitation[:] = np.stack([
|
| 226 |
+
self._precipitation_12h(
|
| 227 |
+
fields, variable_index[RAW_PRECIPITATION], t, frame_stride
|
| 228 |
+
)
|
| 229 |
+
for t in frame_indices
|
| 230 |
+
]).astype(np.float32)
|
| 231 |
+
|
| 232 |
+
# OneScience ERA5 uses north-to-south storage; GenCast spherical noise requires ascending lat.
|
| 233 |
+
values = values[..., ::-1, :]
|
| 234 |
+
precipitation = precipitation[..., ::-1, :]
|
| 235 |
+
height, width = values.shape[-2:]
|
| 236 |
+
lat = np.linspace(-90.0, 90.0, height, dtype=np.float32)
|
| 237 |
+
lon = np.linspace(0.0, 360.0, width, endpoint=False, dtype=np.float32)
|
| 238 |
+
reference_time = dt.datetime(meta["year"], 1, 1) + dt.timedelta(
|
| 239 |
+
hours=reference_index * meta["time_step"]
|
| 240 |
+
)
|
| 241 |
+
datetimes = np.asarray([
|
| 242 |
+
np.datetime64(reference_time + dt.timedelta(hours=(t - reference_index) * meta["time_step"]))
|
| 243 |
+
for t in frame_indices
|
| 244 |
+
], dtype="datetime64[ns]")
|
| 245 |
+
times = np.asarray([
|
| 246 |
+
np.timedelta64((t - reference_index) * meta["time_step"], "h")
|
| 247 |
+
for t in frame_indices
|
| 248 |
+
], dtype="timedelta64[ns]")
|
| 249 |
+
|
| 250 |
+
data_vars: dict[str, Any] = {}
|
| 251 |
+
cursor = 0
|
| 252 |
+
for name in SURFACE_VARIABLES:
|
| 253 |
+
data_vars[name] = (("batch", "time", "lat", "lon"), values[:, cursor][None])
|
| 254 |
+
cursor += 1
|
| 255 |
+
for name in ATMOSPHERIC_VARIABLES:
|
| 256 |
+
data_vars[name] = (
|
| 257 |
+
("batch", "time", "level", "lat", "lon"),
|
| 258 |
+
values[:, cursor:cursor + len(PRESSURE_LEVELS)][None],
|
| 259 |
+
)
|
| 260 |
+
cursor += len(PRESSURE_LEVELS)
|
| 261 |
+
data_vars[TARGET_PRECIPITATION] = (
|
| 262 |
+
("batch", "time", "lat", "lon"), precipitation[None]
|
| 263 |
+
)
|
| 264 |
+
data_vars.update(self._load_static(height, width))
|
| 265 |
+
dataset = xarray.Dataset(
|
| 266 |
+
data_vars=data_vars,
|
| 267 |
+
coords={
|
| 268 |
+
"batch": np.arange(1),
|
| 269 |
+
"time": times,
|
| 270 |
+
"datetime": (("batch", "time"), datetimes[None]),
|
| 271 |
+
"level": np.asarray(PRESSURE_LEVELS, dtype=np.int32),
|
| 272 |
+
"lat": lat,
|
| 273 |
+
"lon": lon,
|
| 274 |
+
},
|
| 275 |
+
)
|
| 276 |
+
dataset.attrs["forecast_reference_time"] = np.datetime_as_string(
|
| 277 |
+
np.datetime64(reference_time), unit="h"
|
| 278 |
+
)
|
| 279 |
+
self.validate_dataset(dataset)
|
| 280 |
+
return dataset
|
| 281 |
+
|
| 282 |
+
@staticmethod
|
| 283 |
+
def _precipitation_12h(fields, channel: int, end: int, frame_stride: int):
|
| 284 |
+
start = end - frame_stride + 1
|
| 285 |
+
if start < 0:
|
| 286 |
+
raise IndexError("Insufficient precipitation history for 12h accumulation")
|
| 287 |
+
return np.sum(fields[start:end + 1, channel], axis=0)
|
| 288 |
+
|
| 289 |
+
def _load_static(self, height: int, width: int) -> dict[str, Any]:
|
| 290 |
+
paths = {
|
| 291 |
+
"geopotential_at_surface": self.static_dir / "geopotential_at_surface.npy",
|
| 292 |
+
"land_sea_mask": self.static_dir / "land_mask.npy",
|
| 293 |
+
}
|
| 294 |
+
result = {}
|
| 295 |
+
for name, path in paths.items():
|
| 296 |
+
if not path.exists():
|
| 297 |
+
raise FileNotFoundError(f"Missing GenCast static field: {path}")
|
| 298 |
+
values = np.load(path).astype(np.float32)
|
| 299 |
+
if values.shape != (height, width):
|
| 300 |
+
raise ValueError(f"{path}: expected {(height, width)}, got {values.shape}")
|
| 301 |
+
result[name] = (("lat", "lon"), values[::-1])
|
| 302 |
+
return result
|
| 303 |
+
|
| 304 |
+
def _validate_task_config(self) -> None:
|
| 305 |
+
expected = gencast.TASK
|
| 306 |
+
for field in (
|
| 307 |
+
"input_variables", "target_variables", "forcing_variables",
|
| 308 |
+
"pressure_levels", "input_duration",
|
| 309 |
+
):
|
| 310 |
+
if getattr(self.task_config, field) != getattr(expected, field):
|
| 311 |
+
raise ValueError(
|
| 312 |
+
"This ERA5 adapter supports the official WB13 GenCast task "
|
| 313 |
+
f"only; checkpoint field {field} differs"
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
@staticmethod
|
| 317 |
+
def validate_dataset(dataset: xarray.Dataset) -> None:
|
| 318 |
+
missing = sorted(
|
| 319 |
+
set(gencast.TASK.input_variables + gencast.TASK.target_variables)
|
| 320 |
+
- set(dataset.data_vars)
|
| 321 |
+
- set(graphcast.GENERATED_FORCING_VARS)
|
| 322 |
+
)
|
| 323 |
+
if missing:
|
| 324 |
+
raise ValueError(f"Missing GenCast variables: {missing}")
|
| 325 |
+
if tuple(int(level) for level in dataset.level.values) != PRESSURE_LEVELS:
|
| 326 |
+
raise ValueError("GenCast WB13 pressure-level order changed")
|
| 327 |
+
if not np.all(np.diff(dataset.lat.values) > 0):
|
| 328 |
+
raise ValueError("GenCast latitude must be strictly ascending")
|
| 329 |
+
height, width = dataset.sizes["lat"], dataset.sizes["lon"]
|
| 330 |
+
if width != 2 * (height - 1):
|
| 331 |
+
raise ValueError(
|
| 332 |
+
"GenCast equiangular grids with poles require lon=2*(lat-1), "
|
| 333 |
+
f"got lat={height}, lon={width}"
|
| 334 |
+
)
|
| 335 |
+
if len(expected_target_channel_names()) != MODEL_TARGET_CHANNELS:
|
| 336 |
+
raise AssertionError("The official GenCast target contract must contain 84 channels")
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def batch_iterator(
|
| 340 |
+
dataset: GenCastERA5Dataset,
|
| 341 |
+
*,
|
| 342 |
+
shuffle: bool,
|
| 343 |
+
seed: int,
|
| 344 |
+
batch_size: int = 1,
|
| 345 |
+
) -> Iterator:
|
| 346 |
+
if batch_size < 1:
|
| 347 |
+
raise ValueError("batch_size must be positive")
|
| 348 |
+
indices = np.arange(len(dataset))
|
| 349 |
+
if shuffle:
|
| 350 |
+
np.random.default_rng(seed).shuffle(indices)
|
| 351 |
+
for start in range(0, len(indices) - batch_size + 1, batch_size):
|
| 352 |
+
samples = [dataset[int(index)] for index in indices[start:start + batch_size]]
|
| 353 |
+
if batch_size == 1:
|
| 354 |
+
yield samples[0]
|
| 355 |
+
continue
|
| 356 |
+
yield tuple(
|
| 357 |
+
xarray.concat(values, dim="batch", data_vars="minimal", coords="minimal")
|
| 358 |
+
for values in zip(*samples)
|
| 359 |
+
)
|
model/gencast.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""项目内官方等价 GenCast JAX/Haiku 实现封装。"""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import dataclasses
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import haiku as hk
|
| 10 |
+
import jax
|
| 11 |
+
import xarray
|
| 12 |
+
|
| 13 |
+
from model.graphcast import checkpoint
|
| 14 |
+
from model.graphcast import denoiser
|
| 15 |
+
from model.graphcast import gencast
|
| 16 |
+
from model.graphcast import nan_cleaning
|
| 17 |
+
from model.graphcast import normalization
|
| 18 |
+
from model.graphcast import xarray_jax
|
| 19 |
+
from model.graphcast import xarray_tree
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def build_model_config(config: dict[str, Any]) -> tuple[
|
| 23 |
+
Any, denoiser.DenoiserArchitectureConfig, gencast.SamplerConfig,
|
| 24 |
+
gencast.NoiseConfig, denoiser.NoiseEncoderConfig
|
| 25 |
+
]:
|
| 26 |
+
"""Build a random-weight configuration without changing GenCast semantics."""
|
| 27 |
+
model_cfg = config["model"]
|
| 28 |
+
sampler_cfg = config["sampler"]
|
| 29 |
+
transformer = denoiser.SparseTransformerConfig(
|
| 30 |
+
attention_k_hop=int(model_cfg["attention_k_hop"]),
|
| 31 |
+
d_model=int(model_cfg["latent_size"]),
|
| 32 |
+
num_layers=int(model_cfg["num_layers"]),
|
| 33 |
+
num_heads=int(model_cfg["num_heads"]),
|
| 34 |
+
attention_type=str(model_cfg["attention_type"]),
|
| 35 |
+
mask_type=str(model_cfg.get("mask_type", "full")),
|
| 36 |
+
ffw_hidden=int(model_cfg["ffw_hidden"]),
|
| 37 |
+
)
|
| 38 |
+
architecture = denoiser.DenoiserArchitectureConfig(
|
| 39 |
+
sparse_transformer_config=transformer,
|
| 40 |
+
mesh_size=int(model_cfg["mesh_size"]),
|
| 41 |
+
latent_size=int(model_cfg["latent_size"]),
|
| 42 |
+
hidden_layers=int(model_cfg.get("hidden_layers", 1)),
|
| 43 |
+
radius_query_fraction_edge_length=float(
|
| 44 |
+
model_cfg.get("radius_query_fraction_edge_length", 0.6)
|
| 45 |
+
),
|
| 46 |
+
)
|
| 47 |
+
sampler = gencast.SamplerConfig(**sampler_cfg)
|
| 48 |
+
return (
|
| 49 |
+
gencast.TASK,
|
| 50 |
+
architecture,
|
| 51 |
+
sampler,
|
| 52 |
+
gencast.NoiseConfig(),
|
| 53 |
+
denoiser.NoiseEncoderConfig(),
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_model_checkpoint(path: str | Path) -> gencast.CheckPoint:
|
| 58 |
+
"""Load the typed official GenCast NPZ checkpoint."""
|
| 59 |
+
with Path(path).open("rb") as source:
|
| 60 |
+
return checkpoint.load(source, gencast.CheckPoint)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class GenCastModel:
|
| 64 |
+
"""Owns official-equivalent GenCast loss and sampling Haiku transforms."""
|
| 65 |
+
|
| 66 |
+
def __init__(
|
| 67 |
+
self,
|
| 68 |
+
*,
|
| 69 |
+
task_config: Any,
|
| 70 |
+
architecture_config: denoiser.DenoiserArchitectureConfig,
|
| 71 |
+
sampler_config: gencast.SamplerConfig,
|
| 72 |
+
noise_config: gencast.NoiseConfig,
|
| 73 |
+
noise_encoder_config: denoiser.NoiseEncoderConfig,
|
| 74 |
+
diffs_stddev_by_level: xarray.Dataset,
|
| 75 |
+
mean_by_level: xarray.Dataset,
|
| 76 |
+
stddev_by_level: xarray.Dataset,
|
| 77 |
+
min_by_level: xarray.Dataset,
|
| 78 |
+
reintroduce_nans: bool = True,
|
| 79 |
+
) -> None:
|
| 80 |
+
self.task_config = task_config
|
| 81 |
+
self.architecture_config = architecture_config
|
| 82 |
+
self.sampler_config = sampler_config
|
| 83 |
+
self.noise_config = noise_config
|
| 84 |
+
self.noise_encoder_config = noise_encoder_config
|
| 85 |
+
self.diffs_stddev_by_level = diffs_stddev_by_level
|
| 86 |
+
self.mean_by_level = mean_by_level
|
| 87 |
+
self.stddev_by_level = stddev_by_level
|
| 88 |
+
self.min_by_level = min_by_level
|
| 89 |
+
self.reintroduce_nans = reintroduce_nans
|
| 90 |
+
|
| 91 |
+
def construct() -> Any:
|
| 92 |
+
predictor = gencast.GenCast(
|
| 93 |
+
task_config=self.task_config,
|
| 94 |
+
denoiser_architecture_config=self.architecture_config,
|
| 95 |
+
sampler_config=self.sampler_config,
|
| 96 |
+
noise_config=self.noise_config,
|
| 97 |
+
noise_encoder_config=self.noise_encoder_config,
|
| 98 |
+
)
|
| 99 |
+
predictor = normalization.InputsAndResiduals(
|
| 100 |
+
predictor,
|
| 101 |
+
diffs_stddev_by_level=self.diffs_stddev_by_level,
|
| 102 |
+
mean_by_level=self.mean_by_level,
|
| 103 |
+
stddev_by_level=self.stddev_by_level,
|
| 104 |
+
)
|
| 105 |
+
return nan_cleaning.NaNCleaner(
|
| 106 |
+
predictor,
|
| 107 |
+
var_to_clean="sea_surface_temperature",
|
| 108 |
+
fill_value=self.min_by_level,
|
| 109 |
+
reintroduce_nans=self.reintroduce_nans,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
@hk.transform_with_state
|
| 113 |
+
def loss_fn(inputs, targets, forcings):
|
| 114 |
+
loss, diagnostics = construct().loss(inputs, targets, forcings)
|
| 115 |
+
return xarray_tree.map_structure(
|
| 116 |
+
lambda value: xarray_jax.unwrap_data(
|
| 117 |
+
value.mean(), require_jax=True
|
| 118 |
+
),
|
| 119 |
+
(loss, diagnostics),
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
@hk.transform_with_state
|
| 123 |
+
def forward_fn(inputs, targets_template, forcings):
|
| 124 |
+
return construct()(
|
| 125 |
+
inputs,
|
| 126 |
+
targets_template=targets_template,
|
| 127 |
+
forcings=forcings,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
self.loss_fn = loss_fn
|
| 131 |
+
self.forward_fn = forward_fn
|
| 132 |
+
|
| 133 |
+
@classmethod
|
| 134 |
+
def from_config_and_stats(
|
| 135 |
+
cls, config: dict[str, Any], stats: dict[str, xarray.Dataset]
|
| 136 |
+
) -> "GenCastModel":
|
| 137 |
+
configs = build_model_config(config)
|
| 138 |
+
return cls(
|
| 139 |
+
task_config=configs[0],
|
| 140 |
+
architecture_config=configs[1],
|
| 141 |
+
sampler_config=configs[2],
|
| 142 |
+
noise_config=configs[3],
|
| 143 |
+
noise_encoder_config=configs[4],
|
| 144 |
+
diffs_stddev_by_level=stats["diffs_stddev_by_level"],
|
| 145 |
+
mean_by_level=stats["mean_by_level"],
|
| 146 |
+
stddev_by_level=stats["stddev_by_level"],
|
| 147 |
+
min_by_level=stats["min_by_level"],
|
| 148 |
+
reintroduce_nans=bool(config.get("data", {}).get("reintroduce_sst_nans", True)),
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
@classmethod
|
| 152 |
+
def from_checkpoint_and_stats(
|
| 153 |
+
cls,
|
| 154 |
+
model_checkpoint: gencast.CheckPoint,
|
| 155 |
+
stats: dict[str, xarray.Dataset],
|
| 156 |
+
*,
|
| 157 |
+
attention_type: str | None = None,
|
| 158 |
+
) -> "GenCastModel":
|
| 159 |
+
architecture = model_checkpoint.denoiser_architecture_config
|
| 160 |
+
if attention_type is not None:
|
| 161 |
+
architecture = dataclasses.replace(
|
| 162 |
+
architecture,
|
| 163 |
+
sparse_transformer_config=dataclasses.replace(
|
| 164 |
+
architecture.sparse_transformer_config,
|
| 165 |
+
attention_type=attention_type,
|
| 166 |
+
mask_type="full",
|
| 167 |
+
),
|
| 168 |
+
)
|
| 169 |
+
return cls(
|
| 170 |
+
task_config=model_checkpoint.task_config,
|
| 171 |
+
architecture_config=architecture,
|
| 172 |
+
sampler_config=model_checkpoint.sampler_config,
|
| 173 |
+
noise_config=model_checkpoint.noise_config,
|
| 174 |
+
noise_encoder_config=model_checkpoint.noise_encoder_config,
|
| 175 |
+
diffs_stddev_by_level=stats["diffs_stddev_by_level"],
|
| 176 |
+
mean_by_level=stats["mean_by_level"],
|
| 177 |
+
stddev_by_level=stats["stddev_by_level"],
|
| 178 |
+
min_by_level=stats["min_by_level"],
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
def init(self, rng, inputs, targets, forcings):
|
| 182 |
+
return self.loss_fn.init(rng, inputs, targets, forcings)
|
| 183 |
+
|
| 184 |
+
def loss(self, params, state, rng, inputs, targets, forcings):
|
| 185 |
+
return self.loss_fn.apply(params, state, rng, inputs, targets, forcings)
|
| 186 |
+
|
| 187 |
+
def predict(self, params, state, rng, inputs, targets_template, forcings):
|
| 188 |
+
return self.forward_fn.apply(
|
| 189 |
+
params, state, rng, inputs, targets_template, forcings
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def parameter_count(params: Any) -> int:
|
| 194 |
+
return sum(int(value.size) for value in jax.tree_util.tree_leaves(params))
|
model/graphcast/autoregressive.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""A Predictor wrapping a one-step Predictor to make autoregressive predictions.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from typing import Optional, cast
|
| 18 |
+
|
| 19 |
+
from absl import logging
|
| 20 |
+
from . import predictor_base
|
| 21 |
+
from . import xarray_jax
|
| 22 |
+
from . import xarray_tree
|
| 23 |
+
import haiku as hk
|
| 24 |
+
import jax
|
| 25 |
+
import xarray
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _unflatten_and_expand_time(flat_variables, tree_def, time_coords):
|
| 29 |
+
variables = jax.tree_util.tree_unflatten(tree_def, flat_variables)
|
| 30 |
+
return variables.expand_dims(time=time_coords, axis=0)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _get_flat_arrays_and_single_timestep_treedef(variables):
|
| 34 |
+
flat_arrays = jax.tree_util.tree_leaves(variables.transpose('time', ...))
|
| 35 |
+
_, treedef = jax.tree_util.tree_flatten(variables.isel(time=0, drop=True))
|
| 36 |
+
return flat_arrays, treedef
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Predictor(predictor_base.Predictor):
|
| 40 |
+
"""Wraps a one-step Predictor to make multi-step predictions autoregressively.
|
| 41 |
+
|
| 42 |
+
The wrapped Predictor will be used to predict a single timestep conditional
|
| 43 |
+
on the inputs passed to the outer Predictor. Its predictions are then
|
| 44 |
+
passed back in as inputs at the next timestep, for as many timesteps as are
|
| 45 |
+
requested in the targets_template. (When multiple timesteps of input are
|
| 46 |
+
used, a rolling window of inputs is maintained with new predictions
|
| 47 |
+
concatenated onto the end).
|
| 48 |
+
|
| 49 |
+
You may ask for additional variables to be predicted as targets which aren't
|
| 50 |
+
used as inputs. These will be predicted as output variables only and not fed
|
| 51 |
+
back in autoregressively. All target variables must be time-dependent however.
|
| 52 |
+
|
| 53 |
+
You may also specify static (non-time-dependent) inputs which will be passed
|
| 54 |
+
in at each timestep but are not predicted.
|
| 55 |
+
|
| 56 |
+
At present, any time-dependent inputs must also be present as targets so they
|
| 57 |
+
can be passed in autoregressively.
|
| 58 |
+
|
| 59 |
+
The loss of the wrapped one-step Predictor is averaged over all timesteps to
|
| 60 |
+
give a loss for the autoregressive Predictor.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(
|
| 64 |
+
self,
|
| 65 |
+
predictor: predictor_base.Predictor,
|
| 66 |
+
noise_level: Optional[float] = None,
|
| 67 |
+
gradient_checkpointing: bool = False,
|
| 68 |
+
):
|
| 69 |
+
"""Initializes an autoregressive predictor wrapper.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
predictor: A predictor to wrap in an auto-regressive way.
|
| 73 |
+
noise_level: Optional value that multiplies the standard normal noise
|
| 74 |
+
added to the time-dependent variables of the predictor inputs. In
|
| 75 |
+
particular, no noise is added to the predictions that are fed back
|
| 76 |
+
auto-regressively. Defaults to not adding noise.
|
| 77 |
+
gradient_checkpointing: If True, gradient checkpointing will be
|
| 78 |
+
used at each step of the computation to save on memory. Roughtly this
|
| 79 |
+
should make the backwards pass two times more expensive, and the time
|
| 80 |
+
per step counting the forward pass, should only increase by about 50%.
|
| 81 |
+
Note this parameter will be ignored with a warning if the scan sequence
|
| 82 |
+
length is 1.
|
| 83 |
+
"""
|
| 84 |
+
self._predictor = predictor
|
| 85 |
+
self._noise_level = noise_level
|
| 86 |
+
self._gradient_checkpointing = gradient_checkpointing
|
| 87 |
+
|
| 88 |
+
def _get_and_validate_constant_inputs(self, inputs, targets, forcings):
|
| 89 |
+
constant_inputs = inputs.drop_vars(targets.keys(), errors='ignore')
|
| 90 |
+
constant_inputs = constant_inputs.drop_vars(
|
| 91 |
+
forcings.keys(), errors='ignore')
|
| 92 |
+
for name, var in constant_inputs.items():
|
| 93 |
+
if 'time' in var.dims:
|
| 94 |
+
raise ValueError(
|
| 95 |
+
f'Time-dependent input variable {name} must either be a forcing '
|
| 96 |
+
'variable, or a target variable to allow for auto-regressive '
|
| 97 |
+
'feedback.')
|
| 98 |
+
return constant_inputs
|
| 99 |
+
|
| 100 |
+
def _validate_targets_and_forcings(self, targets, forcings):
|
| 101 |
+
for name, var in targets.items():
|
| 102 |
+
if 'time' not in var.dims:
|
| 103 |
+
raise ValueError(f'Target variable {name} must be time-dependent.')
|
| 104 |
+
|
| 105 |
+
for name, var in forcings.items():
|
| 106 |
+
if 'time' not in var.dims:
|
| 107 |
+
raise ValueError(f'Forcing variable {name} must be time-dependent.')
|
| 108 |
+
|
| 109 |
+
overlap = forcings.keys() & targets.keys()
|
| 110 |
+
if overlap:
|
| 111 |
+
raise ValueError('The following were specified as both targets and '
|
| 112 |
+
f'forcings, which isn\'t allowed: {overlap}')
|
| 113 |
+
|
| 114 |
+
def _update_inputs(self, inputs, next_frame):
|
| 115 |
+
num_inputs = inputs.dims['time']
|
| 116 |
+
|
| 117 |
+
predicted_or_forced_inputs = next_frame[list(inputs.keys())]
|
| 118 |
+
|
| 119 |
+
# Combining datasets with inputs and target time stamps aligns them.
|
| 120 |
+
# Only keep the num_inputs trailing frames for use as next inputs.
|
| 121 |
+
return (xarray.concat([inputs, predicted_or_forced_inputs], dim='time')
|
| 122 |
+
.tail(time=num_inputs)
|
| 123 |
+
# Update the time coordinate to reset the lead times for
|
| 124 |
+
# next AR iteration.
|
| 125 |
+
.assign_coords(time=inputs.coords['time']))
|
| 126 |
+
|
| 127 |
+
def __call__(self,
|
| 128 |
+
inputs: xarray.Dataset,
|
| 129 |
+
targets_template: xarray.Dataset,
|
| 130 |
+
forcings: xarray.Dataset,
|
| 131 |
+
**kwargs) -> xarray.Dataset:
|
| 132 |
+
"""Calls the Predictor.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
inputs: input variable used to make predictions. Inputs can include both
|
| 136 |
+
time-dependent and time independent variables. Any time-dependent
|
| 137 |
+
input variables must also be present in the targets_template or the
|
| 138 |
+
forcings.
|
| 139 |
+
targets_template: A target template containing informations about which
|
| 140 |
+
variables should be predicted and the time alignment of the predictions.
|
| 141 |
+
All target variables must be time-dependent.
|
| 142 |
+
The number of time frames is used to set the number of unroll of the AR
|
| 143 |
+
predictor (e.g. multiple unroll of the inner predictor for one time step
|
| 144 |
+
in the targets is not supported yet).
|
| 145 |
+
forcings: Variables that will be fed to the model. The variables
|
| 146 |
+
should not overlap with the target ones. The time coordinates of the
|
| 147 |
+
forcing variables should match the target ones.
|
| 148 |
+
Forcing variables which are also present in the inputs, will be used to
|
| 149 |
+
supply ground-truth values for those inputs when they are passed to the
|
| 150 |
+
underlying predictor at timesteps beyond the first timestep.
|
| 151 |
+
**kwargs: Additional arguments passed along to the inner Predictor.
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
predictions: the model predictions matching the target template.
|
| 155 |
+
|
| 156 |
+
Raise:
|
| 157 |
+
ValueError: if the time coordinates of the inputs and targets are not
|
| 158 |
+
different by a constant time step.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
constant_inputs = self._get_and_validate_constant_inputs(
|
| 162 |
+
inputs, targets_template, forcings)
|
| 163 |
+
self._validate_targets_and_forcings(targets_template, forcings)
|
| 164 |
+
|
| 165 |
+
# After the above checks, the remaining inputs must be time-dependent:
|
| 166 |
+
inputs = inputs.drop_vars(constant_inputs.keys())
|
| 167 |
+
|
| 168 |
+
# A predictions template only including the next time to predict.
|
| 169 |
+
target_template = targets_template.isel(time=[0])
|
| 170 |
+
|
| 171 |
+
flat_forcings, forcings_treedef = (
|
| 172 |
+
_get_flat_arrays_and_single_timestep_treedef(forcings))
|
| 173 |
+
scan_variables = flat_forcings
|
| 174 |
+
|
| 175 |
+
def one_step_prediction(inputs, scan_variables):
|
| 176 |
+
|
| 177 |
+
flat_forcings = scan_variables
|
| 178 |
+
forcings = _unflatten_and_expand_time(flat_forcings, forcings_treedef,
|
| 179 |
+
target_template.coords['time'])
|
| 180 |
+
|
| 181 |
+
# Add constant inputs:
|
| 182 |
+
all_inputs = xarray.merge([constant_inputs, inputs])
|
| 183 |
+
predictions: xarray.Dataset = self._predictor(
|
| 184 |
+
all_inputs, target_template,
|
| 185 |
+
forcings=forcings,
|
| 186 |
+
**kwargs)
|
| 187 |
+
|
| 188 |
+
next_frame = xarray.merge([predictions, forcings])
|
| 189 |
+
next_inputs = self._update_inputs(inputs, next_frame)
|
| 190 |
+
|
| 191 |
+
# Drop the length-1 time dimension, since scan will concat all the outputs
|
| 192 |
+
# for different times along a new leading time dimension:
|
| 193 |
+
predictions = predictions.squeeze('time', drop=True)
|
| 194 |
+
# We return the prediction flattened into plain jax arrays, because the
|
| 195 |
+
# extra leading dimension added by scan prevents the tree_util
|
| 196 |
+
# registrations in xarray_jax from unflattening them back into an
|
| 197 |
+
# xarray.Dataset automatically:
|
| 198 |
+
flat_pred = jax.tree_util.tree_leaves(predictions)
|
| 199 |
+
return next_inputs, flat_pred
|
| 200 |
+
|
| 201 |
+
if self._gradient_checkpointing:
|
| 202 |
+
scan_length = targets_template.dims['time']
|
| 203 |
+
if scan_length <= 1:
|
| 204 |
+
logging.warning(
|
| 205 |
+
'Skipping gradient checkpointing for sequence length of 1')
|
| 206 |
+
else:
|
| 207 |
+
# Just in case we take gradients (e.g. for control), although
|
| 208 |
+
# in most cases this will just be for a forward pass.
|
| 209 |
+
one_step_prediction = hk.remat(one_step_prediction)
|
| 210 |
+
|
| 211 |
+
# Loop (without unroll) with hk states in cell (jax.lax.scan won't do).
|
| 212 |
+
_, flat_preds = hk.scan(one_step_prediction, inputs, scan_variables)
|
| 213 |
+
|
| 214 |
+
# The result of scan will have an extra leading axis on all arrays,
|
| 215 |
+
# corresponding to the target times in this case. We need to be prepared for
|
| 216 |
+
# it when unflattening the arrays back into a Dataset:
|
| 217 |
+
scan_result_template = (
|
| 218 |
+
target_template.squeeze('time', drop=True)
|
| 219 |
+
.expand_dims(time=targets_template.coords['time'], axis=0))
|
| 220 |
+
_, scan_result_treedef = jax.tree_util.tree_flatten(scan_result_template)
|
| 221 |
+
predictions = jax.tree_util.tree_unflatten(scan_result_treedef, flat_preds)
|
| 222 |
+
return predictions
|
| 223 |
+
|
| 224 |
+
def loss(self,
|
| 225 |
+
inputs: xarray.Dataset,
|
| 226 |
+
targets: xarray.Dataset,
|
| 227 |
+
forcings: xarray.Dataset,
|
| 228 |
+
**kwargs
|
| 229 |
+
) -> predictor_base.LossAndDiagnostics:
|
| 230 |
+
"""The mean of the per-timestep losses of the underlying predictor."""
|
| 231 |
+
if targets.sizes['time'] == 1:
|
| 232 |
+
# If there is only a single target timestep then we don't need any
|
| 233 |
+
# autoregressive feedback and can delegate the loss directly to the
|
| 234 |
+
# underlying single-step predictor. This means the underlying predictor
|
| 235 |
+
# doesn't need to implement .loss_and_predictions.
|
| 236 |
+
return self._predictor.loss(inputs, targets, forcings, **kwargs)
|
| 237 |
+
|
| 238 |
+
constant_inputs = self._get_and_validate_constant_inputs(
|
| 239 |
+
inputs, targets, forcings)
|
| 240 |
+
self._validate_targets_and_forcings(targets, forcings)
|
| 241 |
+
# After the above checks, the remaining inputs must be time-dependent:
|
| 242 |
+
inputs = inputs.drop_vars(constant_inputs.keys())
|
| 243 |
+
|
| 244 |
+
if self._noise_level:
|
| 245 |
+
def add_noise(x):
|
| 246 |
+
return x + self._noise_level * jax.random.normal(
|
| 247 |
+
hk.next_rng_key(), shape=x.shape)
|
| 248 |
+
# Add noise to time-dependent variables of the inputs.
|
| 249 |
+
inputs = jax.tree.map(add_noise, inputs)
|
| 250 |
+
|
| 251 |
+
# The per-timestep targets passed by scan to one_step_loss below will have
|
| 252 |
+
# no leading time axis. We need a treedef without the time axis to use
|
| 253 |
+
# inside one_step_loss to unflatten it back into a dataset:
|
| 254 |
+
flat_targets, target_treedef = _get_flat_arrays_and_single_timestep_treedef(
|
| 255 |
+
targets)
|
| 256 |
+
scan_variables = flat_targets
|
| 257 |
+
|
| 258 |
+
flat_forcings, forcings_treedef = (
|
| 259 |
+
_get_flat_arrays_and_single_timestep_treedef(forcings))
|
| 260 |
+
scan_variables = (flat_targets, flat_forcings)
|
| 261 |
+
|
| 262 |
+
def one_step_loss(inputs, scan_variables):
|
| 263 |
+
flat_target, flat_forcings = scan_variables
|
| 264 |
+
forcings = _unflatten_and_expand_time(flat_forcings, forcings_treedef,
|
| 265 |
+
targets.coords['time'][:1])
|
| 266 |
+
|
| 267 |
+
target = _unflatten_and_expand_time(flat_target, target_treedef,
|
| 268 |
+
targets.coords['time'][:1])
|
| 269 |
+
|
| 270 |
+
# Add constant inputs:
|
| 271 |
+
all_inputs = xarray.merge([constant_inputs, inputs])
|
| 272 |
+
|
| 273 |
+
(loss, diagnostics), predictions = self._predictor.loss_and_predictions(
|
| 274 |
+
all_inputs,
|
| 275 |
+
target,
|
| 276 |
+
forcings=forcings,
|
| 277 |
+
**kwargs)
|
| 278 |
+
|
| 279 |
+
# Unwrap to jax arrays shape (batch,):
|
| 280 |
+
loss, diagnostics = xarray_tree.map_structure(
|
| 281 |
+
xarray_jax.unwrap_data, (loss, diagnostics))
|
| 282 |
+
|
| 283 |
+
predictions = cast(xarray.Dataset, predictions) # Keeps pytype happy.
|
| 284 |
+
next_frame = xarray.merge([predictions, forcings])
|
| 285 |
+
next_inputs = self._update_inputs(inputs, next_frame)
|
| 286 |
+
|
| 287 |
+
return next_inputs, (loss, diagnostics)
|
| 288 |
+
|
| 289 |
+
if self._gradient_checkpointing:
|
| 290 |
+
scan_length = targets.dims['time']
|
| 291 |
+
if scan_length <= 1:
|
| 292 |
+
logging.warning(
|
| 293 |
+
'Skipping gradient checkpointing for sequence length of 1')
|
| 294 |
+
else:
|
| 295 |
+
one_step_loss = hk.remat(one_step_loss)
|
| 296 |
+
|
| 297 |
+
# We can pass inputs (the initial state of the loop) in directly as a
|
| 298 |
+
# Dataset because the shape we pass in to scan is the same as the shape scan
|
| 299 |
+
# passes to the inner function. But, for scan_variables, we must flatten the
|
| 300 |
+
# targets (and unflatten them inside the inner function) because they are
|
| 301 |
+
# passed to the inner function per-timestep without the original time axis.
|
| 302 |
+
# The same apply to the optional forcing.
|
| 303 |
+
_, (per_timestep_losses, per_timestep_diagnostics) = hk.scan(
|
| 304 |
+
one_step_loss, inputs, scan_variables)
|
| 305 |
+
|
| 306 |
+
# Re-wrap loss and diagnostics as DataArray and average them over time:
|
| 307 |
+
(loss, diagnostics) = jax.tree_util.tree_map(
|
| 308 |
+
lambda x: xarray_jax.DataArray(x, dims=('time', 'batch')).mean( # pylint: disable=g-long-lambda
|
| 309 |
+
'time', skipna=False),
|
| 310 |
+
(per_timestep_losses, per_timestep_diagnostics))
|
| 311 |
+
|
| 312 |
+
return loss, diagnostics
|
model/graphcast/casting.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Wrappers that take care of casting."""
|
| 15 |
+
|
| 16 |
+
import contextlib
|
| 17 |
+
from typing import Any, Mapping, Tuple
|
| 18 |
+
|
| 19 |
+
import chex
|
| 20 |
+
from . import predictor_base
|
| 21 |
+
import haiku as hk
|
| 22 |
+
import jax
|
| 23 |
+
import jax.numpy as jnp
|
| 24 |
+
import numpy as np
|
| 25 |
+
import xarray
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
PyTree = Any
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Bfloat16Cast(predictor_base.Predictor):
|
| 32 |
+
"""Wrapper that casts all inputs to bfloat16 and outputs to targets dtype."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, predictor: predictor_base.Predictor, enabled: bool = True):
|
| 35 |
+
"""Inits the wrapper.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
predictor: predictor being wrapped.
|
| 39 |
+
enabled: disables the wrapper if False, for simpler hyperparameter scans.
|
| 40 |
+
|
| 41 |
+
"""
|
| 42 |
+
self._enabled = enabled
|
| 43 |
+
self._predictor = predictor
|
| 44 |
+
|
| 45 |
+
def __call__(self,
|
| 46 |
+
inputs: xarray.Dataset,
|
| 47 |
+
targets_template: xarray.Dataset,
|
| 48 |
+
forcings: xarray.Dataset,
|
| 49 |
+
**kwargs
|
| 50 |
+
) -> xarray.Dataset:
|
| 51 |
+
if not self._enabled:
|
| 52 |
+
return self._predictor(inputs, targets_template, forcings, **kwargs)
|
| 53 |
+
|
| 54 |
+
with bfloat16_variable_view():
|
| 55 |
+
predictions = self._predictor(
|
| 56 |
+
*_all_inputs_to_bfloat16(inputs, targets_template, forcings),
|
| 57 |
+
**kwargs,)
|
| 58 |
+
|
| 59 |
+
predictions_dtype = infer_floating_dtype(predictions) # pytype: disable=wrong-arg-types
|
| 60 |
+
if predictions_dtype != jnp.bfloat16:
|
| 61 |
+
raise ValueError(f'Expected bfloat16 output, got {predictions_dtype}')
|
| 62 |
+
|
| 63 |
+
targets_dtype = infer_floating_dtype(targets_template) # pytype: disable=wrong-arg-types
|
| 64 |
+
return tree_map_cast(
|
| 65 |
+
predictions, input_dtype=jnp.bfloat16, output_dtype=targets_dtype)
|
| 66 |
+
|
| 67 |
+
def loss(self,
|
| 68 |
+
inputs: xarray.Dataset,
|
| 69 |
+
targets: xarray.Dataset,
|
| 70 |
+
forcings: xarray.Dataset,
|
| 71 |
+
**kwargs,
|
| 72 |
+
) -> predictor_base.LossAndDiagnostics:
|
| 73 |
+
if not self._enabled:
|
| 74 |
+
return self._predictor.loss(inputs, targets, forcings, **kwargs)
|
| 75 |
+
|
| 76 |
+
with bfloat16_variable_view():
|
| 77 |
+
loss, scalars = self._predictor.loss(
|
| 78 |
+
*_all_inputs_to_bfloat16(inputs, targets, forcings), **kwargs)
|
| 79 |
+
|
| 80 |
+
if loss.dtype != jnp.bfloat16:
|
| 81 |
+
raise ValueError(f'Expected bfloat16 loss, got {loss.dtype}')
|
| 82 |
+
|
| 83 |
+
targets_dtype = infer_floating_dtype(targets) # pytype: disable=wrong-arg-types
|
| 84 |
+
|
| 85 |
+
# Note that casting back the loss to e.g. float32 should not affect data
|
| 86 |
+
# types of the backwards pass, because the first thing the backwards pass
|
| 87 |
+
# should do is to go backwards the casting op and cast back to bfloat16
|
| 88 |
+
# (and xprofs seem to confirm this).
|
| 89 |
+
return tree_map_cast((loss, scalars),
|
| 90 |
+
input_dtype=jnp.bfloat16, output_dtype=targets_dtype)
|
| 91 |
+
|
| 92 |
+
def loss_and_predictions( # pytype: disable=signature-mismatch # jax-ndarray
|
| 93 |
+
self,
|
| 94 |
+
inputs: xarray.Dataset,
|
| 95 |
+
targets: xarray.Dataset,
|
| 96 |
+
forcings: xarray.Dataset,
|
| 97 |
+
**kwargs,
|
| 98 |
+
) -> Tuple[predictor_base.LossAndDiagnostics,
|
| 99 |
+
xarray.Dataset]:
|
| 100 |
+
if not self._enabled:
|
| 101 |
+
return self._predictor.loss_and_predictions(inputs, targets, forcings, # pytype: disable=bad-return-type # jax-ndarray
|
| 102 |
+
**kwargs)
|
| 103 |
+
|
| 104 |
+
with bfloat16_variable_view():
|
| 105 |
+
(loss, scalars), predictions = self._predictor.loss_and_predictions(
|
| 106 |
+
*_all_inputs_to_bfloat16(inputs, targets, forcings), **kwargs)
|
| 107 |
+
|
| 108 |
+
if loss.dtype != jnp.bfloat16:
|
| 109 |
+
raise ValueError(f'Expected bfloat16 loss, got {loss.dtype}')
|
| 110 |
+
|
| 111 |
+
predictions_dtype = infer_floating_dtype(predictions) # pytype: disable=wrong-arg-types
|
| 112 |
+
if predictions_dtype != jnp.bfloat16:
|
| 113 |
+
raise ValueError(f'Expected bfloat16 output, got {predictions_dtype}')
|
| 114 |
+
|
| 115 |
+
targets_dtype = infer_floating_dtype(targets) # pytype: disable=wrong-arg-types
|
| 116 |
+
return tree_map_cast(((loss, scalars), predictions),
|
| 117 |
+
input_dtype=jnp.bfloat16, output_dtype=targets_dtype)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def infer_floating_dtype(data_vars: Mapping[str, chex.Array]) -> np.dtype:
|
| 121 |
+
"""Infers a floating dtype from an input mapping of data."""
|
| 122 |
+
dtypes = {
|
| 123 |
+
v.dtype
|
| 124 |
+
for k, v in data_vars.items() if jnp.issubdtype(v.dtype, np.floating)}
|
| 125 |
+
if len(dtypes) != 1:
|
| 126 |
+
dtypes_and_shapes = {
|
| 127 |
+
k: (v.dtype, v.shape)
|
| 128 |
+
for k, v in data_vars.items() if jnp.issubdtype(v.dtype, np.floating)}
|
| 129 |
+
raise ValueError(
|
| 130 |
+
f'Did not found exactly one floating dtype {dtypes} in input variables:'
|
| 131 |
+
f'{dtypes_and_shapes}')
|
| 132 |
+
return list(dtypes)[0]
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _all_inputs_to_bfloat16(
|
| 136 |
+
inputs: xarray.Dataset,
|
| 137 |
+
targets: xarray.Dataset,
|
| 138 |
+
forcings: xarray.Dataset,
|
| 139 |
+
) -> Tuple[xarray.Dataset,
|
| 140 |
+
xarray.Dataset,
|
| 141 |
+
xarray.Dataset]:
|
| 142 |
+
return (inputs.astype(jnp.bfloat16),
|
| 143 |
+
jax.tree.map(lambda x: x.astype(jnp.bfloat16), targets),
|
| 144 |
+
forcings.astype(jnp.bfloat16))
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def tree_map_cast(inputs: PyTree, input_dtype: np.dtype, output_dtype: np.dtype,
|
| 148 |
+
) -> PyTree:
|
| 149 |
+
def cast_fn(x):
|
| 150 |
+
if x.dtype == input_dtype:
|
| 151 |
+
return x.astype(output_dtype)
|
| 152 |
+
return jax.tree.map(cast_fn, inputs)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@contextlib.contextmanager
|
| 156 |
+
def bfloat16_variable_view(enabled: bool = True):
|
| 157 |
+
"""Context for Haiku modules with float32 params, but bfloat16 activations.
|
| 158 |
+
|
| 159 |
+
It works as follows:
|
| 160 |
+
* Every time a variable is requested to be created/set as np.bfloat16,
|
| 161 |
+
it will create an underlying float32 variable, instead.
|
| 162 |
+
* Every time a variable a variable is requested as bfloat16, it will check the
|
| 163 |
+
variable is of float32 type, and cast the variable to bfloat16.
|
| 164 |
+
|
| 165 |
+
Note the gradients are still computed and accumulated as float32, because
|
| 166 |
+
the params returned by init are float32, so the gradient function with
|
| 167 |
+
respect to the params will already include an implicit casting to float32.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
enabled: Only enables bfloat16 behavior if True.
|
| 171 |
+
|
| 172 |
+
Yields:
|
| 173 |
+
None
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
if enabled:
|
| 177 |
+
with hk.custom_creator(
|
| 178 |
+
_bfloat16_creator, state=True), hk.custom_getter(
|
| 179 |
+
_bfloat16_getter, state=True), hk.custom_setter(
|
| 180 |
+
_bfloat16_setter):
|
| 181 |
+
yield
|
| 182 |
+
else:
|
| 183 |
+
yield
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _bfloat16_creator(next_creator, shape, dtype, init, context):
|
| 187 |
+
"""Creates float32 variables when bfloat16 is requested."""
|
| 188 |
+
if context.original_dtype == jnp.bfloat16:
|
| 189 |
+
dtype = jnp.float32
|
| 190 |
+
return next_creator(shape, dtype, init)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _bfloat16_getter(next_getter, value, context):
|
| 194 |
+
"""Casts float32 to bfloat16 when bfloat16 was originally requested."""
|
| 195 |
+
if context.original_dtype == jnp.bfloat16:
|
| 196 |
+
assert value.dtype == jnp.float32
|
| 197 |
+
value = value.astype(jnp.bfloat16)
|
| 198 |
+
return next_getter(value)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _bfloat16_setter(next_setter, value, context):
|
| 202 |
+
"""Casts bfloat16 to float32 when bfloat16 was originally set."""
|
| 203 |
+
if context.original_dtype == jnp.bfloat16:
|
| 204 |
+
value = value.astype(jnp.float32)
|
| 205 |
+
return next_setter(value)
|
model/graphcast/checkpoint.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Serialize and deserialize trees."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
import io
|
| 18 |
+
import types
|
| 19 |
+
from typing import Any, BinaryIO, Optional, TypeVar
|
| 20 |
+
|
| 21 |
+
import numpy as np
|
| 22 |
+
|
| 23 |
+
_T = TypeVar("_T")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def dump(dest: BinaryIO, value: Any) -> None:
|
| 27 |
+
"""Dump a tree of dicts/dataclasses to a file object.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
dest: a file object to write to.
|
| 31 |
+
value: A tree of dicts, lists, tuples and dataclasses of numpy arrays and
|
| 32 |
+
other basic types. Unions are not supported, other than Optional/None
|
| 33 |
+
which is only supported in dataclasses, not in dicts, lists or tuples.
|
| 34 |
+
All leaves must be coercible to a numpy array, and recoverable as a single
|
| 35 |
+
arg to a type.
|
| 36 |
+
"""
|
| 37 |
+
buffer = io.BytesIO() # In case the destination doesn't support seeking.
|
| 38 |
+
np.savez(buffer, **_flatten(value))
|
| 39 |
+
dest.write(buffer.getvalue())
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load(source: BinaryIO, typ: type[_T]) -> _T:
|
| 43 |
+
"""Load from a file object and convert it to the specified type.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
source: a file object to read from.
|
| 47 |
+
typ: a type object that acts as a schema for deserialization. It must match
|
| 48 |
+
what was serialized. If a type is Any, it will be returned however numpy
|
| 49 |
+
serialized it, which is what you want for a tree of numpy arrays.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
the deserialized value as the specified type.
|
| 53 |
+
"""
|
| 54 |
+
return _convert_types(typ, _unflatten(np.load(source)))
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
_SEP = ":"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _flatten(tree: Any) -> dict[str, Any]:
|
| 61 |
+
"""Flatten a tree of dicts/dataclasses/lists/tuples to a single dict."""
|
| 62 |
+
if dataclasses.is_dataclass(tree):
|
| 63 |
+
# Don't use dataclasses.asdict as it is recursive so skips dropping None.
|
| 64 |
+
tree = {f.name: v for f in dataclasses.fields(tree)
|
| 65 |
+
if (v := getattr(tree, f.name)) is not None}
|
| 66 |
+
elif isinstance(tree, (list, tuple)):
|
| 67 |
+
tree = dict(enumerate(tree))
|
| 68 |
+
|
| 69 |
+
assert isinstance(tree, dict)
|
| 70 |
+
|
| 71 |
+
flat = {}
|
| 72 |
+
for k, v in tree.items():
|
| 73 |
+
k = str(k)
|
| 74 |
+
assert _SEP not in k
|
| 75 |
+
if dataclasses.is_dataclass(v) or isinstance(v, (dict, list, tuple)):
|
| 76 |
+
for a, b in _flatten(v).items():
|
| 77 |
+
flat[f"{k}{_SEP}{a}"] = b
|
| 78 |
+
else:
|
| 79 |
+
assert v is not None
|
| 80 |
+
flat[k] = v
|
| 81 |
+
return flat
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _unflatten(flat: dict[str, Any]) -> dict[str, Any]:
|
| 85 |
+
"""Unflatten a dict to a tree of dicts."""
|
| 86 |
+
tree = {}
|
| 87 |
+
for flat_key, v in flat.items():
|
| 88 |
+
node = tree
|
| 89 |
+
keys = flat_key.split(_SEP)
|
| 90 |
+
for k in keys[:-1]:
|
| 91 |
+
if k not in node:
|
| 92 |
+
node[k] = {}
|
| 93 |
+
node = node[k]
|
| 94 |
+
node[keys[-1]] = v
|
| 95 |
+
return tree
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _convert_types(typ: type[_T], value: Any) -> _T:
|
| 99 |
+
"""Convert some structure into the given type. The structures must match."""
|
| 100 |
+
if typ in (Any, ...):
|
| 101 |
+
return value
|
| 102 |
+
|
| 103 |
+
if typ in (int, float, str, bool):
|
| 104 |
+
return typ(value)
|
| 105 |
+
|
| 106 |
+
if typ is np.ndarray:
|
| 107 |
+
assert isinstance(value, np.ndarray)
|
| 108 |
+
return value
|
| 109 |
+
|
| 110 |
+
if dataclasses.is_dataclass(typ):
|
| 111 |
+
kwargs = {}
|
| 112 |
+
for f in dataclasses.fields(typ):
|
| 113 |
+
# Only support Optional for dataclasses, as numpy can't serialize it
|
| 114 |
+
# directly (without pickle), and dataclasses are the only case where we
|
| 115 |
+
# can know the full set of values and types and therefore know the
|
| 116 |
+
# non-existence must mean None.
|
| 117 |
+
if isinstance(f.type, (types.UnionType, type(Optional[int]))):
|
| 118 |
+
constructors = [t for t in f.type.__args__ if t is not types.NoneType]
|
| 119 |
+
if len(constructors) != 1:
|
| 120 |
+
raise TypeError(
|
| 121 |
+
"Optional works, Union with anything except None doesn't")
|
| 122 |
+
if f.name not in value:
|
| 123 |
+
kwargs[f.name] = None
|
| 124 |
+
continue
|
| 125 |
+
constructor = constructors[0]
|
| 126 |
+
else:
|
| 127 |
+
constructor = f.type
|
| 128 |
+
|
| 129 |
+
if f.name in value:
|
| 130 |
+
kwargs[f.name] = _convert_types(constructor, value[f.name])
|
| 131 |
+
else:
|
| 132 |
+
raise ValueError(f"Missing value: {f.name}")
|
| 133 |
+
return typ(**kwargs)
|
| 134 |
+
|
| 135 |
+
base_type = getattr(typ, "__origin__", None)
|
| 136 |
+
|
| 137 |
+
if base_type is dict:
|
| 138 |
+
assert len(typ.__args__) == 2
|
| 139 |
+
key_type, value_type = typ.__args__
|
| 140 |
+
return {_convert_types(key_type, k): _convert_types(value_type, v)
|
| 141 |
+
for k, v in value.items()}
|
| 142 |
+
|
| 143 |
+
if base_type is list:
|
| 144 |
+
assert len(typ.__args__) == 1
|
| 145 |
+
value_type = typ.__args__[0]
|
| 146 |
+
return [_convert_types(value_type, v)
|
| 147 |
+
for _, v in sorted(value.items(), key=lambda x: int(x[0]))]
|
| 148 |
+
|
| 149 |
+
if base_type is tuple:
|
| 150 |
+
if len(typ.__args__) == 2 and typ.__args__[1] == ...:
|
| 151 |
+
# An arbitrary length tuple of a single type, eg: tuple[int, ...]
|
| 152 |
+
value_type = typ.__args__[0]
|
| 153 |
+
return tuple(_convert_types(value_type, v)
|
| 154 |
+
for _, v in sorted(value.items(), key=lambda x: int(x[0])))
|
| 155 |
+
else:
|
| 156 |
+
# A fixed length tuple of arbitrary types, eg: tuple[int, str, float]
|
| 157 |
+
assert len(typ.__args__) == len(value)
|
| 158 |
+
return tuple(
|
| 159 |
+
_convert_types(t, v)
|
| 160 |
+
for t, (_, v) in zip(
|
| 161 |
+
typ.__args__, sorted(value.items(), key=lambda x: int(x[0]))))
|
| 162 |
+
|
| 163 |
+
# This is probably unreachable with reasonable serializable inputs.
|
| 164 |
+
try:
|
| 165 |
+
return typ(value)
|
| 166 |
+
except TypeError as e:
|
| 167 |
+
raise TypeError(
|
| 168 |
+
"_convert_types expects the type argument to be a dataclass defined "
|
| 169 |
+
"with types that are valid constructors (eg tuple is fine, Tuple "
|
| 170 |
+
"isn't), and accept a numpy array as the sole argument.") from e
|
model/graphcast/data_utils.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Dataset utilities."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Mapping, Sequence, Tuple, Union
|
| 17 |
+
|
| 18 |
+
from . import solar_radiation
|
| 19 |
+
import numpy as np
|
| 20 |
+
import pandas as pd
|
| 21 |
+
import xarray
|
| 22 |
+
|
| 23 |
+
TimedeltaLike = Any # Something convertible to pd.Timedelta.
|
| 24 |
+
TimedeltaStr = str # A string convertible to pd.Timedelta.
|
| 25 |
+
|
| 26 |
+
TargetLeadTimes = Union[
|
| 27 |
+
TimedeltaLike,
|
| 28 |
+
Sequence[TimedeltaLike],
|
| 29 |
+
slice # with TimedeltaLike as its start and stop.
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
_SEC_PER_HOUR = 3600
|
| 33 |
+
_HOUR_PER_DAY = 24
|
| 34 |
+
SEC_PER_DAY = _SEC_PER_HOUR * _HOUR_PER_DAY
|
| 35 |
+
_AVG_DAY_PER_YEAR = 365.24219
|
| 36 |
+
AVG_SEC_PER_YEAR = SEC_PER_DAY * _AVG_DAY_PER_YEAR
|
| 37 |
+
|
| 38 |
+
DAY_PROGRESS = "day_progress"
|
| 39 |
+
YEAR_PROGRESS = "year_progress"
|
| 40 |
+
_DERIVED_VARS = {
|
| 41 |
+
DAY_PROGRESS,
|
| 42 |
+
f"{DAY_PROGRESS}_sin",
|
| 43 |
+
f"{DAY_PROGRESS}_cos",
|
| 44 |
+
YEAR_PROGRESS,
|
| 45 |
+
f"{YEAR_PROGRESS}_sin",
|
| 46 |
+
f"{YEAR_PROGRESS}_cos",
|
| 47 |
+
}
|
| 48 |
+
TISR = "toa_incident_solar_radiation"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_year_progress(seconds_since_epoch: np.ndarray) -> np.ndarray:
|
| 52 |
+
"""Computes year progress for times in seconds.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
seconds_since_epoch: Times in seconds since the "epoch" (the point at which
|
| 56 |
+
UNIX time starts).
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Year progress normalized to be in the [0, 1) interval for each time point.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
# Start with the pure integer division, and then float at the very end.
|
| 63 |
+
# We will try to keep as much precision as possible.
|
| 64 |
+
years_since_epoch = (
|
| 65 |
+
seconds_since_epoch / SEC_PER_DAY / np.float64(_AVG_DAY_PER_YEAR)
|
| 66 |
+
)
|
| 67 |
+
# Note depending on how these ops are down, we may end up with a "weak_type"
|
| 68 |
+
# which can cause issues in subtle ways, and hard to track here.
|
| 69 |
+
# In any case, casting to float32 should get rid of the weak type.
|
| 70 |
+
# [0, 1.) Interval.
|
| 71 |
+
return np.mod(years_since_epoch, 1.0).astype(np.float32)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_day_progress(
|
| 75 |
+
seconds_since_epoch: np.ndarray,
|
| 76 |
+
longitude: np.ndarray,
|
| 77 |
+
) -> np.ndarray:
|
| 78 |
+
"""Computes day progress for times in seconds at each longitude.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
seconds_since_epoch: 1D array of times in seconds since the 'epoch' (the
|
| 82 |
+
point at which UNIX time starts).
|
| 83 |
+
longitude: 1D array of longitudes at which day progress is computed.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
2D array of day progress values normalized to be in the [0, 1) inverval
|
| 87 |
+
for each time point at each longitude.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
# [0.0, 1.0) Interval.
|
| 91 |
+
day_progress_greenwich = (
|
| 92 |
+
np.mod(seconds_since_epoch, SEC_PER_DAY) / SEC_PER_DAY
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# Offset the day progress to the longitude of each point on Earth.
|
| 96 |
+
longitude_offsets = np.deg2rad(longitude) / (2 * np.pi)
|
| 97 |
+
day_progress = np.mod(
|
| 98 |
+
day_progress_greenwich[..., np.newaxis] + longitude_offsets, 1.0
|
| 99 |
+
)
|
| 100 |
+
return day_progress.astype(np.float32)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def featurize_progress(
|
| 104 |
+
name: str, dims: Sequence[str], progress: np.ndarray
|
| 105 |
+
) -> Mapping[str, xarray.Variable]:
|
| 106 |
+
"""Derives features used by ML models from the `progress` variable.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
name: Base variable name from which features are derived.
|
| 110 |
+
dims: List of the output feature dimensions, e.g. ("day", "lon").
|
| 111 |
+
progress: Progress variable values.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Dictionary of xarray variables derived from the `progress` values. It
|
| 115 |
+
includes the original `progress` variable along with its sin and cos
|
| 116 |
+
transformations.
|
| 117 |
+
|
| 118 |
+
Raises:
|
| 119 |
+
ValueError if the number of feature dimensions is not equal to the number
|
| 120 |
+
of data dimensions.
|
| 121 |
+
"""
|
| 122 |
+
if len(dims) != progress.ndim:
|
| 123 |
+
raise ValueError(
|
| 124 |
+
f"Number of feature dimensions ({len(dims)}) must be equal to the"
|
| 125 |
+
f" number of data dimensions: {progress.ndim}."
|
| 126 |
+
)
|
| 127 |
+
progress_phase = progress * (2 * np.pi)
|
| 128 |
+
return {
|
| 129 |
+
name: xarray.Variable(dims, progress),
|
| 130 |
+
name + "_sin": xarray.Variable(dims, np.sin(progress_phase)),
|
| 131 |
+
name + "_cos": xarray.Variable(dims, np.cos(progress_phase)),
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_seconds_since_epoch(datetime_sequence: xarray.DataArray) -> np.ndarray:
|
| 136 |
+
"""Computes seconds since epoch from `data` in place if missing."""
|
| 137 |
+
# Note `datetime_sequence.astype("datetime64[s]").astype(np.int64)`
|
| 138 |
+
# does not work as xarrays always cast dates into nanoseconds!
|
| 139 |
+
return datetime_sequence.data.astype("datetime64[s]").astype(np.int64)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def add_derived_vars(data: xarray.Dataset) -> None:
|
| 143 |
+
"""Adds year and day progress features to `data` in place if missing.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
data: Xarray dataset to which derived features will be added.
|
| 147 |
+
|
| 148 |
+
Raises:
|
| 149 |
+
ValueError if `datetime` or `lon` are not in `data` coordinates.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
for coord in ("datetime", "lon"):
|
| 153 |
+
if coord not in data.coords:
|
| 154 |
+
raise ValueError(f"'{coord}' must be in `data` coordinates.")
|
| 155 |
+
|
| 156 |
+
# Compute seconds since epoch.
|
| 157 |
+
seconds_since_epoch = get_seconds_since_epoch(data.coords["datetime"])
|
| 158 |
+
batch_dim = ("batch",) if "batch" in data.dims else ()
|
| 159 |
+
|
| 160 |
+
# Add year progress features if missing.
|
| 161 |
+
if YEAR_PROGRESS not in data.data_vars:
|
| 162 |
+
year_progress = get_year_progress(seconds_since_epoch)
|
| 163 |
+
data.update(
|
| 164 |
+
featurize_progress(
|
| 165 |
+
name=YEAR_PROGRESS,
|
| 166 |
+
dims=batch_dim + ("time",),
|
| 167 |
+
progress=year_progress,
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Add day progress features if missing.
|
| 172 |
+
if DAY_PROGRESS not in data.data_vars:
|
| 173 |
+
longitude_coord = data.coords["lon"]
|
| 174 |
+
day_progress = get_day_progress(seconds_since_epoch, longitude_coord.data)
|
| 175 |
+
data.update(
|
| 176 |
+
featurize_progress(
|
| 177 |
+
name=DAY_PROGRESS,
|
| 178 |
+
dims=batch_dim + ("time",) + longitude_coord.dims,
|
| 179 |
+
progress=day_progress,
|
| 180 |
+
)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def add_tisr_var(data: xarray.Dataset) -> None:
|
| 185 |
+
"""Adds TISR feature to `data` in place if missing.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
data: Xarray dataset to which TISR feature will be added.
|
| 189 |
+
|
| 190 |
+
Raises:
|
| 191 |
+
ValueError if `datetime`, 'lat', or `lon` are not in `data` coordinates.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
if TISR in data.data_vars:
|
| 195 |
+
return
|
| 196 |
+
|
| 197 |
+
for coord in ("datetime", "lat", "lon"):
|
| 198 |
+
if coord not in data.coords:
|
| 199 |
+
raise ValueError(f"'{coord}' must be in `data` coordinates.")
|
| 200 |
+
|
| 201 |
+
# Remove `batch` dimension of size one if present. An error will be raised if
|
| 202 |
+
# the `batch` dimension exists and has size greater than one.
|
| 203 |
+
data_no_batch = data.squeeze("batch") if "batch" in data.dims else data
|
| 204 |
+
|
| 205 |
+
tisr = solar_radiation.get_toa_incident_solar_radiation_for_xarray(
|
| 206 |
+
data_no_batch, use_jit=True
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if "batch" in data.dims:
|
| 210 |
+
tisr = tisr.expand_dims("batch", axis=0)
|
| 211 |
+
|
| 212 |
+
data.update({TISR: tisr})
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def extract_input_target_times(
|
| 216 |
+
dataset: xarray.Dataset,
|
| 217 |
+
input_duration: TimedeltaLike,
|
| 218 |
+
target_lead_times: TargetLeadTimes,
|
| 219 |
+
) -> Tuple[xarray.Dataset, xarray.Dataset]:
|
| 220 |
+
"""Extracts inputs and targets for prediction, from a Dataset with a time dim.
|
| 221 |
+
|
| 222 |
+
The input period is assumed to be contiguous (specified by a duration), but
|
| 223 |
+
the targets can be a list of arbitrary lead times.
|
| 224 |
+
|
| 225 |
+
Examples:
|
| 226 |
+
|
| 227 |
+
# Use 18 hours of data as inputs, and two specific lead times as targets:
|
| 228 |
+
# 3 days and 5 days after the final input.
|
| 229 |
+
extract_inputs_targets(
|
| 230 |
+
dataset,
|
| 231 |
+
input_duration='18h',
|
| 232 |
+
target_lead_times=('3d', '5d')
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Use 1 day of data as input, and all lead times between 6 hours and
|
| 236 |
+
# 24 hours inclusive as targets. Demonstrates a friendlier supported string
|
| 237 |
+
# syntax.
|
| 238 |
+
extract_inputs_targets(
|
| 239 |
+
dataset,
|
| 240 |
+
input_duration='1 day',
|
| 241 |
+
target_lead_times=slice('6 hours', '24 hours')
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# Just use a single target lead time of 3 days:
|
| 245 |
+
extract_inputs_targets(
|
| 246 |
+
dataset,
|
| 247 |
+
input_duration='24h',
|
| 248 |
+
target_lead_times='3d'
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
dataset: An xarray.Dataset with a 'time' dimension whose coordinates are
|
| 253 |
+
timedeltas. It's assumed that the time coordinates have a fixed offset /
|
| 254 |
+
time resolution, and that the input_duration and target_lead_times are
|
| 255 |
+
multiples of this.
|
| 256 |
+
input_duration: pandas.Timedelta or something convertible to it (e.g. a
|
| 257 |
+
shorthand string like '6h' or '5d12h').
|
| 258 |
+
target_lead_times: Either a single lead time, a slice with start and stop
|
| 259 |
+
(inclusive) lead times, or a sequence of lead times. Lead times should be
|
| 260 |
+
Timedeltas (or something convertible to). They are given relative to the
|
| 261 |
+
final input timestep, and should be positive.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
inputs:
|
| 265 |
+
targets:
|
| 266 |
+
Two datasets with the same shape as the input dataset except that a
|
| 267 |
+
selection has been made from the time axis, and the origin of the
|
| 268 |
+
time coordinate will be shifted to refer to lead times relative to the
|
| 269 |
+
final input timestep. So for inputs the times will end at lead time 0,
|
| 270 |
+
for targets the time coordinates will refer to the lead times requested.
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
(target_lead_times, target_duration
|
| 274 |
+
) = _process_target_lead_times_and_get_duration(target_lead_times)
|
| 275 |
+
|
| 276 |
+
# Shift the coordinates for the time axis so that a timedelta of zero
|
| 277 |
+
# corresponds to the forecast reference time. That is, the final timestep
|
| 278 |
+
# that's available as input to the forecast, with all following timesteps
|
| 279 |
+
# forming the target period which needs to be predicted.
|
| 280 |
+
# This means the time coordinates are now forecast lead times.
|
| 281 |
+
time = dataset.coords["time"]
|
| 282 |
+
dataset = dataset.assign_coords(time=time + target_duration - time[-1])
|
| 283 |
+
|
| 284 |
+
# Slice out targets:
|
| 285 |
+
targets = dataset.sel({"time": target_lead_times})
|
| 286 |
+
|
| 287 |
+
input_duration = pd.Timedelta(input_duration)
|
| 288 |
+
# Both endpoints are inclusive with label-based slicing, so we offset by a
|
| 289 |
+
# small epsilon to make one of the endpoints non-inclusive:
|
| 290 |
+
zero = pd.Timedelta(0)
|
| 291 |
+
epsilon = pd.Timedelta(1, "ns")
|
| 292 |
+
inputs = dataset.sel({"time": slice(-input_duration + epsilon, zero)})
|
| 293 |
+
return inputs, targets
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def _process_target_lead_times_and_get_duration(
|
| 297 |
+
target_lead_times: TargetLeadTimes) -> TimedeltaLike:
|
| 298 |
+
"""Returns the minimum duration for the target lead times."""
|
| 299 |
+
if isinstance(target_lead_times, slice):
|
| 300 |
+
# A slice of lead times. xarray already accepts timedelta-like values for
|
| 301 |
+
# the begin/end/step of the slice.
|
| 302 |
+
if target_lead_times.start is None:
|
| 303 |
+
# If the start isn't specified, we assume it starts at the next timestep
|
| 304 |
+
# after lead time 0 (lead time 0 is the final input timestep):
|
| 305 |
+
target_lead_times = slice(
|
| 306 |
+
pd.Timedelta(1, "ns"), target_lead_times.stop, target_lead_times.step
|
| 307 |
+
)
|
| 308 |
+
target_duration = pd.Timedelta(target_lead_times.stop)
|
| 309 |
+
else:
|
| 310 |
+
if not isinstance(target_lead_times, (list, tuple, set)):
|
| 311 |
+
# A single lead time, which we wrap as a length-1 array to ensure there
|
| 312 |
+
# still remains a time dimension (here of length 1) for consistency.
|
| 313 |
+
target_lead_times = [target_lead_times]
|
| 314 |
+
|
| 315 |
+
# A list of multiple (not necessarily contiguous) lead times:
|
| 316 |
+
target_lead_times = [pd.Timedelta(x) for x in target_lead_times]
|
| 317 |
+
target_lead_times.sort()
|
| 318 |
+
target_duration = target_lead_times[-1]
|
| 319 |
+
return target_lead_times, target_duration
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def extract_inputs_targets_forcings(
|
| 323 |
+
dataset: xarray.Dataset,
|
| 324 |
+
*,
|
| 325 |
+
input_variables: Tuple[str, ...],
|
| 326 |
+
target_variables: Tuple[str, ...],
|
| 327 |
+
forcing_variables: Tuple[str, ...],
|
| 328 |
+
pressure_levels: Tuple[int, ...],
|
| 329 |
+
input_duration: TimedeltaLike,
|
| 330 |
+
target_lead_times: TargetLeadTimes,
|
| 331 |
+
) -> Tuple[xarray.Dataset, xarray.Dataset, xarray.Dataset]:
|
| 332 |
+
"""Extracts inputs, targets and forcings according to requirements."""
|
| 333 |
+
dataset = dataset.sel(level=list(pressure_levels))
|
| 334 |
+
|
| 335 |
+
# "Forcings" include derived variables that do not exist in the original ERA5
|
| 336 |
+
# or HRES datasets, as well as other variables (e.g. tisr) that need to be
|
| 337 |
+
# computed manually for the target lead times. Compute the requested ones.
|
| 338 |
+
if set(forcing_variables) & _DERIVED_VARS:
|
| 339 |
+
add_derived_vars(dataset)
|
| 340 |
+
if set(forcing_variables) & {TISR}:
|
| 341 |
+
add_tisr_var(dataset)
|
| 342 |
+
|
| 343 |
+
# `datetime` is needed by add_derived_vars but breaks autoregressive rollouts.
|
| 344 |
+
dataset = dataset.drop_vars("datetime")
|
| 345 |
+
|
| 346 |
+
inputs, targets = extract_input_target_times(
|
| 347 |
+
dataset,
|
| 348 |
+
input_duration=input_duration,
|
| 349 |
+
target_lead_times=target_lead_times)
|
| 350 |
+
|
| 351 |
+
if set(forcing_variables) & set(target_variables):
|
| 352 |
+
raise ValueError(
|
| 353 |
+
f"Forcing variables {forcing_variables} should not "
|
| 354 |
+
f"overlap with target variables {target_variables}."
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
inputs = inputs[list(input_variables)]
|
| 358 |
+
# The forcing uses the same time coordinates as the target.
|
| 359 |
+
forcings = targets[list(forcing_variables)]
|
| 360 |
+
targets = targets[list(target_variables)]
|
| 361 |
+
|
| 362 |
+
return inputs, targets, forcings
|
model/graphcast/deep_typed_graph_net.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""JAX implementation of Graph Networks Simulator.
|
| 15 |
+
|
| 16 |
+
Generalization to TypedGraphs of the deep Graph Neural Network from:
|
| 17 |
+
|
| 18 |
+
@inproceedings{pfaff2021learning,
|
| 19 |
+
title={Learning Mesh-Based Simulation with Graph Networks},
|
| 20 |
+
author={Pfaff, Tobias and Fortunato, Meire and Sanchez-Gonzalez, Alvaro and
|
| 21 |
+
Battaglia, Peter},
|
| 22 |
+
booktitle={International Conference on Learning Representations},
|
| 23 |
+
year={2021}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
@inproceedings{sanchez2020learning,
|
| 27 |
+
title={Learning to simulate complex physics with graph networks},
|
| 28 |
+
author={Sanchez-Gonzalez, Alvaro and Godwin, Jonathan and Pfaff, Tobias and
|
| 29 |
+
Ying, Rex and Leskovec, Jure and Battaglia, Peter},
|
| 30 |
+
booktitle={International conference on machine learning},
|
| 31 |
+
pages={8459--8468},
|
| 32 |
+
year={2020},
|
| 33 |
+
organization={PMLR}
|
| 34 |
+
}
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
import functools
|
| 38 |
+
from typing import Callable, List, Mapping, Optional, Tuple
|
| 39 |
+
|
| 40 |
+
import chex
|
| 41 |
+
from . import mlp as mlp_builder
|
| 42 |
+
from . import typed_graph
|
| 43 |
+
from . import typed_graph_net
|
| 44 |
+
import haiku as hk
|
| 45 |
+
import jax
|
| 46 |
+
import jax.numpy as jnp
|
| 47 |
+
import jraph
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
GraphToGraphNetwork = Callable[[typed_graph.TypedGraph], typed_graph.TypedGraph]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class DeepTypedGraphNet(hk.Module):
|
| 54 |
+
"""Deep Graph Neural Network.
|
| 55 |
+
|
| 56 |
+
It works with TypedGraphs with typed nodes and edges. It runs message
|
| 57 |
+
passing on all of the node sets and all of the edge sets in the graph. For
|
| 58 |
+
each message passing step a `typed_graph_net.InteractionNetwork` is used to
|
| 59 |
+
update the full TypedGraph by using different MLPs for each of the node sets
|
| 60 |
+
and each of the edge sets.
|
| 61 |
+
|
| 62 |
+
If embed_{nodes,edges} is specified the node/edge features will be embedded
|
| 63 |
+
into a fixed dimensionality before running the first step of message passing.
|
| 64 |
+
|
| 65 |
+
If {node,edge}_output_size the final node/edge features will be embedded into
|
| 66 |
+
the specified output size.
|
| 67 |
+
|
| 68 |
+
This class may be used for shared or unshared message passing:
|
| 69 |
+
* num_message_passing_steps = N, num_processor_repetitions = 1, gives
|
| 70 |
+
N layers of message passing with fully unshared weights:
|
| 71 |
+
[W_1, W_2, ... , W_M] (default)
|
| 72 |
+
* num_message_passing_steps = 1, num_processor_repetitions = M, gives
|
| 73 |
+
N layers of message passing with fully shared weights:
|
| 74 |
+
[W_1] * M
|
| 75 |
+
* num_message_passing_steps = N, num_processor_repetitions = M, gives
|
| 76 |
+
M*N layers of message passing with both shared and unshared message passing
|
| 77 |
+
such that the weights used at each iteration are:
|
| 78 |
+
[W_1, W_2, ... , W_N] * M
|
| 79 |
+
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self,
|
| 83 |
+
*,
|
| 84 |
+
node_latent_size: Mapping[str, int],
|
| 85 |
+
edge_latent_size: Mapping[str, int],
|
| 86 |
+
mlp_hidden_size: int,
|
| 87 |
+
mlp_num_hidden_layers: int,
|
| 88 |
+
num_message_passing_steps: int,
|
| 89 |
+
num_processor_repetitions: int = 1,
|
| 90 |
+
embed_nodes: bool = True,
|
| 91 |
+
embed_edges: bool = True,
|
| 92 |
+
node_output_size: Optional[Mapping[str, int]] = None,
|
| 93 |
+
edge_output_size: Optional[Mapping[str, int]] = None,
|
| 94 |
+
include_sent_messages_in_node_update: bool = False,
|
| 95 |
+
use_layer_norm: bool = True,
|
| 96 |
+
use_norm_conditioning: bool = False,
|
| 97 |
+
activation: str = "relu",
|
| 98 |
+
f32_aggregation: bool = False,
|
| 99 |
+
aggregate_edges_for_nodes_fn: str = "segment_sum",
|
| 100 |
+
aggregate_normalization: Optional[float] = None,
|
| 101 |
+
name: str = "DeepTypedGraphNet"):
|
| 102 |
+
"""Inits the model.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
node_latent_size: Size of the node latent representations.
|
| 106 |
+
edge_latent_size: Size of the edge latent representations.
|
| 107 |
+
mlp_hidden_size: Hidden layer size for all MLPs.
|
| 108 |
+
mlp_num_hidden_layers: Number of hidden layers in all MLPs.
|
| 109 |
+
num_message_passing_steps: Number of unshared message passing steps
|
| 110 |
+
in the processor steps.
|
| 111 |
+
num_processor_repetitions: Number of times that the same processor is
|
| 112 |
+
applied sequencially.
|
| 113 |
+
embed_nodes: If False, the node embedder will be omitted.
|
| 114 |
+
embed_edges: If False, the edge embedder will be omitted.
|
| 115 |
+
node_output_size: Size of the output node representations for
|
| 116 |
+
each node type. For node types not specified here, the latent node
|
| 117 |
+
representation from the output of the processor will be returned.
|
| 118 |
+
edge_output_size: Size of the output edge representations for
|
| 119 |
+
each edge type. For edge types not specified here, the latent edge
|
| 120 |
+
representation from the output of the processor will be returned.
|
| 121 |
+
include_sent_messages_in_node_update: Whether to include pooled sent
|
| 122 |
+
messages from each node in the node update.
|
| 123 |
+
use_layer_norm: Whether it uses layer norm or not.
|
| 124 |
+
use_norm_conditioning: If True, the latent feaures outputted by the
|
| 125 |
+
activation normalization that follows the MLPs (e.g. LayerNorm), rather
|
| 126 |
+
than being scaled/offset by learned parameters of the normalization
|
| 127 |
+
module, will be scaled/offset by offsets/biases produced by a linear
|
| 128 |
+
layer (with different weights for each MLP), which takes an extra
|
| 129 |
+
argument "global_norm_conditioning". This argument is used to condition
|
| 130 |
+
the normalization of all nodes and all edges (hence global), and would
|
| 131 |
+
usually only have a batch and feature axis. This is typically used to
|
| 132 |
+
condition diffusion models on the "diffusion time". Will raise an error
|
| 133 |
+
if this is set to True but the "global_norm_conditioning" is not passed
|
| 134 |
+
to the __call__ method, as well as if this is set to False, but
|
| 135 |
+
"global_norm_conditioning" is passed to the call method.
|
| 136 |
+
activation: name of activation function.
|
| 137 |
+
f32_aggregation: Use float32 in the edge aggregation.
|
| 138 |
+
aggregate_edges_for_nodes_fn: function used to aggregate messages to each
|
| 139 |
+
node.
|
| 140 |
+
aggregate_normalization: An optional constant that normalizes the output
|
| 141 |
+
of aggregate_edges_for_nodes_fn. For context, this can be used to
|
| 142 |
+
reduce the shock the model undergoes when switching resolution, which
|
| 143 |
+
increase the number of edges connected to a node. In particular, this is
|
| 144 |
+
useful when using segment_sum, but should not be combined with
|
| 145 |
+
segment_mean.
|
| 146 |
+
name: Name of the model.
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
super().__init__(name=name)
|
| 150 |
+
|
| 151 |
+
self._node_latent_size = node_latent_size
|
| 152 |
+
self._edge_latent_size = edge_latent_size
|
| 153 |
+
self._mlp_hidden_size = mlp_hidden_size
|
| 154 |
+
self._mlp_num_hidden_layers = mlp_num_hidden_layers
|
| 155 |
+
self._num_message_passing_steps = num_message_passing_steps
|
| 156 |
+
self._num_processor_repetitions = num_processor_repetitions
|
| 157 |
+
self._embed_nodes = embed_nodes
|
| 158 |
+
self._embed_edges = embed_edges
|
| 159 |
+
self._node_output_size = node_output_size
|
| 160 |
+
self._edge_output_size = edge_output_size
|
| 161 |
+
self._include_sent_messages_in_node_update = (
|
| 162 |
+
include_sent_messages_in_node_update)
|
| 163 |
+
if use_norm_conditioning and not use_layer_norm:
|
| 164 |
+
raise ValueError(
|
| 165 |
+
"`norm_conditioning` can only be used when "
|
| 166 |
+
"`use_layer_norm` is true."
|
| 167 |
+
)
|
| 168 |
+
self._use_layer_norm = use_layer_norm
|
| 169 |
+
self._use_norm_conditioning = use_norm_conditioning
|
| 170 |
+
self._activation = _get_activation_fn(activation)
|
| 171 |
+
self._f32_aggregation = f32_aggregation
|
| 172 |
+
self._aggregate_edges_for_nodes_fn = _get_aggregate_edges_for_nodes_fn(
|
| 173 |
+
aggregate_edges_for_nodes_fn)
|
| 174 |
+
self._aggregate_normalization = aggregate_normalization
|
| 175 |
+
|
| 176 |
+
if aggregate_normalization:
|
| 177 |
+
# using aggregate_normalization only makes sense with segment_sum.
|
| 178 |
+
assert aggregate_edges_for_nodes_fn == "segment_sum"
|
| 179 |
+
|
| 180 |
+
def __call__(self,
|
| 181 |
+
input_graph: typed_graph.TypedGraph,
|
| 182 |
+
global_norm_conditioning: Optional[chex.Array] = None
|
| 183 |
+
) -> typed_graph.TypedGraph:
|
| 184 |
+
"""Forward pass of the learnable dynamics model."""
|
| 185 |
+
embedder_network, processor_networks, decoder_network = (
|
| 186 |
+
self._networks_builder(input_graph, global_norm_conditioning)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Embed input features (if applicable).
|
| 190 |
+
latent_graph_0 = self._embed(input_graph, embedder_network)
|
| 191 |
+
|
| 192 |
+
# Do `m` message passing steps in the latent graphs.
|
| 193 |
+
latent_graph_m = self._process(latent_graph_0, processor_networks)
|
| 194 |
+
|
| 195 |
+
# Compute outputs from the last latent graph (if applicable).
|
| 196 |
+
return self._output(latent_graph_m, decoder_network)
|
| 197 |
+
|
| 198 |
+
def _networks_builder(
|
| 199 |
+
self,
|
| 200 |
+
graph_template: typed_graph.TypedGraph,
|
| 201 |
+
global_norm_conditioning: Optional[chex.Array] = None,
|
| 202 |
+
) -> Tuple[
|
| 203 |
+
GraphToGraphNetwork, List[GraphToGraphNetwork], GraphToGraphNetwork
|
| 204 |
+
]:
|
| 205 |
+
# TODO(aelkadi): move to mlp_builder.
|
| 206 |
+
def build_mlp(name, output_size):
|
| 207 |
+
mlp = hk.nets.MLP(
|
| 208 |
+
output_sizes=[self._mlp_hidden_size] * self._mlp_num_hidden_layers + [
|
| 209 |
+
output_size], name=name + "_mlp", activation=self._activation)
|
| 210 |
+
return jraph.concatenated_args(mlp)
|
| 211 |
+
|
| 212 |
+
def build_mlp_with_maybe_layer_norm(name, output_size):
|
| 213 |
+
network = build_mlp(name, output_size)
|
| 214 |
+
stages = [network]
|
| 215 |
+
if self._use_norm_conditioning:
|
| 216 |
+
if global_norm_conditioning is None:
|
| 217 |
+
raise ValueError(
|
| 218 |
+
"When using norm conditioning, `global_norm_conditioning` must"
|
| 219 |
+
"be passed to the call method.")
|
| 220 |
+
# If using norm conditioning, it is no longer the responsibility of the
|
| 221 |
+
# LayerNorm module itself to learn its scale and offset. These will be
|
| 222 |
+
# learned for the module by the norm conditioning layer instead.
|
| 223 |
+
create_scale = create_offset = False
|
| 224 |
+
else:
|
| 225 |
+
if global_norm_conditioning is not None:
|
| 226 |
+
raise ValueError(
|
| 227 |
+
"`globa_norm_conditioning` was passed, but `norm_conditioning`"
|
| 228 |
+
" is not enabled.")
|
| 229 |
+
create_scale = create_offset = True
|
| 230 |
+
|
| 231 |
+
if self._use_layer_norm:
|
| 232 |
+
layer_norm = hk.LayerNorm(
|
| 233 |
+
axis=-1, create_scale=create_scale, create_offset=create_offset,
|
| 234 |
+
name=name + "_layer_norm")
|
| 235 |
+
stages.append(layer_norm)
|
| 236 |
+
|
| 237 |
+
if self._use_norm_conditioning:
|
| 238 |
+
norm_conditioning_layer = mlp_builder.LinearNormConditioning(
|
| 239 |
+
name=name + "_norm_conditioning")
|
| 240 |
+
norm_conditioning_layer = functools.partial(
|
| 241 |
+
norm_conditioning_layer,
|
| 242 |
+
# Broadcast to the node/edge axis.
|
| 243 |
+
norm_conditioning=global_norm_conditioning[None],
|
| 244 |
+
)
|
| 245 |
+
stages.append(norm_conditioning_layer)
|
| 246 |
+
|
| 247 |
+
network = hk.Sequential(stages)
|
| 248 |
+
return jraph.concatenated_args(network)
|
| 249 |
+
|
| 250 |
+
# The embedder graph network independently embeds edge and node features.
|
| 251 |
+
if self._embed_edges:
|
| 252 |
+
embed_edge_fn = _build_update_fns_for_edge_types(
|
| 253 |
+
build_mlp_with_maybe_layer_norm,
|
| 254 |
+
graph_template,
|
| 255 |
+
"encoder_edges_",
|
| 256 |
+
output_sizes=self._edge_latent_size)
|
| 257 |
+
else:
|
| 258 |
+
embed_edge_fn = None
|
| 259 |
+
if self._embed_nodes:
|
| 260 |
+
embed_node_fn = _build_update_fns_for_node_types(
|
| 261 |
+
build_mlp_with_maybe_layer_norm,
|
| 262 |
+
graph_template,
|
| 263 |
+
"encoder_nodes_",
|
| 264 |
+
output_sizes=self._node_latent_size)
|
| 265 |
+
else:
|
| 266 |
+
embed_node_fn = None
|
| 267 |
+
embedder_kwargs = dict(
|
| 268 |
+
embed_edge_fn=embed_edge_fn,
|
| 269 |
+
embed_node_fn=embed_node_fn,
|
| 270 |
+
)
|
| 271 |
+
embedder_network = typed_graph_net.GraphMapFeatures(
|
| 272 |
+
**embedder_kwargs)
|
| 273 |
+
|
| 274 |
+
if self._f32_aggregation:
|
| 275 |
+
def aggregate_fn(data, *args, **kwargs):
|
| 276 |
+
dtype = data.dtype
|
| 277 |
+
data = data.astype(jnp.float32)
|
| 278 |
+
output = self._aggregate_edges_for_nodes_fn(data, *args, **kwargs)
|
| 279 |
+
if self._aggregate_normalization:
|
| 280 |
+
output = output / self._aggregate_normalization
|
| 281 |
+
output = output.astype(dtype)
|
| 282 |
+
return output
|
| 283 |
+
|
| 284 |
+
else:
|
| 285 |
+
def aggregate_fn(data, *args, **kwargs):
|
| 286 |
+
output = self._aggregate_edges_for_nodes_fn(data, *args, **kwargs)
|
| 287 |
+
if self._aggregate_normalization:
|
| 288 |
+
output = output / self._aggregate_normalization
|
| 289 |
+
return output
|
| 290 |
+
|
| 291 |
+
# Create `num_message_passing_steps` graph networks with unshared parameters
|
| 292 |
+
# that update the node and edge latent features.
|
| 293 |
+
# Note that we can use `modules.InteractionNetwork` because
|
| 294 |
+
# it also outputs the messages as updated edge latent features.
|
| 295 |
+
processor_networks = []
|
| 296 |
+
for step_i in range(self._num_message_passing_steps):
|
| 297 |
+
processor_networks.append(
|
| 298 |
+
typed_graph_net.InteractionNetwork(
|
| 299 |
+
update_edge_fn=_build_update_fns_for_edge_types(
|
| 300 |
+
build_mlp_with_maybe_layer_norm,
|
| 301 |
+
graph_template,
|
| 302 |
+
f"processor_edges_{step_i}_",
|
| 303 |
+
output_sizes=self._edge_latent_size),
|
| 304 |
+
update_node_fn=_build_update_fns_for_node_types(
|
| 305 |
+
build_mlp_with_maybe_layer_norm,
|
| 306 |
+
graph_template,
|
| 307 |
+
f"processor_nodes_{step_i}_",
|
| 308 |
+
output_sizes=self._node_latent_size),
|
| 309 |
+
aggregate_edges_for_nodes_fn=aggregate_fn,
|
| 310 |
+
include_sent_messages_in_node_update=(
|
| 311 |
+
self._include_sent_messages_in_node_update),
|
| 312 |
+
))
|
| 313 |
+
|
| 314 |
+
# The output MLPs converts edge/node latent features into the output sizes.
|
| 315 |
+
output_kwargs = dict(
|
| 316 |
+
embed_edge_fn=_build_update_fns_for_edge_types(
|
| 317 |
+
build_mlp, graph_template, "decoder_edges_", self._edge_output_size)
|
| 318 |
+
if self._edge_output_size else None,
|
| 319 |
+
embed_node_fn=_build_update_fns_for_node_types(
|
| 320 |
+
build_mlp, graph_template, "decoder_nodes_", self._node_output_size)
|
| 321 |
+
if self._node_output_size else None,)
|
| 322 |
+
output_network = typed_graph_net.GraphMapFeatures(
|
| 323 |
+
**output_kwargs)
|
| 324 |
+
return embedder_network, processor_networks, output_network
|
| 325 |
+
|
| 326 |
+
def _embed(
|
| 327 |
+
self,
|
| 328 |
+
input_graph: typed_graph.TypedGraph,
|
| 329 |
+
embedder_network: GraphToGraphNetwork,
|
| 330 |
+
) -> typed_graph.TypedGraph:
|
| 331 |
+
"""Embeds the input graph features into a latent graph."""
|
| 332 |
+
|
| 333 |
+
# Copy the context to all of the node types, if applicable.
|
| 334 |
+
context_features = input_graph.context.features
|
| 335 |
+
if jax.tree_util.tree_leaves(context_features):
|
| 336 |
+
# This code assumes a single input feature array for the context and for
|
| 337 |
+
# each node type.
|
| 338 |
+
assert len(jax.tree_util.tree_leaves(context_features)) == 1
|
| 339 |
+
new_nodes = {}
|
| 340 |
+
for node_set_name, node_set in input_graph.nodes.items():
|
| 341 |
+
node_features = node_set.features
|
| 342 |
+
broadcasted_context = jnp.repeat(
|
| 343 |
+
context_features, node_set.n_node, axis=0,
|
| 344 |
+
total_repeat_length=node_features.shape[0])
|
| 345 |
+
new_nodes[node_set_name] = node_set._replace(
|
| 346 |
+
features=jnp.concatenate(
|
| 347 |
+
[node_features, broadcasted_context], axis=-1))
|
| 348 |
+
input_graph = input_graph._replace(
|
| 349 |
+
nodes=new_nodes,
|
| 350 |
+
context=input_graph.context._replace(features=()))
|
| 351 |
+
|
| 352 |
+
# Embeds the node and edge features.
|
| 353 |
+
latent_graph_0 = embedder_network(input_graph)
|
| 354 |
+
return latent_graph_0
|
| 355 |
+
|
| 356 |
+
def _process(
|
| 357 |
+
self,
|
| 358 |
+
latent_graph_0: typed_graph.TypedGraph,
|
| 359 |
+
processor_networks: List[GraphToGraphNetwork],
|
| 360 |
+
) -> typed_graph.TypedGraph:
|
| 361 |
+
"""Processes the latent graph with several steps of message passing."""
|
| 362 |
+
|
| 363 |
+
# Do `num_message_passing_steps` with each of the `self._processor_networks`
|
| 364 |
+
# with unshared weights, and repeat that `self._num_processor_repetitions`
|
| 365 |
+
# times.
|
| 366 |
+
latent_graph = latent_graph_0
|
| 367 |
+
for unused_repetition_i in range(self._num_processor_repetitions):
|
| 368 |
+
for processor_network in processor_networks:
|
| 369 |
+
latent_graph = self._process_step(processor_network, latent_graph)
|
| 370 |
+
|
| 371 |
+
return latent_graph
|
| 372 |
+
|
| 373 |
+
def _process_step(
|
| 374 |
+
self, processor_network_k,
|
| 375 |
+
latent_graph_prev_k: typed_graph.TypedGraph) -> typed_graph.TypedGraph:
|
| 376 |
+
"""Single step of message passing with node/edge residual connections."""
|
| 377 |
+
|
| 378 |
+
# One step of message passing.
|
| 379 |
+
latent_graph_k = processor_network_k(latent_graph_prev_k)
|
| 380 |
+
|
| 381 |
+
# Add residuals.
|
| 382 |
+
nodes_with_residuals = {}
|
| 383 |
+
for k, prev_set in latent_graph_prev_k.nodes.items():
|
| 384 |
+
nodes_with_residuals[k] = prev_set._replace(
|
| 385 |
+
features=prev_set.features + latent_graph_k.nodes[k].features)
|
| 386 |
+
|
| 387 |
+
edges_with_residuals = {}
|
| 388 |
+
for k, prev_set in latent_graph_prev_k.edges.items():
|
| 389 |
+
edges_with_residuals[k] = prev_set._replace(
|
| 390 |
+
features=prev_set.features + latent_graph_k.edges[k].features)
|
| 391 |
+
|
| 392 |
+
latent_graph_k = latent_graph_k._replace(
|
| 393 |
+
nodes=nodes_with_residuals, edges=edges_with_residuals)
|
| 394 |
+
return latent_graph_k
|
| 395 |
+
|
| 396 |
+
def _output(
|
| 397 |
+
self,
|
| 398 |
+
latent_graph: typed_graph.TypedGraph,
|
| 399 |
+
output_network: GraphToGraphNetwork,
|
| 400 |
+
) -> typed_graph.TypedGraph:
|
| 401 |
+
"""Produces the output from the latent graph."""
|
| 402 |
+
return output_network(latent_graph)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def _build_update_fns_for_node_types(
|
| 406 |
+
builder_fn, graph_template, prefix, output_sizes=None):
|
| 407 |
+
"""Builds an update function for all node types or a subset of them."""
|
| 408 |
+
|
| 409 |
+
output_fns = {}
|
| 410 |
+
for node_set_name in graph_template.nodes.keys():
|
| 411 |
+
if output_sizes is None:
|
| 412 |
+
# Use the default output size for all types.
|
| 413 |
+
output_size = None
|
| 414 |
+
else:
|
| 415 |
+
# Otherwise, ignore any type that does not have an explicit output size.
|
| 416 |
+
if node_set_name in output_sizes:
|
| 417 |
+
output_size = output_sizes[node_set_name]
|
| 418 |
+
else:
|
| 419 |
+
continue
|
| 420 |
+
output_fns[node_set_name] = builder_fn(
|
| 421 |
+
f"{prefix}{node_set_name}", output_size)
|
| 422 |
+
return output_fns
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def _build_update_fns_for_edge_types(
|
| 426 |
+
builder_fn, graph_template, prefix, output_sizes=None):
|
| 427 |
+
"""Builds an edge function for all node types or a subset of them."""
|
| 428 |
+
output_fns = {}
|
| 429 |
+
for edge_set_key in graph_template.edges.keys():
|
| 430 |
+
edge_set_name = edge_set_key.name
|
| 431 |
+
if output_sizes is None:
|
| 432 |
+
# Use the default output size for all types.
|
| 433 |
+
output_size = None
|
| 434 |
+
else:
|
| 435 |
+
# Otherwise, ignore any type that does not have an explicit output size.
|
| 436 |
+
if edge_set_name in output_sizes:
|
| 437 |
+
output_size = output_sizes[edge_set_name]
|
| 438 |
+
else:
|
| 439 |
+
continue
|
| 440 |
+
output_fns[edge_set_name] = builder_fn(
|
| 441 |
+
f"{prefix}{edge_set_name}", output_size)
|
| 442 |
+
return output_fns
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def _get_activation_fn(name):
|
| 446 |
+
"""Return activation function corresponding to function_name."""
|
| 447 |
+
if name == "identity":
|
| 448 |
+
return lambda x: x
|
| 449 |
+
if hasattr(jax.nn, name):
|
| 450 |
+
return getattr(jax.nn, name)
|
| 451 |
+
if hasattr(jnp, name):
|
| 452 |
+
return getattr(jnp, name)
|
| 453 |
+
raise ValueError(f"Unknown activation function {name} specified.")
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _get_aggregate_edges_for_nodes_fn(name):
|
| 457 |
+
"""Return aggregate_edges_for_nodes_fn corresponding to function_name."""
|
| 458 |
+
if hasattr(jraph, name):
|
| 459 |
+
return getattr(jraph, name)
|
| 460 |
+
raise ValueError(
|
| 461 |
+
f"Unknown aggregate_edges_for_nodes_fn function {name} specified.")
|
model/graphcast/denoiser.py
ADDED
|
@@ -0,0 +1,852 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Support for wrapping a general Predictor to act as a Denoiser."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
from typing import Any, Callable, Mapping, Optional, Sequence, Tuple
|
| 18 |
+
|
| 19 |
+
import chex
|
| 20 |
+
from . import deep_typed_graph_net
|
| 21 |
+
from . import denoisers_base as base
|
| 22 |
+
from . import grid_mesh_connectivity
|
| 23 |
+
from . import icosahedral_mesh
|
| 24 |
+
from . import model_utils
|
| 25 |
+
from . import sparse_transformer
|
| 26 |
+
from . import transformer
|
| 27 |
+
from . import typed_graph
|
| 28 |
+
from . import xarray_jax
|
| 29 |
+
import haiku as hk
|
| 30 |
+
import jax
|
| 31 |
+
import jax.numpy as jnp
|
| 32 |
+
import numpy as np
|
| 33 |
+
from scipy import sparse
|
| 34 |
+
import xarray
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
Kwargs = Mapping[str, Any]
|
| 38 |
+
NoiseLevelEncoder = Callable[[jnp.ndarray], jnp.ndarray]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class FourierFeaturesMLP(hk.Module):
|
| 42 |
+
"""A simple MLP applied to Fourier features of values or their logarithms."""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
base_period: float,
|
| 47 |
+
num_frequencies: int,
|
| 48 |
+
output_sizes: Sequence[int],
|
| 49 |
+
apply_log_first: bool = False,
|
| 50 |
+
w_init=None,
|
| 51 |
+
activation=jax.nn.gelu,
|
| 52 |
+
**mlp_kwargs
|
| 53 |
+
):
|
| 54 |
+
"""Initializes the module.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
base_period:
|
| 58 |
+
See model_utils.fourier_features. Note this would apply to log inputs if
|
| 59 |
+
apply_log_first is used.
|
| 60 |
+
num_frequencies:
|
| 61 |
+
See model_utils.fourier_features.
|
| 62 |
+
output_sizes:
|
| 63 |
+
Layer sizes for the MLP.
|
| 64 |
+
apply_log_first:
|
| 65 |
+
Whether to take the log of the inputs before computing Fourier features.
|
| 66 |
+
w_init:
|
| 67 |
+
Weights initializer for the MLP, default setting aims to produce
|
| 68 |
+
approx unit-variance outputs given the input sin/cos features.
|
| 69 |
+
activation:
|
| 70 |
+
**mlp_kwargs:
|
| 71 |
+
Further settings for the MLP.
|
| 72 |
+
"""
|
| 73 |
+
super().__init__()
|
| 74 |
+
self._base_period = base_period
|
| 75 |
+
self._num_frequencies = num_frequencies
|
| 76 |
+
self._apply_log_first = apply_log_first
|
| 77 |
+
if w_init is None:
|
| 78 |
+
# Scale of 2 is appropriate for input layer as sin/cos fourier features
|
| 79 |
+
# have variance 0.5 for random inputs. Also reasonable to use for later
|
| 80 |
+
# layers as relu activation cuts variance in half for inputs to later
|
| 81 |
+
# layers and gelu something close enough too.
|
| 82 |
+
w_init = hk.initializers.VarianceScaling(
|
| 83 |
+
2.0, mode="fan_in", distribution="uniform"
|
| 84 |
+
)
|
| 85 |
+
self._mlp = hk.nets.MLP(
|
| 86 |
+
output_sizes=output_sizes,
|
| 87 |
+
w_init=w_init,
|
| 88 |
+
activation=activation,
|
| 89 |
+
**mlp_kwargs)
|
| 90 |
+
|
| 91 |
+
def __call__(self, values: jnp.ndarray) -> jnp.ndarray:
|
| 92 |
+
if self._apply_log_first:
|
| 93 |
+
values = jnp.log(values)
|
| 94 |
+
|
| 95 |
+
features = model_utils.fourier_features(
|
| 96 |
+
values, self._base_period, self._num_frequencies)
|
| 97 |
+
|
| 98 |
+
return self._mlp(features)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 102 |
+
class NoiseEncoderConfig:
|
| 103 |
+
"""Configures the noise level encoding.
|
| 104 |
+
|
| 105 |
+
Properties:
|
| 106 |
+
apply_log_first: Whether to take the log of the inputs before computing
|
| 107 |
+
Fourier features.
|
| 108 |
+
base_period: The base period to use. This should be greater or equal to the
|
| 109 |
+
range of the values, or to the period if the values have periodic
|
| 110 |
+
semantics (e.g. 2pi if they represent angles). Frequencies used will be
|
| 111 |
+
integer multiples of 1/base_period.
|
| 112 |
+
num_frequencies: The number of frequencies to use, we will use integer
|
| 113 |
+
multiples of 1/base_period from 1 up to num_frequencies inclusive. (We
|
| 114 |
+
don't include a zero frequency as this would just give constant features
|
| 115 |
+
which are redundant if a bias term is present).
|
| 116 |
+
output_sizes: Layer sizes for the MLP.
|
| 117 |
+
"""
|
| 118 |
+
apply_log_first: bool = True
|
| 119 |
+
base_period: float = 16.0
|
| 120 |
+
num_frequencies: int = 32
|
| 121 |
+
# 2-layer MLP applied to Fourier features
|
| 122 |
+
output_sizes: tuple[int, int] = (32, 16)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@chex.dataclass(eq=True)
|
| 126 |
+
class SparseTransformerConfig:
|
| 127 |
+
"""Sparse Transformer config."""
|
| 128 |
+
# Neighbours to attend to.
|
| 129 |
+
attention_k_hop: int
|
| 130 |
+
# Primary width, the number of channels on the carrier path.
|
| 131 |
+
d_model: int
|
| 132 |
+
# Depth, or num transformer blocks. One 'layer' is attn + ffw.
|
| 133 |
+
num_layers: int = 16
|
| 134 |
+
# Number of heads for self-attention.
|
| 135 |
+
num_heads: int = 4
|
| 136 |
+
# Attention type.
|
| 137 |
+
attention_type: str = "splash_mha"
|
| 138 |
+
# mask type if splash attention being used.
|
| 139 |
+
mask_type: str = "lazy"
|
| 140 |
+
block_q: int = 1024
|
| 141 |
+
block_kv: int = 512
|
| 142 |
+
block_kv_compute: int = 256
|
| 143 |
+
block_q_dkv: int = 512
|
| 144 |
+
block_kv_dkv: int = 1024
|
| 145 |
+
block_kv_dkv_compute: int = 1024
|
| 146 |
+
# Init scale for final ffw layer (divided by depth)
|
| 147 |
+
ffw_winit_final_mult: float = 0.0
|
| 148 |
+
# Init scale for mha w (divided by depth).
|
| 149 |
+
attn_winit_final_mult: float = 0.0
|
| 150 |
+
# Number of hidden units in the MLP blocks. Defaults to 4 * d_model.
|
| 151 |
+
ffw_hidden: int = 2048
|
| 152 |
+
# Name for haiku module.
|
| 153 |
+
name: Optional[str] = None
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@chex.dataclass(eq=True)
|
| 157 |
+
class DenoiserArchitectureConfig:
|
| 158 |
+
"""Defines the GenCast architecture.
|
| 159 |
+
|
| 160 |
+
Properties:
|
| 161 |
+
sparse_transformer_config: Config for the mesh transformer.
|
| 162 |
+
mesh_size: How many refinements to do on the multi-mesh.
|
| 163 |
+
latent_size: How many latent features to include in the various MLPs.
|
| 164 |
+
hidden_layers: How many hidden layers for each MLP.
|
| 165 |
+
radius_query_fraction_edge_length: Scalar that will be multiplied by the
|
| 166 |
+
length of the longest edge of the finest mesh to define the radius of
|
| 167 |
+
connectivity to use in the Grid2Mesh graph. Reasonable values are
|
| 168 |
+
between 0.6 and 1. 0.6 reduces the number of grid points feeding into
|
| 169 |
+
multiple mesh nodes and therefore reduces edge count and memory use, but
|
| 170 |
+
1 gives better predictions.
|
| 171 |
+
norm_conditioning_features: List of feature names which will be used to
|
| 172 |
+
condition the GNN via norm_conditioning, rather than as regular
|
| 173 |
+
features. If this is provided, the GNN has to support the
|
| 174 |
+
`global_norm_conditioning` argument. For now it only supports global
|
| 175 |
+
norm conditioning (e.g. the same vector conditions all edges and nodes
|
| 176 |
+
normalization), which means features passed here must not have "lat" or
|
| 177 |
+
"lon" axes. In the future it may support node level norm conditioning
|
| 178 |
+
too.
|
| 179 |
+
grid2mesh_aggregate_normalization: Optional constant to normalize the output
|
| 180 |
+
of aggregate_edges_for_nodes_fn in the mesh2grid GNN. This can be used to
|
| 181 |
+
reduce the shock the model undergoes when switching resolution, which
|
| 182 |
+
increases the number of edges connected to a node.
|
| 183 |
+
node_output_size: Size of the output node representations for
|
| 184 |
+
each node type. For node types not specified here, the latent node
|
| 185 |
+
representation from the output of the processor will be returned.
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
sparse_transformer_config: SparseTransformerConfig
|
| 189 |
+
mesh_size: int
|
| 190 |
+
latent_size: int = 512
|
| 191 |
+
hidden_layers: int = 1
|
| 192 |
+
radius_query_fraction_edge_length: float = 0.6
|
| 193 |
+
norm_conditioning_features: tuple[str, ...] = ("noise_level_encodings",)
|
| 194 |
+
grid2mesh_aggregate_normalization: Optional[float] = None
|
| 195 |
+
node_output_size: Optional[int] = None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class Denoiser(base.Denoiser):
|
| 199 |
+
"""Wraps a general deterministic Predictor to act as a Denoiser.
|
| 200 |
+
|
| 201 |
+
This passes an encoding of the noise level as an additional input to the
|
| 202 |
+
Predictor as an additional input 'noise_level_encodings' with shape
|
| 203 |
+
('batch', 'noise_level_encoding_channels'). It passes the noisy_targets as
|
| 204 |
+
additional forcings (since they are also per-target-timestep data that the
|
| 205 |
+
predictor needs to condition on) with the same names as the original target
|
| 206 |
+
variables.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
def __init__(
|
| 210 |
+
self,
|
| 211 |
+
noise_encoder_config: Optional[NoiseEncoderConfig],
|
| 212 |
+
denoiser_architecture_config: DenoiserArchitectureConfig,
|
| 213 |
+
):
|
| 214 |
+
self._predictor = _DenoiserArchitecture(
|
| 215 |
+
denoiser_architecture_config=denoiser_architecture_config,
|
| 216 |
+
)
|
| 217 |
+
# Use default values if not specified.
|
| 218 |
+
if noise_encoder_config is None:
|
| 219 |
+
noise_encoder_config = NoiseEncoderConfig()
|
| 220 |
+
self._noise_level_encoder = FourierFeaturesMLP(**noise_encoder_config)
|
| 221 |
+
|
| 222 |
+
def __call__(
|
| 223 |
+
self,
|
| 224 |
+
inputs: xarray.Dataset,
|
| 225 |
+
noisy_targets: xarray.Dataset,
|
| 226 |
+
noise_levels: xarray.DataArray,
|
| 227 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 228 |
+
**kwargs) -> xarray.Dataset:
|
| 229 |
+
if forcings is None: forcings = xarray.Dataset()
|
| 230 |
+
forcings = forcings.assign(noisy_targets)
|
| 231 |
+
|
| 232 |
+
if noise_levels.dims != ("batch",):
|
| 233 |
+
raise ValueError("noise_levels expected to be shape (batch,).")
|
| 234 |
+
noise_level_encodings = self._noise_level_encoder(
|
| 235 |
+
xarray_jax.unwrap_data(noise_levels)
|
| 236 |
+
)
|
| 237 |
+
noise_level_encodings = xarray_jax.Variable(
|
| 238 |
+
("batch", "noise_level_encoding_channels"), noise_level_encodings
|
| 239 |
+
)
|
| 240 |
+
inputs = inputs.assign(noise_level_encodings=noise_level_encodings)
|
| 241 |
+
|
| 242 |
+
return self._predictor(
|
| 243 |
+
inputs=inputs,
|
| 244 |
+
targets_template=noisy_targets,
|
| 245 |
+
forcings=forcings,
|
| 246 |
+
**kwargs)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class _DenoiserArchitecture:
|
| 250 |
+
"""GenCast Predictor.
|
| 251 |
+
|
| 252 |
+
The model works on graphs that take into account:
|
| 253 |
+
* Mesh nodes: nodes for the vertices of the mesh.
|
| 254 |
+
* Grid nodes: nodes for the points of the grid.
|
| 255 |
+
* Nodes: When referring to just "nodes", this means the joint set of
|
| 256 |
+
both mesh nodes, concatenated with grid nodes.
|
| 257 |
+
|
| 258 |
+
The model works with 3 graphs:
|
| 259 |
+
* Grid2Mesh graph: Graph that contains all nodes. This graph is strictly
|
| 260 |
+
bipartite with edges going from grid nodes to mesh nodes using a
|
| 261 |
+
fixed radius query. The grid2mesh_gnn will operate in this graph. The output
|
| 262 |
+
of this stage will be a latent representation for the mesh nodes, and a
|
| 263 |
+
latent representation for the grid nodes.
|
| 264 |
+
* Mesh graph: Graph that contains mesh nodes only. The mesh_gnn will
|
| 265 |
+
operate in this graph. It will update the latent state of the mesh nodes
|
| 266 |
+
only.
|
| 267 |
+
* Mesh2Grid graph: Graph that contains all nodes. This graph is strictly
|
| 268 |
+
bipartite with edges going from mesh nodes to grid nodes such that each grid
|
| 269 |
+
node is connected to 3 nodes of the mesh triangular face that contains
|
| 270 |
+
the grid points. The mesh2grid_gnn will operate in this graph. It will
|
| 271 |
+
process the updated latent state of the mesh nodes, and the latent state
|
| 272 |
+
of the grid nodes, to produce the final output for the grid nodes.
|
| 273 |
+
|
| 274 |
+
The model is built on top of `TypedGraph`s so the different types of nodes and
|
| 275 |
+
edges can be stored and treated separately.
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
def __init__(
|
| 279 |
+
self,
|
| 280 |
+
denoiser_architecture_config: DenoiserArchitectureConfig,
|
| 281 |
+
):
|
| 282 |
+
"""Initializes the predictor."""
|
| 283 |
+
self._spatial_features_kwargs = dict(
|
| 284 |
+
add_node_positions=False,
|
| 285 |
+
add_node_latitude=True,
|
| 286 |
+
add_node_longitude=True,
|
| 287 |
+
add_relative_positions=True,
|
| 288 |
+
relative_longitude_local_coordinates=True,
|
| 289 |
+
relative_latitude_local_coordinates=True,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
# Construct the mesh.
|
| 293 |
+
mesh = icosahedral_mesh.get_last_triangular_mesh_for_sphere(
|
| 294 |
+
splits=denoiser_architecture_config.mesh_size
|
| 295 |
+
)
|
| 296 |
+
# Permute the mesh to a banded structure so we can run sparse attention
|
| 297 |
+
# operations.
|
| 298 |
+
self._mesh = _permute_mesh_to_banded(mesh=mesh)
|
| 299 |
+
|
| 300 |
+
# Encoder, which moves data from the grid to the mesh with a single message
|
| 301 |
+
# passing step.
|
| 302 |
+
self._grid2mesh_gnn = (
|
| 303 |
+
deep_typed_graph_net.DeepTypedGraphNet(
|
| 304 |
+
activation="swish",
|
| 305 |
+
aggregate_normalization=(
|
| 306 |
+
denoiser_architecture_config.grid2mesh_aggregate_normalization
|
| 307 |
+
),
|
| 308 |
+
edge_latent_size=dict(
|
| 309 |
+
grid2mesh=denoiser_architecture_config.latent_size
|
| 310 |
+
),
|
| 311 |
+
embed_edges=True,
|
| 312 |
+
embed_nodes=True,
|
| 313 |
+
f32_aggregation=True,
|
| 314 |
+
include_sent_messages_in_node_update=False,
|
| 315 |
+
mlp_hidden_size=denoiser_architecture_config.latent_size,
|
| 316 |
+
mlp_num_hidden_layers=denoiser_architecture_config.hidden_layers,
|
| 317 |
+
name="grid2mesh_gnn",
|
| 318 |
+
node_latent_size=dict(
|
| 319 |
+
grid_nodes=denoiser_architecture_config.latent_size,
|
| 320 |
+
mesh_nodes=denoiser_architecture_config.latent_size
|
| 321 |
+
),
|
| 322 |
+
node_output_size=None,
|
| 323 |
+
num_message_passing_steps=1,
|
| 324 |
+
use_layer_norm=True,
|
| 325 |
+
use_norm_conditioning=True,
|
| 326 |
+
)
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Processor - performs multiple rounds of message passing on the mesh.
|
| 330 |
+
self._mesh_gnn = transformer.MeshTransformer(
|
| 331 |
+
name="mesh_transformer",
|
| 332 |
+
transformer_ctor=sparse_transformer.Transformer,
|
| 333 |
+
transformer_kwargs=dataclasses.asdict(
|
| 334 |
+
denoiser_architecture_config.sparse_transformer_config
|
| 335 |
+
),
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Decoder, which moves data from the mesh back into the grid with a single
|
| 339 |
+
# message passing step.
|
| 340 |
+
self._mesh2grid_gnn = (
|
| 341 |
+
deep_typed_graph_net.DeepTypedGraphNet(
|
| 342 |
+
activation="swish",
|
| 343 |
+
edge_latent_size=dict(
|
| 344 |
+
mesh2grid=denoiser_architecture_config.latent_size
|
| 345 |
+
),
|
| 346 |
+
embed_nodes=False,
|
| 347 |
+
f32_aggregation=False,
|
| 348 |
+
include_sent_messages_in_node_update=False,
|
| 349 |
+
mlp_hidden_size=denoiser_architecture_config.latent_size,
|
| 350 |
+
mlp_num_hidden_layers=denoiser_architecture_config.hidden_layers,
|
| 351 |
+
name="mesh2grid_gnn",
|
| 352 |
+
node_latent_size=dict(
|
| 353 |
+
grid_nodes=denoiser_architecture_config.latent_size,
|
| 354 |
+
mesh_nodes=denoiser_architecture_config.latent_size,
|
| 355 |
+
),
|
| 356 |
+
node_output_size={
|
| 357 |
+
"grid_nodes": denoiser_architecture_config.node_output_size
|
| 358 |
+
},
|
| 359 |
+
num_message_passing_steps=1,
|
| 360 |
+
use_layer_norm=True,
|
| 361 |
+
use_norm_conditioning=True,
|
| 362 |
+
)
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
self._norm_conditioning_features = (
|
| 366 |
+
denoiser_architecture_config.norm_conditioning_features
|
| 367 |
+
)
|
| 368 |
+
# Obtain the query radius in absolute units for the unit-sphere for the
|
| 369 |
+
# grid2mesh model, by rescaling the `radius_query_fraction_edge_length`.
|
| 370 |
+
self._query_radius = (
|
| 371 |
+
_get_max_edge_distance(self._mesh)
|
| 372 |
+
* denoiser_architecture_config.radius_query_fraction_edge_length
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
# Other initialization is delayed until the first call (`_maybe_init`)
|
| 376 |
+
# when we get some sample data so we know the lat/lon values.
|
| 377 |
+
self._initialized = False
|
| 378 |
+
|
| 379 |
+
# A "_init_mesh_properties":
|
| 380 |
+
# This one could be initialized at init but we delay it for consistency too.
|
| 381 |
+
self._num_mesh_nodes = None # num_mesh_nodes
|
| 382 |
+
self._mesh_nodes_lat = None # [num_mesh_nodes]
|
| 383 |
+
self._mesh_nodes_lon = None # [num_mesh_nodes]
|
| 384 |
+
|
| 385 |
+
# A "_init_grid_properties":
|
| 386 |
+
self._grid_lat = None # [num_lat_points]
|
| 387 |
+
self._grid_lon = None # [num_lon_points]
|
| 388 |
+
self._num_grid_nodes = None # num_lat_points * num_lon_points
|
| 389 |
+
self._grid_nodes_lat = None # [num_grid_nodes]
|
| 390 |
+
self._grid_nodes_lon = None # [num_grid_nodes]
|
| 391 |
+
|
| 392 |
+
# A "_init_{grid2mesh,processor,mesh2grid}_graph"
|
| 393 |
+
self._grid2mesh_graph_structure = None
|
| 394 |
+
self._mesh_graph_structure = None
|
| 395 |
+
self._mesh2grid_graph_structure = None
|
| 396 |
+
|
| 397 |
+
def __call__(self,
|
| 398 |
+
inputs: xarray.Dataset,
|
| 399 |
+
targets_template: xarray.Dataset,
|
| 400 |
+
forcings: xarray.Dataset,
|
| 401 |
+
) -> xarray.Dataset:
|
| 402 |
+
self._maybe_init(inputs)
|
| 403 |
+
|
| 404 |
+
# Convert all input data into flat vectors for each of the grid nodes.
|
| 405 |
+
# xarray (batch, time, lat, lon, level, multiple vars, forcings)
|
| 406 |
+
# -> [num_grid_nodes, batch, num_channels]
|
| 407 |
+
grid_node_features, global_norm_conditioning = (
|
| 408 |
+
self._inputs_to_grid_node_features_and_norm_conditioning(
|
| 409 |
+
inputs, forcings
|
| 410 |
+
)
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# [num_mesh_nodes, batch, latent_size], [num_grid_nodes, batch, latent_size]
|
| 414 |
+
(latent_mesh_nodes, latent_grid_nodes) = self._run_grid2mesh_gnn(
|
| 415 |
+
grid_node_features, global_norm_conditioning
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
# Run message passing in the multimesh.
|
| 419 |
+
# [num_mesh_nodes, batch, latent_size]
|
| 420 |
+
updated_latent_mesh_nodes = self._run_mesh_gnn(
|
| 421 |
+
latent_mesh_nodes, global_norm_conditioning
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
# Transfer data from the mesh to the grid.
|
| 425 |
+
# [num_grid_nodes, batch, output_size]
|
| 426 |
+
output_grid_nodes = self._run_mesh2grid_gnn(
|
| 427 |
+
updated_latent_mesh_nodes, latent_grid_nodes, global_norm_conditioning
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# Convert output flat vectors for the grid nodes to the format of the
|
| 431 |
+
# output. [num_grid_nodes, batch, output_size] -> xarray (batch, one time
|
| 432 |
+
# step, lat, lon, level, multiple vars)
|
| 433 |
+
return self._grid_node_outputs_to_prediction(
|
| 434 |
+
output_grid_nodes, targets_template
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
def _maybe_init(self, sample_inputs: xarray.Dataset):
|
| 438 |
+
"""Inits everything that has a dependency on the input coordinates."""
|
| 439 |
+
if not self._initialized:
|
| 440 |
+
self._init_mesh_properties()
|
| 441 |
+
self._init_grid_properties(
|
| 442 |
+
grid_lat=sample_inputs.lat, grid_lon=sample_inputs.lon)
|
| 443 |
+
self._grid2mesh_graph_structure = self._init_grid2mesh_graph()
|
| 444 |
+
self._mesh_graph_structure = self._init_mesh_graph()
|
| 445 |
+
self._mesh2grid_graph_structure = self._init_mesh2grid_graph()
|
| 446 |
+
|
| 447 |
+
self._initialized = True
|
| 448 |
+
|
| 449 |
+
def _init_mesh_properties(self):
|
| 450 |
+
"""Inits static properties that have to do with mesh nodes."""
|
| 451 |
+
self._num_mesh_nodes = self._mesh.vertices.shape[0]
|
| 452 |
+
mesh_phi, mesh_theta = model_utils.cartesian_to_spherical(
|
| 453 |
+
self._mesh.vertices[:, 0],
|
| 454 |
+
self._mesh.vertices[:, 1],
|
| 455 |
+
self._mesh.vertices[:, 2])
|
| 456 |
+
(
|
| 457 |
+
mesh_nodes_lat,
|
| 458 |
+
mesh_nodes_lon,
|
| 459 |
+
) = model_utils.spherical_to_lat_lon(
|
| 460 |
+
phi=mesh_phi, theta=mesh_theta)
|
| 461 |
+
# Convert to f32 to ensure the lat/lon features aren't in f64.
|
| 462 |
+
self._mesh_nodes_lat = mesh_nodes_lat.astype(np.float32)
|
| 463 |
+
self._mesh_nodes_lon = mesh_nodes_lon.astype(np.float32)
|
| 464 |
+
|
| 465 |
+
def _init_grid_properties(self, grid_lat: np.ndarray, grid_lon: np.ndarray):
|
| 466 |
+
"""Inits static properties that have to do with grid nodes."""
|
| 467 |
+
self._grid_lat = grid_lat.astype(np.float32)
|
| 468 |
+
self._grid_lon = grid_lon.astype(np.float32)
|
| 469 |
+
# Initialized the counters.
|
| 470 |
+
self._num_grid_nodes = grid_lat.shape[0] * grid_lon.shape[0]
|
| 471 |
+
|
| 472 |
+
# Initialize lat and lon for the grid.
|
| 473 |
+
grid_nodes_lon, grid_nodes_lat = np.meshgrid(grid_lon, grid_lat)
|
| 474 |
+
self._grid_nodes_lon = grid_nodes_lon.reshape([-1]).astype(np.float32)
|
| 475 |
+
self._grid_nodes_lat = grid_nodes_lat.reshape([-1]).astype(np.float32)
|
| 476 |
+
|
| 477 |
+
def _init_grid2mesh_graph(self) -> typed_graph.TypedGraph:
|
| 478 |
+
"""Build Grid2Mesh graph."""
|
| 479 |
+
|
| 480 |
+
# Create some edges according to distance between mesh and grid nodes.
|
| 481 |
+
assert self._grid_lat is not None and self._grid_lon is not None
|
| 482 |
+
(grid_indices, mesh_indices) = grid_mesh_connectivity.radius_query_indices(
|
| 483 |
+
grid_latitude=self._grid_lat,
|
| 484 |
+
grid_longitude=self._grid_lon,
|
| 485 |
+
mesh=self._mesh,
|
| 486 |
+
radius=self._query_radius)
|
| 487 |
+
|
| 488 |
+
# Edges sending info from grid to mesh.
|
| 489 |
+
senders = grid_indices
|
| 490 |
+
receivers = mesh_indices
|
| 491 |
+
|
| 492 |
+
# Precompute structural node and edge features according to config options.
|
| 493 |
+
# Structural features are those that depend on the fixed values of the
|
| 494 |
+
# latitude and longitudes of the nodes.
|
| 495 |
+
(senders_node_features, receivers_node_features,
|
| 496 |
+
edge_features) = model_utils.get_bipartite_graph_spatial_features(
|
| 497 |
+
senders_node_lat=self._grid_nodes_lat,
|
| 498 |
+
senders_node_lon=self._grid_nodes_lon,
|
| 499 |
+
receivers_node_lat=self._mesh_nodes_lat,
|
| 500 |
+
receivers_node_lon=self._mesh_nodes_lon,
|
| 501 |
+
senders=senders,
|
| 502 |
+
receivers=receivers,
|
| 503 |
+
edge_normalization_factor=None,
|
| 504 |
+
**self._spatial_features_kwargs,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
n_grid_node = np.array([self._num_grid_nodes])
|
| 508 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 509 |
+
n_edge = np.array([mesh_indices.shape[0]])
|
| 510 |
+
grid_node_set = typed_graph.NodeSet(
|
| 511 |
+
n_node=n_grid_node, features=senders_node_features)
|
| 512 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 513 |
+
n_node=n_mesh_node, features=receivers_node_features)
|
| 514 |
+
edge_set = typed_graph.EdgeSet(
|
| 515 |
+
n_edge=n_edge,
|
| 516 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 517 |
+
features=edge_features)
|
| 518 |
+
nodes = {"grid_nodes": grid_node_set, "mesh_nodes": mesh_node_set}
|
| 519 |
+
edges = {
|
| 520 |
+
typed_graph.EdgeSetKey("grid2mesh", ("grid_nodes", "mesh_nodes")):
|
| 521 |
+
edge_set
|
| 522 |
+
}
|
| 523 |
+
grid2mesh_graph = typed_graph.TypedGraph(
|
| 524 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 525 |
+
nodes=nodes,
|
| 526 |
+
edges=edges)
|
| 527 |
+
return grid2mesh_graph
|
| 528 |
+
|
| 529 |
+
def _init_mesh_graph(self) -> typed_graph.TypedGraph:
|
| 530 |
+
"""Build Mesh graph."""
|
| 531 |
+
# Work simply on the mesh edges.
|
| 532 |
+
# N.B.To make sure ordering is preserved, any changes to faces_to_edges here
|
| 533 |
+
# should be reflected in the other 2 calls to faces_to_edges in this file.
|
| 534 |
+
senders, receivers = icosahedral_mesh.faces_to_edges(self._mesh.faces)
|
| 535 |
+
|
| 536 |
+
# Precompute structural node and edge features according to config options.
|
| 537 |
+
# Structural features are those that depend on the fixed values of the
|
| 538 |
+
# latitude and longitudes of the nodes.
|
| 539 |
+
assert self._mesh_nodes_lat is not None and self._mesh_nodes_lon is not None
|
| 540 |
+
node_features, edge_features = model_utils.get_graph_spatial_features(
|
| 541 |
+
node_lat=self._mesh_nodes_lat,
|
| 542 |
+
node_lon=self._mesh_nodes_lon,
|
| 543 |
+
senders=senders,
|
| 544 |
+
receivers=receivers,
|
| 545 |
+
**self._spatial_features_kwargs,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 549 |
+
n_edge = np.array([senders.shape[0]])
|
| 550 |
+
assert n_mesh_node == len(node_features)
|
| 551 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 552 |
+
n_node=n_mesh_node, features=node_features)
|
| 553 |
+
edge_set = typed_graph.EdgeSet(
|
| 554 |
+
n_edge=n_edge,
|
| 555 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 556 |
+
features=edge_features)
|
| 557 |
+
nodes = {"mesh_nodes": mesh_node_set}
|
| 558 |
+
edges = {
|
| 559 |
+
typed_graph.EdgeSetKey("mesh", ("mesh_nodes", "mesh_nodes")): edge_set
|
| 560 |
+
}
|
| 561 |
+
mesh_graph = typed_graph.TypedGraph(
|
| 562 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 563 |
+
nodes=nodes,
|
| 564 |
+
edges=edges)
|
| 565 |
+
|
| 566 |
+
return mesh_graph
|
| 567 |
+
|
| 568 |
+
def _init_mesh2grid_graph(self) -> typed_graph.TypedGraph:
|
| 569 |
+
"""Build Mesh2Grid graph."""
|
| 570 |
+
|
| 571 |
+
# Create some edges according to how the grid nodes are contained by
|
| 572 |
+
# mesh triangles.
|
| 573 |
+
(grid_indices,
|
| 574 |
+
mesh_indices) = grid_mesh_connectivity.in_mesh_triangle_indices(
|
| 575 |
+
grid_latitude=self._grid_lat,
|
| 576 |
+
grid_longitude=self._grid_lon,
|
| 577 |
+
mesh=self._mesh)
|
| 578 |
+
|
| 579 |
+
# Edges sending info from mesh to grid.
|
| 580 |
+
senders = mesh_indices
|
| 581 |
+
receivers = grid_indices
|
| 582 |
+
|
| 583 |
+
# Precompute structural node and edge features according to config options.
|
| 584 |
+
assert self._mesh_nodes_lat is not None and self._mesh_nodes_lon is not None
|
| 585 |
+
(senders_node_features, receivers_node_features,
|
| 586 |
+
edge_features) = model_utils.get_bipartite_graph_spatial_features(
|
| 587 |
+
senders_node_lat=self._mesh_nodes_lat,
|
| 588 |
+
senders_node_lon=self._mesh_nodes_lon,
|
| 589 |
+
receivers_node_lat=self._grid_nodes_lat,
|
| 590 |
+
receivers_node_lon=self._grid_nodes_lon,
|
| 591 |
+
senders=senders,
|
| 592 |
+
receivers=receivers,
|
| 593 |
+
edge_normalization_factor=None,
|
| 594 |
+
**self._spatial_features_kwargs,
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
n_grid_node = np.array([self._num_grid_nodes])
|
| 598 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 599 |
+
n_edge = np.array([senders.shape[0]])
|
| 600 |
+
grid_node_set = typed_graph.NodeSet(
|
| 601 |
+
n_node=n_grid_node, features=receivers_node_features)
|
| 602 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 603 |
+
n_node=n_mesh_node, features=senders_node_features)
|
| 604 |
+
edge_set = typed_graph.EdgeSet(
|
| 605 |
+
n_edge=n_edge,
|
| 606 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 607 |
+
features=edge_features)
|
| 608 |
+
nodes = {"grid_nodes": grid_node_set, "mesh_nodes": mesh_node_set}
|
| 609 |
+
edges = {
|
| 610 |
+
typed_graph.EdgeSetKey("mesh2grid", ("mesh_nodes", "grid_nodes")):
|
| 611 |
+
edge_set
|
| 612 |
+
}
|
| 613 |
+
mesh2grid_graph = typed_graph.TypedGraph(
|
| 614 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 615 |
+
nodes=nodes,
|
| 616 |
+
edges=edges)
|
| 617 |
+
return mesh2grid_graph
|
| 618 |
+
|
| 619 |
+
def _run_grid2mesh_gnn(self, grid_node_features: chex.Array,
|
| 620 |
+
global_norm_conditioning: Optional[chex.Array] = None,
|
| 621 |
+
) -> tuple[chex.Array, chex.Array]:
|
| 622 |
+
"""Runs the grid2mesh_gnn, extracting latent mesh and grid nodes."""
|
| 623 |
+
|
| 624 |
+
# Concatenate node structural features with input features.
|
| 625 |
+
batch_size = grid_node_features.shape[1]
|
| 626 |
+
|
| 627 |
+
grid2mesh_graph = self._grid2mesh_graph_structure
|
| 628 |
+
assert grid2mesh_graph is not None
|
| 629 |
+
grid_nodes = grid2mesh_graph.nodes["grid_nodes"]
|
| 630 |
+
mesh_nodes = grid2mesh_graph.nodes["mesh_nodes"]
|
| 631 |
+
new_grid_nodes = grid_nodes._replace(
|
| 632 |
+
features=jnp.concatenate([
|
| 633 |
+
grid_node_features,
|
| 634 |
+
_add_batch_second_axis(
|
| 635 |
+
grid_nodes.features.astype(grid_node_features.dtype),
|
| 636 |
+
batch_size)
|
| 637 |
+
],
|
| 638 |
+
axis=-1))
|
| 639 |
+
|
| 640 |
+
# To make sure capacity of the embedded is identical for the grid nodes and
|
| 641 |
+
# the mesh nodes, we also append some dummy zero input features for the
|
| 642 |
+
# mesh nodes.
|
| 643 |
+
dummy_mesh_node_features = jnp.zeros(
|
| 644 |
+
(self._num_mesh_nodes,) + grid_node_features.shape[1:],
|
| 645 |
+
dtype=grid_node_features.dtype)
|
| 646 |
+
new_mesh_nodes = mesh_nodes._replace(
|
| 647 |
+
features=jnp.concatenate([
|
| 648 |
+
dummy_mesh_node_features,
|
| 649 |
+
_add_batch_second_axis(
|
| 650 |
+
mesh_nodes.features.astype(dummy_mesh_node_features.dtype),
|
| 651 |
+
batch_size)
|
| 652 |
+
],
|
| 653 |
+
axis=-1))
|
| 654 |
+
|
| 655 |
+
# Broadcast edge structural features to the required batch size.
|
| 656 |
+
grid2mesh_edges_key = grid2mesh_graph.edge_key_by_name("grid2mesh")
|
| 657 |
+
edges = grid2mesh_graph.edges[grid2mesh_edges_key]
|
| 658 |
+
|
| 659 |
+
new_edges = edges._replace(
|
| 660 |
+
features=_add_batch_second_axis(
|
| 661 |
+
edges.features.astype(dummy_mesh_node_features.dtype), batch_size))
|
| 662 |
+
|
| 663 |
+
input_graph = self._grid2mesh_graph_structure._replace(
|
| 664 |
+
edges={grid2mesh_edges_key: new_edges},
|
| 665 |
+
nodes={
|
| 666 |
+
"grid_nodes": new_grid_nodes,
|
| 667 |
+
"mesh_nodes": new_mesh_nodes
|
| 668 |
+
})
|
| 669 |
+
|
| 670 |
+
# Run the GNN.
|
| 671 |
+
grid2mesh_out = self._grid2mesh_gnn(input_graph, global_norm_conditioning)
|
| 672 |
+
latent_mesh_nodes = grid2mesh_out.nodes["mesh_nodes"].features
|
| 673 |
+
latent_grid_nodes = grid2mesh_out.nodes["grid_nodes"].features
|
| 674 |
+
return latent_mesh_nodes, latent_grid_nodes
|
| 675 |
+
|
| 676 |
+
def _run_mesh_gnn(self, latent_mesh_nodes: chex.Array,
|
| 677 |
+
global_norm_conditioning: Optional[chex.Array] = None
|
| 678 |
+
) -> chex.Array:
|
| 679 |
+
"""Runs the mesh_gnn, extracting updated latent mesh nodes."""
|
| 680 |
+
|
| 681 |
+
# Add the structural edge features of this graph. Note we don't need
|
| 682 |
+
# to add the structural node features, because these are already part of
|
| 683 |
+
# the latent state, via the original Grid2Mesh gnn, however, we need
|
| 684 |
+
# the edge ones, because it is the first time we are seeing this particular
|
| 685 |
+
# set of edges.
|
| 686 |
+
batch_size = latent_mesh_nodes.shape[1]
|
| 687 |
+
|
| 688 |
+
mesh_graph = self._mesh_graph_structure
|
| 689 |
+
assert mesh_graph is not None
|
| 690 |
+
mesh_edges_key = mesh_graph.edge_key_by_name("mesh")
|
| 691 |
+
edges = mesh_graph.edges[mesh_edges_key]
|
| 692 |
+
|
| 693 |
+
# We are assuming here that the mesh gnn uses a single set of edge keys
|
| 694 |
+
# named "mesh" for the edges and that it uses a single set of nodes named
|
| 695 |
+
# "mesh_nodes"
|
| 696 |
+
msg = ("The setup currently requires to only have one kind of edge in the"
|
| 697 |
+
" mesh GNN.")
|
| 698 |
+
assert len(mesh_graph.edges) == 1, msg
|
| 699 |
+
|
| 700 |
+
new_edges = edges._replace(
|
| 701 |
+
features=_add_batch_second_axis(
|
| 702 |
+
edges.features.astype(latent_mesh_nodes.dtype), batch_size))
|
| 703 |
+
|
| 704 |
+
nodes = mesh_graph.nodes["mesh_nodes"]
|
| 705 |
+
nodes = nodes._replace(features=latent_mesh_nodes)
|
| 706 |
+
|
| 707 |
+
input_graph = mesh_graph._replace(
|
| 708 |
+
edges={mesh_edges_key: new_edges}, nodes={"mesh_nodes": nodes})
|
| 709 |
+
|
| 710 |
+
# Run the GNN.
|
| 711 |
+
return self._mesh_gnn(input_graph,
|
| 712 |
+
global_norm_conditioning=global_norm_conditioning
|
| 713 |
+
).nodes["mesh_nodes"].features
|
| 714 |
+
|
| 715 |
+
def _run_mesh2grid_gnn(self,
|
| 716 |
+
updated_latent_mesh_nodes: chex.Array,
|
| 717 |
+
latent_grid_nodes: chex.Array,
|
| 718 |
+
global_norm_conditioning: Optional[chex.Array] = None,
|
| 719 |
+
) -> chex.Array:
|
| 720 |
+
"""Runs the mesh2grid_gnn, extracting the output grid nodes."""
|
| 721 |
+
|
| 722 |
+
# Add the structural edge features of this graph. Note we don't need
|
| 723 |
+
# to add the structural node features, because these are already part of
|
| 724 |
+
# the latent state, via the original Grid2Mesh gnn, however, we need
|
| 725 |
+
# the edge ones, because it is the first time we are seeing this particular
|
| 726 |
+
# set of edges.
|
| 727 |
+
batch_size = updated_latent_mesh_nodes.shape[1]
|
| 728 |
+
|
| 729 |
+
mesh2grid_graph = self._mesh2grid_graph_structure
|
| 730 |
+
assert mesh2grid_graph is not None
|
| 731 |
+
mesh_nodes = mesh2grid_graph.nodes["mesh_nodes"]
|
| 732 |
+
grid_nodes = mesh2grid_graph.nodes["grid_nodes"]
|
| 733 |
+
new_mesh_nodes = mesh_nodes._replace(features=updated_latent_mesh_nodes)
|
| 734 |
+
new_grid_nodes = grid_nodes._replace(features=latent_grid_nodes)
|
| 735 |
+
mesh2grid_key = mesh2grid_graph.edge_key_by_name("mesh2grid")
|
| 736 |
+
edges = mesh2grid_graph.edges[mesh2grid_key]
|
| 737 |
+
|
| 738 |
+
new_edges = edges._replace(
|
| 739 |
+
features=_add_batch_second_axis(
|
| 740 |
+
edges.features.astype(latent_grid_nodes.dtype), batch_size))
|
| 741 |
+
|
| 742 |
+
input_graph = mesh2grid_graph._replace(
|
| 743 |
+
edges={mesh2grid_key: new_edges},
|
| 744 |
+
nodes={
|
| 745 |
+
"mesh_nodes": new_mesh_nodes,
|
| 746 |
+
"grid_nodes": new_grid_nodes
|
| 747 |
+
})
|
| 748 |
+
|
| 749 |
+
# Run the GNN.
|
| 750 |
+
output_graph = self._mesh2grid_gnn(input_graph, global_norm_conditioning)
|
| 751 |
+
output_grid_nodes = output_graph.nodes["grid_nodes"].features
|
| 752 |
+
|
| 753 |
+
return output_grid_nodes
|
| 754 |
+
|
| 755 |
+
def _inputs_to_grid_node_features_and_norm_conditioning(
|
| 756 |
+
self,
|
| 757 |
+
inputs: xarray.Dataset,
|
| 758 |
+
forcings: xarray.Dataset,
|
| 759 |
+
) -> Tuple[chex.Array, Optional[chex.Array]]:
|
| 760 |
+
"""xarray ->[n_grid_nodes, batch, n_channels], [batch, n_cond channels]."""
|
| 761 |
+
|
| 762 |
+
if self._norm_conditioning_features:
|
| 763 |
+
norm_conditioning_inputs = inputs[list(self._norm_conditioning_features)]
|
| 764 |
+
inputs = inputs.drop_vars(list(self._norm_conditioning_features))
|
| 765 |
+
|
| 766 |
+
if "lat" in norm_conditioning_inputs or "lon" in norm_conditioning_inputs:
|
| 767 |
+
raise ValueError("Features with lat or lon dims are not currently "
|
| 768 |
+
"supported for norm conditioning.")
|
| 769 |
+
global_norm_conditioning = xarray_jax.unwrap_data(
|
| 770 |
+
model_utils.dataset_to_stacked(norm_conditioning_inputs,
|
| 771 |
+
preserved_dims=("batch",),
|
| 772 |
+
).transpose("batch", ...))
|
| 773 |
+
|
| 774 |
+
else:
|
| 775 |
+
global_norm_conditioning = None
|
| 776 |
+
|
| 777 |
+
# xarray `Dataset` (batch, time, lat, lon, level, multiple vars)
|
| 778 |
+
# to xarray `DataArray` (batch, lat, lon, channels)
|
| 779 |
+
stacked_inputs = model_utils.dataset_to_stacked(inputs)
|
| 780 |
+
stacked_forcings = model_utils.dataset_to_stacked(forcings)
|
| 781 |
+
stacked_inputs = xarray.concat(
|
| 782 |
+
[stacked_inputs, stacked_forcings], dim="channels")
|
| 783 |
+
|
| 784 |
+
# xarray `DataArray` (batch, lat, lon, channels)
|
| 785 |
+
# to single numpy array with shape [lat_lon_node, batch, channels]
|
| 786 |
+
grid_xarray_lat_lon_leading = model_utils.lat_lon_to_leading_axes(
|
| 787 |
+
stacked_inputs)
|
| 788 |
+
# ["node", "batch", "features"]
|
| 789 |
+
grid_node_features = xarray_jax.unwrap(
|
| 790 |
+
grid_xarray_lat_lon_leading.data
|
| 791 |
+
).reshape((-1,) + grid_xarray_lat_lon_leading.data.shape[2:])
|
| 792 |
+
return grid_node_features, global_norm_conditioning
|
| 793 |
+
|
| 794 |
+
def _grid_node_outputs_to_prediction(
|
| 795 |
+
self,
|
| 796 |
+
grid_node_outputs: chex.Array,
|
| 797 |
+
targets_template: xarray.Dataset,
|
| 798 |
+
) -> xarray.Dataset:
|
| 799 |
+
"""[num_grid_nodes, batch, num_outputs] -> xarray."""
|
| 800 |
+
|
| 801 |
+
# numpy array with shape [lat_lon_node, batch, channels]
|
| 802 |
+
assert self._grid_lat is not None and self._grid_lon is not None
|
| 803 |
+
grid_shape = (self._grid_lat.shape[0], self._grid_lon.shape[0])
|
| 804 |
+
grid_outputs_lat_lon_leading = grid_node_outputs.reshape(
|
| 805 |
+
grid_shape + grid_node_outputs.shape[1:])
|
| 806 |
+
dims = ("lat", "lon", "batch", "channels")
|
| 807 |
+
grid_xarray_lat_lon_leading = xarray_jax.DataArray(
|
| 808 |
+
data=grid_outputs_lat_lon_leading,
|
| 809 |
+
dims=dims)
|
| 810 |
+
grid_xarray = model_utils.restore_leading_axes(grid_xarray_lat_lon_leading)
|
| 811 |
+
|
| 812 |
+
# xarray `DataArray` (batch, lat, lon, channels)
|
| 813 |
+
# to xarray `Dataset` (batch, one time step, lat, lon, level, multiple vars)
|
| 814 |
+
return model_utils.stacked_to_dataset(
|
| 815 |
+
grid_xarray.variable, targets_template)
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
def _add_batch_second_axis(data, batch_size):
|
| 819 |
+
# data [leading_dim, trailing_dim]
|
| 820 |
+
assert data.ndim == 2
|
| 821 |
+
ones = jnp.ones([batch_size, 1], dtype=data.dtype)
|
| 822 |
+
return data[:, None] * ones # [leading_dim, batch, trailing_dim]
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def _get_max_edge_distance(mesh):
|
| 826 |
+
# N.B.To make sure ordering is preserved, any changes to faces_to_edges here
|
| 827 |
+
# should be reflected in the other 2 calls to faces_to_edges in this file.
|
| 828 |
+
senders, receivers = icosahedral_mesh.faces_to_edges(mesh.faces)
|
| 829 |
+
edge_distances = np.linalg.norm(
|
| 830 |
+
mesh.vertices[senders] - mesh.vertices[receivers], axis=-1)
|
| 831 |
+
return edge_distances.max()
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
def _permute_mesh_to_banded(mesh):
|
| 835 |
+
"""Permutes the mesh nodes such that adjacency matrix has banded structure."""
|
| 836 |
+
# Build adjacency matrix.
|
| 837 |
+
# N.B.To make sure ordering is preserved, any changes to faces_to_edges here
|
| 838 |
+
# should be reflected in the other 2 calls to faces_to_edges in this file.
|
| 839 |
+
senders, receivers = icosahedral_mesh.faces_to_edges(mesh.faces)
|
| 840 |
+
num_mesh_nodes = mesh.vertices.shape[0]
|
| 841 |
+
adj_mat = sparse.csr_matrix((num_mesh_nodes, num_mesh_nodes))
|
| 842 |
+
adj_mat[senders, receivers] = 1
|
| 843 |
+
# Permutation to banded (this algorithm is deterministic, a given sparse
|
| 844 |
+
# adjacency matrix will yield the same permutation every time this is run).
|
| 845 |
+
mesh_permutation = sparse.csgraph.reverse_cuthill_mckee(
|
| 846 |
+
adj_mat, symmetric_mode=True
|
| 847 |
+
)
|
| 848 |
+
vertex_permutation_map = {j: i for i, j in enumerate(mesh_permutation)}
|
| 849 |
+
permute_func = np.vectorize(lambda x: vertex_permutation_map[x])
|
| 850 |
+
return icosahedral_mesh.TriangularMesh(
|
| 851 |
+
vertices=mesh.vertices[mesh_permutation], faces=permute_func(mesh.faces)
|
| 852 |
+
)
|
model/graphcast/denoisers_base.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Base class for Denoisers used in diffusion Predictors.
|
| 15 |
+
|
| 16 |
+
Denoisers are a bit like deterministic Predictors, except:
|
| 17 |
+
* Their __call__ method also conditions on noisy_targets and the noise_levels
|
| 18 |
+
of those noisy targets
|
| 19 |
+
* They don't have an overrideable loss function (the loss is assumed to be some
|
| 20 |
+
form of MSE and is implemented outside the Denoiser itself)
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from typing import Optional, Protocol
|
| 24 |
+
|
| 25 |
+
import xarray
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Denoiser(Protocol):
|
| 29 |
+
"""A denoising model that conditions on inputs as well as noise level."""
|
| 30 |
+
|
| 31 |
+
def __call__(
|
| 32 |
+
self,
|
| 33 |
+
inputs: xarray.Dataset,
|
| 34 |
+
noisy_targets: xarray.Dataset,
|
| 35 |
+
noise_levels: xarray.DataArray,
|
| 36 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 37 |
+
**kwargs) -> xarray.Dataset:
|
| 38 |
+
"""Computes denoised targets from noisy targets.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
inputs: Inputs to condition on, as for Predictor.__call__.
|
| 42 |
+
noisy_targets: Targets which have had i.i.d. zero-mean Gaussian noise
|
| 43 |
+
added to them (where the noise level used may vary along the 'batch'
|
| 44 |
+
dimension).
|
| 45 |
+
noise_levels: A DataArray with dimensions ('batch',) specifying the noise
|
| 46 |
+
levels that were used for each example in the batch.
|
| 47 |
+
forcings: Optional additional per-target-timestep forcings to condition
|
| 48 |
+
on, as for Predictor.__call__.
|
| 49 |
+
**kwargs: Any additional custom kwargs.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
Denoised predictions with the same shape as noisy_targets.
|
| 53 |
+
"""
|
model/graphcast/dpm_solver_plus_plus_2s.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""DPM-Solver++ 2S sampler from https://arxiv.org/abs/2211.01095."""
|
| 15 |
+
|
| 16 |
+
from typing import Optional
|
| 17 |
+
|
| 18 |
+
from . import casting
|
| 19 |
+
from . import denoisers_base
|
| 20 |
+
from . import samplers_base as base
|
| 21 |
+
from . import samplers_utils as utils
|
| 22 |
+
from . import xarray_jax
|
| 23 |
+
import haiku as hk
|
| 24 |
+
import jax.numpy as jnp
|
| 25 |
+
import xarray
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Sampler(base.Sampler):
|
| 29 |
+
"""Sampling using DPM-Solver++ 2S from [1].
|
| 30 |
+
|
| 31 |
+
This is combined with optional stochastic churn as described in [2].
|
| 32 |
+
|
| 33 |
+
The '2S' terminology from [1] means that this is a second-order (2),
|
| 34 |
+
single-step (S) solver. Here 'single-step' here distinguishes it from
|
| 35 |
+
'multi-step' methods where the results of function evaluations from previous
|
| 36 |
+
steps are reused in computing updates for subsequent steps. The solver still
|
| 37 |
+
uses multiple steps though.
|
| 38 |
+
|
| 39 |
+
[1] DPM-Solver++: Fast Solver for Guided Sampling of Diffusion Probabilistic
|
| 40 |
+
Models, https://arxiv.org/abs/2211.01095
|
| 41 |
+
[2] Elucidating the Design Space of Diffusion-Based Generative Models,
|
| 42 |
+
https://arxiv.org/abs/2206.00364
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self,
|
| 46 |
+
denoiser: denoisers_base.Denoiser,
|
| 47 |
+
max_noise_level: float,
|
| 48 |
+
min_noise_level: float,
|
| 49 |
+
num_noise_levels: int,
|
| 50 |
+
rho: float,
|
| 51 |
+
stochastic_churn_rate: float,
|
| 52 |
+
churn_min_noise_level: float,
|
| 53 |
+
churn_max_noise_level: float,
|
| 54 |
+
noise_level_inflation_factor: float
|
| 55 |
+
):
|
| 56 |
+
"""Initializes the sampler.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
denoiser: A Denoiser which predicts noise-free targets.
|
| 60 |
+
max_noise_level: The highest noise level used at the start of the
|
| 61 |
+
sequence of reverse diffusion steps.
|
| 62 |
+
min_noise_level: The lowest noise level used at the end of the sequence of
|
| 63 |
+
reverse diffusion steps.
|
| 64 |
+
num_noise_levels: Determines the number of noise levels used and hence the
|
| 65 |
+
number of reverse diffusion steps performed.
|
| 66 |
+
rho: Parameter affecting the spacing of noise steps. Higher values will
|
| 67 |
+
concentrate noise steps more around zero.
|
| 68 |
+
stochastic_churn_rate: S_churn from the paper. This controls the rate
|
| 69 |
+
at which noise is re-injected/'churned' during the sampling algorithm.
|
| 70 |
+
If this is set to zero then we are performing deterministic sampling
|
| 71 |
+
as described in Algorithm 1.
|
| 72 |
+
churn_min_noise_level: Minimum noise level at which stochastic churn
|
| 73 |
+
occurs. S_min from the paper. Only used if stochastic_churn_rate > 0.
|
| 74 |
+
churn_max_noise_level: Maximum noise level at which stochastic churn
|
| 75 |
+
occurs. S_min from the paper. Only used if stochastic_churn_rate > 0.
|
| 76 |
+
noise_level_inflation_factor: This can be used to set the actual amount of
|
| 77 |
+
noise injected higher than what the denoiser is told has been added.
|
| 78 |
+
The motivation is to compensate for a tendency of L2-trained denoisers
|
| 79 |
+
to remove slightly too much noise / blur too much. S_noise from the
|
| 80 |
+
paper. Only used if stochastic_churn_rate > 0.
|
| 81 |
+
"""
|
| 82 |
+
super().__init__(denoiser)
|
| 83 |
+
self._noise_levels = utils.noise_schedule(
|
| 84 |
+
max_noise_level, min_noise_level, num_noise_levels, rho)
|
| 85 |
+
self._stochastic_churn = stochastic_churn_rate > 0
|
| 86 |
+
self._per_step_churn_rates = utils.stochastic_churn_rate_schedule(
|
| 87 |
+
self._noise_levels, stochastic_churn_rate, churn_min_noise_level,
|
| 88 |
+
churn_max_noise_level)
|
| 89 |
+
self._noise_level_inflation_factor = noise_level_inflation_factor
|
| 90 |
+
|
| 91 |
+
def __call__(
|
| 92 |
+
self,
|
| 93 |
+
inputs: xarray.Dataset,
|
| 94 |
+
targets_template: xarray.Dataset,
|
| 95 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 96 |
+
**kwargs) -> xarray.Dataset:
|
| 97 |
+
|
| 98 |
+
dtype = casting.infer_floating_dtype(targets_template) # pytype: disable=wrong-arg-types
|
| 99 |
+
noise_levels = jnp.array(self._noise_levels).astype(dtype)
|
| 100 |
+
per_step_churn_rates = jnp.array(self._per_step_churn_rates).astype(dtype)
|
| 101 |
+
|
| 102 |
+
def denoiser(noise_level: jnp.ndarray, x: xarray.Dataset) -> xarray.Dataset:
|
| 103 |
+
"""Computes D(x, sigma, y)."""
|
| 104 |
+
bcast_noise_level = xarray_jax.DataArray(
|
| 105 |
+
jnp.tile(noise_level, x.sizes['batch']), dims=('batch',))
|
| 106 |
+
# Estimate the expectation of the fully-denoised target x0, conditional on
|
| 107 |
+
# inputs/forcings, noisy targets and their noise level:
|
| 108 |
+
return self._denoiser(
|
| 109 |
+
inputs=inputs,
|
| 110 |
+
noisy_targets=x,
|
| 111 |
+
noise_levels=bcast_noise_level,
|
| 112 |
+
forcings=forcings)
|
| 113 |
+
|
| 114 |
+
def body_fn(i: jnp.ndarray, x: xarray.Dataset) -> xarray.Dataset:
|
| 115 |
+
"""One iteration of the sampling algorithm.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
i: Sampling iteration.
|
| 119 |
+
x: Noisy targets at iteration i, these will have noise level
|
| 120 |
+
self._noise_levels[i].
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
Noisy targets at the next lowest noise level self._noise_levels[i+1].
|
| 124 |
+
"""
|
| 125 |
+
def init_noise(template):
|
| 126 |
+
return noise_levels[0] * utils.spherical_white_noise_like(template)
|
| 127 |
+
|
| 128 |
+
# Initialise the inputs if i == 0.
|
| 129 |
+
# This is done here to ensure both noise sampler calls can use the same
|
| 130 |
+
# spherical harmonic basis functions. While there may be a small compute
|
| 131 |
+
# cost the memory savings can be significant.
|
| 132 |
+
# TODO(dominicmasters): Figure out if we can merge the two noise sampler
|
| 133 |
+
# calls into one to avoid this hack.
|
| 134 |
+
maybe_init_noise = (i == 0).astype(noise_levels[0].dtype)
|
| 135 |
+
x = x + init_noise(x) * maybe_init_noise
|
| 136 |
+
|
| 137 |
+
noise_level = noise_levels[i]
|
| 138 |
+
|
| 139 |
+
if self._stochastic_churn:
|
| 140 |
+
# We increase the noise level of x a bit before taking it down again:
|
| 141 |
+
x, noise_level = utils.apply_stochastic_churn(
|
| 142 |
+
x, noise_level,
|
| 143 |
+
stochastic_churn_rate=per_step_churn_rates[i],
|
| 144 |
+
noise_level_inflation_factor=self._noise_level_inflation_factor)
|
| 145 |
+
|
| 146 |
+
# Apply one step of the ODE solver to take x down to the next lowest
|
| 147 |
+
# noise level.
|
| 148 |
+
|
| 149 |
+
# Note that the Elucidating paper's choice of sigma(t)=t and s(t)=1
|
| 150 |
+
# (corresponding to alpha(t)=1 in the DPM paper) as well as the standard
|
| 151 |
+
# choice of r=1/2 (corresponding to a geometric mean for the s_i
|
| 152 |
+
# midpoints) greatly simplifies the update from the DPM-Solver++ paper.
|
| 153 |
+
# You need to do a bit of algebraic fiddling to arrive at the below after
|
| 154 |
+
# substituting these choices into DPMSolver++'s Algorithm 1. The simpler
|
| 155 |
+
# update we arrive at helps with intuition too.
|
| 156 |
+
|
| 157 |
+
next_noise_level = noise_levels[i + 1]
|
| 158 |
+
# This is s_{i+1} from the paper. They don't explain how the s_i are
|
| 159 |
+
# chosen, but the default choice seems to be a geometric mean, which is
|
| 160 |
+
# equivalent to setting all the r_i = 1/2.
|
| 161 |
+
mid_noise_level = jnp.sqrt(noise_level * next_noise_level)
|
| 162 |
+
|
| 163 |
+
mid_over_current = mid_noise_level / noise_level
|
| 164 |
+
x_denoised = denoiser(noise_level, x)
|
| 165 |
+
# This turns out to be a convex combination of current and denoised x,
|
| 166 |
+
# which isn't entirely apparent from the paper formulae:
|
| 167 |
+
x_mid = mid_over_current * x + (1 - mid_over_current) * x_denoised
|
| 168 |
+
|
| 169 |
+
next_over_current = next_noise_level / noise_level
|
| 170 |
+
x_mid_denoised = denoiser(mid_noise_level, x_mid) # pytype: disable=wrong-arg-types
|
| 171 |
+
x_next = next_over_current * x + (1 - next_over_current) * x_mid_denoised
|
| 172 |
+
|
| 173 |
+
# For the final step to noise level 0, we do an Euler update which
|
| 174 |
+
# corresponds to just returning the denoiser's prediction directly.
|
| 175 |
+
#
|
| 176 |
+
# In fact the behaviour above when next_noise_level == 0 is almost
|
| 177 |
+
# equivalent, except that it runs the denoiser a second time to denoise
|
| 178 |
+
# from noise level 0. The denoiser should just be the identity function in
|
| 179 |
+
# this case, but it hasn't necessarily been trained at noise level 0 so
|
| 180 |
+
# we avoid relying on this.
|
| 181 |
+
return utils.tree_where(next_noise_level == 0, x_denoised, x_next)
|
| 182 |
+
|
| 183 |
+
# Init with zeros but apply additional noise at step 0 to initialise the
|
| 184 |
+
# state.
|
| 185 |
+
noise_init = xarray.zeros_like(targets_template)
|
| 186 |
+
return hk.fori_loop(
|
| 187 |
+
0, len(noise_levels) - 1, body_fun=body_fn, init_val=noise_init)
|
model/graphcast/gencast.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Denoising diffusion models based on the framework of [1].
|
| 15 |
+
|
| 16 |
+
Throughout we will refer to notation and equations from [1].
|
| 17 |
+
|
| 18 |
+
[1] Elucidating the Design Space of Diffusion-Based Generative Models
|
| 19 |
+
Karras, Aittala, Aila and Laine, 2022
|
| 20 |
+
https://arxiv.org/abs/2206.00364
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from typing import Any, Optional, Tuple
|
| 24 |
+
|
| 25 |
+
import chex
|
| 26 |
+
from . import casting
|
| 27 |
+
from . import denoiser
|
| 28 |
+
from . import dpm_solver_plus_plus_2s
|
| 29 |
+
from . import graphcast
|
| 30 |
+
from . import losses
|
| 31 |
+
from . import predictor_base
|
| 32 |
+
from . import samplers_utils
|
| 33 |
+
from . import xarray_jax
|
| 34 |
+
import haiku as hk
|
| 35 |
+
import jax
|
| 36 |
+
import xarray
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
TARGET_SURFACE_VARS = (
|
| 40 |
+
'2m_temperature',
|
| 41 |
+
'mean_sea_level_pressure',
|
| 42 |
+
'10m_v_component_of_wind',
|
| 43 |
+
'10m_u_component_of_wind', # GenCast predicts in 12hr timesteps.
|
| 44 |
+
'total_precipitation_12hr',
|
| 45 |
+
'sea_surface_temperature',
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
TARGET_SURFACE_NO_PRECIP_VARS = (
|
| 49 |
+
'2m_temperature',
|
| 50 |
+
'mean_sea_level_pressure',
|
| 51 |
+
'10m_v_component_of_wind',
|
| 52 |
+
'10m_u_component_of_wind',
|
| 53 |
+
'sea_surface_temperature',
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
TASK = graphcast.TaskConfig(
|
| 58 |
+
input_variables=(
|
| 59 |
+
# GenCast doesn't take precipitation as input.
|
| 60 |
+
TARGET_SURFACE_NO_PRECIP_VARS
|
| 61 |
+
+ graphcast.TARGET_ATMOSPHERIC_VARS
|
| 62 |
+
+ graphcast.GENERATED_FORCING_VARS
|
| 63 |
+
+ graphcast.STATIC_VARS
|
| 64 |
+
),
|
| 65 |
+
target_variables=TARGET_SURFACE_VARS + graphcast.TARGET_ATMOSPHERIC_VARS,
|
| 66 |
+
# GenCast doesn't take incident solar radiation as a forcing.
|
| 67 |
+
forcing_variables=graphcast.GENERATED_FORCING_VARS,
|
| 68 |
+
pressure_levels=graphcast.PRESSURE_LEVELS_WEATHERBENCH_13,
|
| 69 |
+
# GenCast takes the current frame and the frame 12 hours prior.
|
| 70 |
+
input_duration='24h',
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 75 |
+
class SamplerConfig:
|
| 76 |
+
"""Configures the sampler used to draw samples from GenCast.
|
| 77 |
+
|
| 78 |
+
max_noise_level: The highest noise level used at the start of the
|
| 79 |
+
sequence of reverse diffusion steps.
|
| 80 |
+
min_noise_level: The lowest noise level used at the end of the sequence of
|
| 81 |
+
reverse diffusion steps.
|
| 82 |
+
num_noise_levels: Determines the number of noise levels used and hence the
|
| 83 |
+
number of reverse diffusion steps performed.
|
| 84 |
+
rho: Parameter affecting the spacing of noise steps. Higher values will
|
| 85 |
+
concentrate noise steps more around zero.
|
| 86 |
+
stochastic_churn_rate: S_churn from the paper. This controls the rate
|
| 87 |
+
at which noise is re-injected/'churned' during the sampling algorithm.
|
| 88 |
+
If this is set to zero then we are performing deterministic sampling
|
| 89 |
+
as described in Algorithm 1.
|
| 90 |
+
churn_max_noise_level: Maximum noise level at which stochastic churn
|
| 91 |
+
occurs. S_min from the paper. Only used if stochastic_churn_rate > 0.
|
| 92 |
+
churn_min_noise_level: Minimum noise level at which stochastic churn
|
| 93 |
+
occurs. S_min from the paper. Only used if stochastic_churn_rate > 0.
|
| 94 |
+
noise_level_inflation_factor: This can be used to set the actual amount of
|
| 95 |
+
noise injected higher than what the denoiser is told has been added.
|
| 96 |
+
The motivation is to compensate for a tendency of L2-trained denoisers
|
| 97 |
+
to remove slightly too much noise / blur too much. S_noise from the
|
| 98 |
+
paper. Only used if stochastic_churn_rate > 0.
|
| 99 |
+
"""
|
| 100 |
+
max_noise_level: float = 80.
|
| 101 |
+
min_noise_level: float = 0.03
|
| 102 |
+
num_noise_levels: int = 20
|
| 103 |
+
rho: float = 7.
|
| 104 |
+
# Stochastic sampler settings.
|
| 105 |
+
stochastic_churn_rate: float = 2.5
|
| 106 |
+
churn_min_noise_level: float = 0.75
|
| 107 |
+
churn_max_noise_level: float = float('inf')
|
| 108 |
+
noise_level_inflation_factor: float = 1.05
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 112 |
+
class NoiseConfig:
|
| 113 |
+
training_noise_level_rho: float = 7.0
|
| 114 |
+
training_max_noise_level: float = 88.0
|
| 115 |
+
training_min_noise_level: float = 0.02
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 119 |
+
class CheckPoint:
|
| 120 |
+
description: str
|
| 121 |
+
license: str
|
| 122 |
+
params: dict[str, Any]
|
| 123 |
+
task_config: graphcast.TaskConfig
|
| 124 |
+
denoiser_architecture_config: denoiser.DenoiserArchitectureConfig
|
| 125 |
+
sampler_config: SamplerConfig
|
| 126 |
+
noise_config: NoiseConfig
|
| 127 |
+
noise_encoder_config: denoiser.NoiseEncoderConfig
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class GenCast(predictor_base.Predictor):
|
| 131 |
+
"""Predictor for a denoising diffusion model following the framework of [1].
|
| 132 |
+
|
| 133 |
+
[1] Elucidating the Design Space of Diffusion-Based Generative Models
|
| 134 |
+
Karras, Aittala, Aila and Laine, 2022
|
| 135 |
+
https://arxiv.org/abs/2206.00364
|
| 136 |
+
|
| 137 |
+
Unlike the paper, we have a conditional model and our denoising function
|
| 138 |
+
conditions on previous timesteps.
|
| 139 |
+
|
| 140 |
+
As the paper demonstrates, the sampling algorithm can be varied independently
|
| 141 |
+
of the denoising model and its training procedure, and it is separately
|
| 142 |
+
configurable here.
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
def __init__(
|
| 146 |
+
self,
|
| 147 |
+
task_config: graphcast.TaskConfig,
|
| 148 |
+
denoiser_architecture_config: denoiser.DenoiserArchitectureConfig,
|
| 149 |
+
sampler_config: Optional[SamplerConfig] = None,
|
| 150 |
+
noise_config: Optional[NoiseConfig] = None,
|
| 151 |
+
noise_encoder_config: Optional[denoiser.NoiseEncoderConfig] = None,
|
| 152 |
+
):
|
| 153 |
+
"""Constructs GenCast."""
|
| 154 |
+
# Output size depends on number of variables being predicted.
|
| 155 |
+
num_surface_vars = len(
|
| 156 |
+
set(task_config.target_variables)
|
| 157 |
+
- set(graphcast.ALL_ATMOSPHERIC_VARS)
|
| 158 |
+
)
|
| 159 |
+
num_atmospheric_vars = len(
|
| 160 |
+
set(task_config.target_variables)
|
| 161 |
+
& set(graphcast.ALL_ATMOSPHERIC_VARS)
|
| 162 |
+
)
|
| 163 |
+
num_outputs = (
|
| 164 |
+
num_surface_vars
|
| 165 |
+
+ len(task_config.pressure_levels) * num_atmospheric_vars
|
| 166 |
+
)
|
| 167 |
+
denoiser_architecture_config.node_output_size = num_outputs
|
| 168 |
+
self._denoiser = denoiser.Denoiser(
|
| 169 |
+
noise_encoder_config,
|
| 170 |
+
denoiser_architecture_config,
|
| 171 |
+
)
|
| 172 |
+
self._sampler_config = sampler_config
|
| 173 |
+
# Singleton to avoid re-initializing the sampler for each inference call.
|
| 174 |
+
self._sampler = None
|
| 175 |
+
self._noise_config = noise_config
|
| 176 |
+
|
| 177 |
+
def _c_in(self, noise_scale: xarray.DataArray) -> xarray.DataArray:
|
| 178 |
+
"""Scaling applied to the noisy targets input to the underlying network."""
|
| 179 |
+
return (noise_scale**2 + 1)**-0.5
|
| 180 |
+
|
| 181 |
+
def _c_out(self, noise_scale: xarray.DataArray) -> xarray.DataArray:
|
| 182 |
+
"""Scaling applied to the underlying network's raw outputs."""
|
| 183 |
+
return noise_scale * (noise_scale**2 + 1)**-0.5
|
| 184 |
+
|
| 185 |
+
def _c_skip(self, noise_scale: xarray.DataArray) -> xarray.DataArray:
|
| 186 |
+
"""Scaling applied to the skip connection."""
|
| 187 |
+
return 1 / (noise_scale**2 + 1)
|
| 188 |
+
|
| 189 |
+
def _loss_weighting(self, noise_scale: xarray.DataArray) -> xarray.DataArray:
|
| 190 |
+
r"""The loss weighting \lambda(\sigma) from the paper."""
|
| 191 |
+
return self._c_out(noise_scale) ** -2
|
| 192 |
+
|
| 193 |
+
def _preconditioned_denoiser(
|
| 194 |
+
self,
|
| 195 |
+
inputs: xarray.Dataset,
|
| 196 |
+
noisy_targets: xarray.Dataset,
|
| 197 |
+
noise_levels: xarray.DataArray,
|
| 198 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 199 |
+
**kwargs) -> xarray.Dataset:
|
| 200 |
+
"""The preconditioned denoising function D from the paper (Eqn 7)."""
|
| 201 |
+
raw_predictions = self._denoiser(
|
| 202 |
+
inputs=inputs,
|
| 203 |
+
noisy_targets=noisy_targets * self._c_in(noise_levels),
|
| 204 |
+
noise_levels=noise_levels,
|
| 205 |
+
forcings=forcings,
|
| 206 |
+
**kwargs)
|
| 207 |
+
return (raw_predictions * self._c_out(noise_levels) +
|
| 208 |
+
noisy_targets * self._c_skip(noise_levels))
|
| 209 |
+
|
| 210 |
+
def loss_and_predictions(
|
| 211 |
+
self,
|
| 212 |
+
inputs: xarray.Dataset,
|
| 213 |
+
targets: xarray.Dataset,
|
| 214 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 215 |
+
) -> Tuple[predictor_base.LossAndDiagnostics, xarray.Dataset]:
|
| 216 |
+
return self.loss(inputs, targets, forcings), self(inputs, targets, forcings)
|
| 217 |
+
|
| 218 |
+
def loss(self,
|
| 219 |
+
inputs: xarray.Dataset,
|
| 220 |
+
targets: xarray.Dataset,
|
| 221 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 222 |
+
) -> predictor_base.LossAndDiagnostics:
|
| 223 |
+
|
| 224 |
+
if self._noise_config is None:
|
| 225 |
+
raise ValueError('Noise config must be specified to train GenCast.')
|
| 226 |
+
|
| 227 |
+
# Sample noise levels:
|
| 228 |
+
dtype = casting.infer_floating_dtype(targets) # pytype: disable=wrong-arg-types
|
| 229 |
+
key = hk.next_rng_key()
|
| 230 |
+
batch_size = inputs.sizes['batch']
|
| 231 |
+
noise_levels = xarray_jax.DataArray(
|
| 232 |
+
data=samplers_utils.rho_inverse_cdf(
|
| 233 |
+
min_value=self._noise_config.training_min_noise_level,
|
| 234 |
+
max_value=self._noise_config.training_max_noise_level,
|
| 235 |
+
rho=self._noise_config.training_noise_level_rho,
|
| 236 |
+
cdf=jax.random.uniform(key, shape=(batch_size,), dtype=dtype)),
|
| 237 |
+
dims=('batch',))
|
| 238 |
+
|
| 239 |
+
# Sample noise and apply it to targets:
|
| 240 |
+
noise = (
|
| 241 |
+
samplers_utils.spherical_white_noise_like(targets) * noise_levels
|
| 242 |
+
)
|
| 243 |
+
noisy_targets = targets + noise
|
| 244 |
+
|
| 245 |
+
denoised_predictions = self._preconditioned_denoiser(
|
| 246 |
+
inputs, noisy_targets, noise_levels, forcings)
|
| 247 |
+
|
| 248 |
+
loss, diagnostics = losses.weighted_mse_per_level(
|
| 249 |
+
denoised_predictions,
|
| 250 |
+
targets,
|
| 251 |
+
# Weights are same as we used for GraphCast.
|
| 252 |
+
per_variable_weights={
|
| 253 |
+
# Any variables not specified here are weighted as 1.0.
|
| 254 |
+
# A single-level variable, but an important headline variable
|
| 255 |
+
# and also one which we have struggled to get good performance
|
| 256 |
+
# on at short lead times, so leaving it weighted at 1.0, equal
|
| 257 |
+
# to the multi-level variables:
|
| 258 |
+
'2m_temperature': 1.0,
|
| 259 |
+
# New single-level variables, which we don't weight too highly
|
| 260 |
+
# to avoid hurting performance on other variables.
|
| 261 |
+
'10m_u_component_of_wind': 0.1,
|
| 262 |
+
'10m_v_component_of_wind': 0.1,
|
| 263 |
+
'mean_sea_level_pressure': 0.1,
|
| 264 |
+
'sea_surface_temperature': 0.1,
|
| 265 |
+
'total_precipitation_12hr': 0.1
|
| 266 |
+
},
|
| 267 |
+
)
|
| 268 |
+
loss *= self._loss_weighting(noise_levels)
|
| 269 |
+
return loss, diagnostics
|
| 270 |
+
|
| 271 |
+
def __call__(self,
|
| 272 |
+
inputs: xarray.Dataset,
|
| 273 |
+
targets_template: xarray.Dataset,
|
| 274 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 275 |
+
**kwargs) -> xarray.Dataset:
|
| 276 |
+
if self._sampler_config is None:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
'Sampler config must be specified to run inference on GenCast.'
|
| 279 |
+
)
|
| 280 |
+
if self._sampler is None:
|
| 281 |
+
self._sampler = dpm_solver_plus_plus_2s.Sampler(
|
| 282 |
+
self._preconditioned_denoiser, **self._sampler_config
|
| 283 |
+
)
|
| 284 |
+
return self._sampler(inputs, targets_template, forcings, **kwargs)
|
model/graphcast/graphcast.py
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""A predictor that runs multiple graph neural networks on mesh data.
|
| 15 |
+
|
| 16 |
+
It learns to interpolate between the grid and the mesh nodes, with the loss
|
| 17 |
+
and the rollouts ultimately computed at the grid level.
|
| 18 |
+
|
| 19 |
+
It uses ideas similar to those in Keisler (2022):
|
| 20 |
+
|
| 21 |
+
Reference:
|
| 22 |
+
https://arxiv.org/pdf/2202.07575.pdf
|
| 23 |
+
|
| 24 |
+
It assumes data across time and level is stacked, and operates only operates in
|
| 25 |
+
a 2D mesh over latitudes and longitudes.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from typing import Any, Callable, Mapping, Optional
|
| 29 |
+
|
| 30 |
+
import chex
|
| 31 |
+
from . import deep_typed_graph_net
|
| 32 |
+
from . import grid_mesh_connectivity
|
| 33 |
+
from . import icosahedral_mesh
|
| 34 |
+
from . import losses
|
| 35 |
+
from . import model_utils
|
| 36 |
+
from . import predictor_base
|
| 37 |
+
from . import typed_graph
|
| 38 |
+
from . import xarray_jax
|
| 39 |
+
import jax.numpy as jnp
|
| 40 |
+
import jraph
|
| 41 |
+
import numpy as np
|
| 42 |
+
import xarray
|
| 43 |
+
|
| 44 |
+
Kwargs = Mapping[str, Any]
|
| 45 |
+
|
| 46 |
+
GNN = Callable[[jraph.GraphsTuple], jraph.GraphsTuple]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# https://www.ecmwf.int/en/forecasts/dataset/ecmwf-reanalysis-v5
|
| 50 |
+
PRESSURE_LEVELS_ERA5_37 = (
|
| 51 |
+
1, 2, 3, 5, 7, 10, 20, 30, 50, 70, 100, 125, 150, 175, 200, 225, 250, 300,
|
| 52 |
+
350, 400, 450, 500, 550, 600, 650, 700, 750, 775, 800, 825, 850, 875, 900,
|
| 53 |
+
925, 950, 975, 1000)
|
| 54 |
+
|
| 55 |
+
# https://www.ecmwf.int/en/forecasts/datasets/set-i
|
| 56 |
+
PRESSURE_LEVELS_HRES_25 = (
|
| 57 |
+
1, 2, 3, 5, 7, 10, 20, 30, 50, 70, 100, 150, 200, 250, 300, 400, 500, 600,
|
| 58 |
+
700, 800, 850, 900, 925, 950, 1000)
|
| 59 |
+
|
| 60 |
+
# https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2020MS002203
|
| 61 |
+
PRESSURE_LEVELS_WEATHERBENCH_13 = (
|
| 62 |
+
50, 100, 150, 200, 250, 300, 400, 500, 600, 700, 850, 925, 1000)
|
| 63 |
+
|
| 64 |
+
PRESSURE_LEVELS = {
|
| 65 |
+
13: PRESSURE_LEVELS_WEATHERBENCH_13,
|
| 66 |
+
25: PRESSURE_LEVELS_HRES_25,
|
| 67 |
+
37: PRESSURE_LEVELS_ERA5_37,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# The list of all possible atmospheric variables. Taken from:
|
| 71 |
+
# https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation#ERA5:datadocumentation-Table9
|
| 72 |
+
ALL_ATMOSPHERIC_VARS = (
|
| 73 |
+
"potential_vorticity",
|
| 74 |
+
"specific_rain_water_content",
|
| 75 |
+
"specific_snow_water_content",
|
| 76 |
+
"geopotential",
|
| 77 |
+
"temperature",
|
| 78 |
+
"u_component_of_wind",
|
| 79 |
+
"v_component_of_wind",
|
| 80 |
+
"specific_humidity",
|
| 81 |
+
"vertical_velocity",
|
| 82 |
+
"vorticity",
|
| 83 |
+
"divergence",
|
| 84 |
+
"relative_humidity",
|
| 85 |
+
"ozone_mass_mixing_ratio",
|
| 86 |
+
"specific_cloud_liquid_water_content",
|
| 87 |
+
"specific_cloud_ice_water_content",
|
| 88 |
+
"fraction_of_cloud_cover",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
TARGET_SURFACE_VARS = (
|
| 92 |
+
"2m_temperature",
|
| 93 |
+
"mean_sea_level_pressure",
|
| 94 |
+
"10m_v_component_of_wind",
|
| 95 |
+
"10m_u_component_of_wind",
|
| 96 |
+
"total_precipitation_6hr",
|
| 97 |
+
)
|
| 98 |
+
TARGET_SURFACE_NO_PRECIP_VARS = (
|
| 99 |
+
"2m_temperature",
|
| 100 |
+
"mean_sea_level_pressure",
|
| 101 |
+
"10m_v_component_of_wind",
|
| 102 |
+
"10m_u_component_of_wind",
|
| 103 |
+
)
|
| 104 |
+
TARGET_ATMOSPHERIC_VARS = (
|
| 105 |
+
"temperature",
|
| 106 |
+
"geopotential",
|
| 107 |
+
"u_component_of_wind",
|
| 108 |
+
"v_component_of_wind",
|
| 109 |
+
"vertical_velocity",
|
| 110 |
+
"specific_humidity",
|
| 111 |
+
)
|
| 112 |
+
TARGET_ATMOSPHERIC_NO_W_VARS = (
|
| 113 |
+
"temperature",
|
| 114 |
+
"geopotential",
|
| 115 |
+
"u_component_of_wind",
|
| 116 |
+
"v_component_of_wind",
|
| 117 |
+
"specific_humidity",
|
| 118 |
+
)
|
| 119 |
+
EXTERNAL_FORCING_VARS = (
|
| 120 |
+
"toa_incident_solar_radiation",
|
| 121 |
+
)
|
| 122 |
+
GENERATED_FORCING_VARS = (
|
| 123 |
+
"year_progress_sin",
|
| 124 |
+
"year_progress_cos",
|
| 125 |
+
"day_progress_sin",
|
| 126 |
+
"day_progress_cos",
|
| 127 |
+
)
|
| 128 |
+
FORCING_VARS = EXTERNAL_FORCING_VARS + GENERATED_FORCING_VARS
|
| 129 |
+
STATIC_VARS = (
|
| 130 |
+
"geopotential_at_surface",
|
| 131 |
+
"land_sea_mask",
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 136 |
+
class TaskConfig:
|
| 137 |
+
"""Defines inputs and targets on which a model is trained and/or evaluated."""
|
| 138 |
+
input_variables: tuple[str, ...]
|
| 139 |
+
# Target variables which the model is expected to predict.
|
| 140 |
+
target_variables: tuple[str, ...]
|
| 141 |
+
forcing_variables: tuple[str, ...]
|
| 142 |
+
pressure_levels: tuple[int, ...]
|
| 143 |
+
input_duration: str
|
| 144 |
+
|
| 145 |
+
TASK = TaskConfig(
|
| 146 |
+
input_variables=(
|
| 147 |
+
TARGET_SURFACE_VARS + TARGET_ATMOSPHERIC_VARS + FORCING_VARS +
|
| 148 |
+
STATIC_VARS),
|
| 149 |
+
target_variables=TARGET_SURFACE_VARS + TARGET_ATMOSPHERIC_VARS,
|
| 150 |
+
forcing_variables=FORCING_VARS,
|
| 151 |
+
pressure_levels=PRESSURE_LEVELS_ERA5_37,
|
| 152 |
+
input_duration="12h",
|
| 153 |
+
)
|
| 154 |
+
TASK_13 = TaskConfig(
|
| 155 |
+
input_variables=(
|
| 156 |
+
TARGET_SURFACE_VARS + TARGET_ATMOSPHERIC_VARS + FORCING_VARS +
|
| 157 |
+
STATIC_VARS),
|
| 158 |
+
target_variables=TARGET_SURFACE_VARS + TARGET_ATMOSPHERIC_VARS,
|
| 159 |
+
forcing_variables=FORCING_VARS,
|
| 160 |
+
pressure_levels=PRESSURE_LEVELS_WEATHERBENCH_13,
|
| 161 |
+
input_duration="12h",
|
| 162 |
+
)
|
| 163 |
+
TASK_13_PRECIP_OUT = TaskConfig(
|
| 164 |
+
input_variables=(
|
| 165 |
+
TARGET_SURFACE_NO_PRECIP_VARS + TARGET_ATMOSPHERIC_VARS + FORCING_VARS +
|
| 166 |
+
STATIC_VARS),
|
| 167 |
+
target_variables=TARGET_SURFACE_VARS + TARGET_ATMOSPHERIC_VARS,
|
| 168 |
+
forcing_variables=FORCING_VARS,
|
| 169 |
+
pressure_levels=PRESSURE_LEVELS_WEATHERBENCH_13,
|
| 170 |
+
input_duration="12h",
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 175 |
+
class ModelConfig:
|
| 176 |
+
"""Defines the architecture of the GraphCast neural network architecture.
|
| 177 |
+
|
| 178 |
+
Properties:
|
| 179 |
+
resolution: The resolution of the data, in degrees (e.g. 0.25 or 1.0).
|
| 180 |
+
mesh_size: How many refinements to do on the multi-mesh.
|
| 181 |
+
gnn_msg_steps: How many Graph Network message passing steps to do.
|
| 182 |
+
latent_size: How many latent features to include in the various MLPs.
|
| 183 |
+
hidden_layers: How many hidden layers for each MLP.
|
| 184 |
+
radius_query_fraction_edge_length: Scalar that will be multiplied by the
|
| 185 |
+
length of the longest edge of the finest mesh to define the radius of
|
| 186 |
+
connectivity to use in the Grid2Mesh graph. Reasonable values are
|
| 187 |
+
between 0.6 and 1. 0.6 reduces the number of grid points feeding into
|
| 188 |
+
multiple mesh nodes and therefore reduces edge count and memory use, but
|
| 189 |
+
1 gives better predictions.
|
| 190 |
+
mesh2grid_edge_normalization_factor: Allows explicitly controlling edge
|
| 191 |
+
normalization for mesh2grid edges. If None, defaults to max edge length.
|
| 192 |
+
This supports using pre-trained model weights with a different graph
|
| 193 |
+
structure to what it was trained on.
|
| 194 |
+
"""
|
| 195 |
+
resolution: float
|
| 196 |
+
mesh_size: int
|
| 197 |
+
latent_size: int
|
| 198 |
+
gnn_msg_steps: int
|
| 199 |
+
hidden_layers: int
|
| 200 |
+
radius_query_fraction_edge_length: float
|
| 201 |
+
mesh2grid_edge_normalization_factor: Optional[float] = None
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@chex.dataclass(frozen=True, eq=True)
|
| 205 |
+
class CheckPoint:
|
| 206 |
+
params: dict[str, Any]
|
| 207 |
+
model_config: ModelConfig
|
| 208 |
+
task_config: TaskConfig
|
| 209 |
+
description: str
|
| 210 |
+
license: str
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class GraphCast(predictor_base.Predictor):
|
| 214 |
+
"""GraphCast Predictor.
|
| 215 |
+
|
| 216 |
+
The model works on graphs that take into account:
|
| 217 |
+
* Mesh nodes: nodes for the vertices of the mesh.
|
| 218 |
+
* Grid nodes: nodes for the points of the grid.
|
| 219 |
+
* Nodes: When referring to just "nodes", this means the joint set of
|
| 220 |
+
both mesh nodes, concatenated with grid nodes.
|
| 221 |
+
|
| 222 |
+
The model works with 3 graphs:
|
| 223 |
+
* Grid2Mesh graph: Graph that contains all nodes. This graph is strictly
|
| 224 |
+
bipartite with edges going from grid nodes to mesh nodes using a
|
| 225 |
+
fixed radius query. The grid2mesh_gnn will operate in this graph. The output
|
| 226 |
+
of this stage will be a latent representation for the mesh nodes, and a
|
| 227 |
+
latent representation for the grid nodes.
|
| 228 |
+
* Mesh graph: Graph that contains mesh nodes only. The mesh_gnn will
|
| 229 |
+
operate in this graph. It will update the latent state of the mesh nodes
|
| 230 |
+
only.
|
| 231 |
+
* Mesh2Grid graph: Graph that contains all nodes. This graph is strictly
|
| 232 |
+
bipartite with edges going from mesh nodes to grid nodes such that each grid
|
| 233 |
+
nodes is connected to 3 nodes of the mesh triangular face that contains
|
| 234 |
+
the grid points. The mesh2grid_gnn will operate in this graph. It will
|
| 235 |
+
process the updated latent state of the mesh nodes, and the latent state
|
| 236 |
+
of the grid nodes, to produce the final output for the grid nodes.
|
| 237 |
+
|
| 238 |
+
The model is built on top of `TypedGraph`s so the different types of nodes and
|
| 239 |
+
edges can be stored and treated separately.
|
| 240 |
+
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
def __init__(self, model_config: ModelConfig, task_config: TaskConfig):
|
| 244 |
+
"""Initializes the predictor."""
|
| 245 |
+
self._spatial_features_kwargs = dict(
|
| 246 |
+
add_node_positions=False,
|
| 247 |
+
add_node_latitude=True,
|
| 248 |
+
add_node_longitude=True,
|
| 249 |
+
add_relative_positions=True,
|
| 250 |
+
relative_longitude_local_coordinates=True,
|
| 251 |
+
relative_latitude_local_coordinates=True,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Specification of the multimesh.
|
| 255 |
+
self._meshes = (
|
| 256 |
+
icosahedral_mesh.get_hierarchy_of_triangular_meshes_for_sphere(
|
| 257 |
+
splits=model_config.mesh_size))
|
| 258 |
+
|
| 259 |
+
# Encoder, which moves data from the grid to the mesh with a single message
|
| 260 |
+
# passing step.
|
| 261 |
+
self._grid2mesh_gnn = deep_typed_graph_net.DeepTypedGraphNet(
|
| 262 |
+
embed_nodes=True, # Embed raw features of the grid and mesh nodes.
|
| 263 |
+
embed_edges=True, # Embed raw features of the grid2mesh edges.
|
| 264 |
+
edge_latent_size=dict(grid2mesh=model_config.latent_size),
|
| 265 |
+
node_latent_size=dict(
|
| 266 |
+
mesh_nodes=model_config.latent_size,
|
| 267 |
+
grid_nodes=model_config.latent_size),
|
| 268 |
+
mlp_hidden_size=model_config.latent_size,
|
| 269 |
+
mlp_num_hidden_layers=model_config.hidden_layers,
|
| 270 |
+
num_message_passing_steps=1,
|
| 271 |
+
use_layer_norm=True,
|
| 272 |
+
include_sent_messages_in_node_update=False,
|
| 273 |
+
activation="swish",
|
| 274 |
+
f32_aggregation=True,
|
| 275 |
+
aggregate_normalization=None,
|
| 276 |
+
name="grid2mesh_gnn",
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# Processor, which performs message passing on the multi-mesh.
|
| 280 |
+
self._mesh_gnn = deep_typed_graph_net.DeepTypedGraphNet(
|
| 281 |
+
embed_nodes=False, # Node features already embdded by previous layers.
|
| 282 |
+
embed_edges=True, # Embed raw features of the multi-mesh edges.
|
| 283 |
+
node_latent_size=dict(mesh_nodes=model_config.latent_size),
|
| 284 |
+
edge_latent_size=dict(mesh=model_config.latent_size),
|
| 285 |
+
mlp_hidden_size=model_config.latent_size,
|
| 286 |
+
mlp_num_hidden_layers=model_config.hidden_layers,
|
| 287 |
+
num_message_passing_steps=model_config.gnn_msg_steps,
|
| 288 |
+
use_layer_norm=True,
|
| 289 |
+
include_sent_messages_in_node_update=False,
|
| 290 |
+
activation="swish",
|
| 291 |
+
f32_aggregation=False,
|
| 292 |
+
name="mesh_gnn",
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
num_surface_vars = len(
|
| 296 |
+
set(task_config.target_variables) - set(ALL_ATMOSPHERIC_VARS))
|
| 297 |
+
num_atmospheric_vars = len(
|
| 298 |
+
set(task_config.target_variables) & set(ALL_ATMOSPHERIC_VARS))
|
| 299 |
+
num_outputs = (num_surface_vars +
|
| 300 |
+
len(task_config.pressure_levels) * num_atmospheric_vars)
|
| 301 |
+
|
| 302 |
+
# Decoder, which moves data from the mesh back into the grid with a single
|
| 303 |
+
# message passing step.
|
| 304 |
+
self._mesh2grid_gnn = deep_typed_graph_net.DeepTypedGraphNet(
|
| 305 |
+
# Require a specific node dimensionaly for the grid node outputs.
|
| 306 |
+
node_output_size=dict(grid_nodes=num_outputs),
|
| 307 |
+
embed_nodes=False, # Node features already embdded by previous layers.
|
| 308 |
+
embed_edges=True, # Embed raw features of the mesh2grid edges.
|
| 309 |
+
edge_latent_size=dict(mesh2grid=model_config.latent_size),
|
| 310 |
+
node_latent_size=dict(
|
| 311 |
+
mesh_nodes=model_config.latent_size,
|
| 312 |
+
grid_nodes=model_config.latent_size),
|
| 313 |
+
mlp_hidden_size=model_config.latent_size,
|
| 314 |
+
mlp_num_hidden_layers=model_config.hidden_layers,
|
| 315 |
+
num_message_passing_steps=1,
|
| 316 |
+
use_layer_norm=True,
|
| 317 |
+
include_sent_messages_in_node_update=False,
|
| 318 |
+
activation="swish",
|
| 319 |
+
f32_aggregation=False,
|
| 320 |
+
name="mesh2grid_gnn",
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# Obtain the query radius in absolute units for the unit-sphere for the
|
| 324 |
+
# grid2mesh model, by rescaling the `radius_query_fraction_edge_length`.
|
| 325 |
+
self._query_radius = (_get_max_edge_distance(self._finest_mesh)
|
| 326 |
+
* model_config.radius_query_fraction_edge_length)
|
| 327 |
+
self._mesh2grid_edge_normalization_factor = (
|
| 328 |
+
model_config.mesh2grid_edge_normalization_factor
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
# Other initialization is delayed until the first call (`_maybe_init`)
|
| 332 |
+
# when we get some sample data so we know the lat/lon values.
|
| 333 |
+
self._initialized = False
|
| 334 |
+
|
| 335 |
+
# A "_init_mesh_properties":
|
| 336 |
+
# This one could be initialized at init but we delay it for consistency too.
|
| 337 |
+
self._num_mesh_nodes = None # num_mesh_nodes
|
| 338 |
+
self._mesh_nodes_lat = None # [num_mesh_nodes]
|
| 339 |
+
self._mesh_nodes_lon = None # [num_mesh_nodes]
|
| 340 |
+
|
| 341 |
+
# A "_init_grid_properties":
|
| 342 |
+
self._grid_lat = None # [num_lat_points]
|
| 343 |
+
self._grid_lon = None # [num_lon_points]
|
| 344 |
+
self._num_grid_nodes = None # num_lat_points * num_lon_points
|
| 345 |
+
self._grid_nodes_lat = None # [num_grid_nodes]
|
| 346 |
+
self._grid_nodes_lon = None # [num_grid_nodes]
|
| 347 |
+
|
| 348 |
+
# A "_init_{grid2mesh,processor,mesh2grid}_graph"
|
| 349 |
+
self._grid2mesh_graph_structure = None
|
| 350 |
+
self._mesh_graph_structure = None
|
| 351 |
+
self._mesh2grid_graph_structure = None
|
| 352 |
+
|
| 353 |
+
@property
|
| 354 |
+
def _finest_mesh(self):
|
| 355 |
+
return self._meshes[-1]
|
| 356 |
+
|
| 357 |
+
def __call__(self,
|
| 358 |
+
inputs: xarray.Dataset,
|
| 359 |
+
targets_template: xarray.Dataset,
|
| 360 |
+
forcings: xarray.Dataset,
|
| 361 |
+
is_training: bool = False,
|
| 362 |
+
) -> xarray.Dataset:
|
| 363 |
+
self._maybe_init(inputs)
|
| 364 |
+
|
| 365 |
+
# Convert all input data into flat vectors for each of the grid nodes.
|
| 366 |
+
# xarray (batch, time, lat, lon, level, multiple vars, forcings)
|
| 367 |
+
# -> [num_grid_nodes, batch, num_channels]
|
| 368 |
+
grid_node_features = self._inputs_to_grid_node_features(inputs, forcings)
|
| 369 |
+
|
| 370 |
+
# Transfer data for the grid to the mesh,
|
| 371 |
+
# [num_mesh_nodes, batch, latent_size], [num_grid_nodes, batch, latent_size]
|
| 372 |
+
(latent_mesh_nodes, latent_grid_nodes
|
| 373 |
+
) = self._run_grid2mesh_gnn(grid_node_features)
|
| 374 |
+
|
| 375 |
+
# Run message passing in the multimesh.
|
| 376 |
+
# [num_mesh_nodes, batch, latent_size]
|
| 377 |
+
updated_latent_mesh_nodes = self._run_mesh_gnn(latent_mesh_nodes)
|
| 378 |
+
|
| 379 |
+
# Transfer data frome the mesh to the grid.
|
| 380 |
+
# [num_grid_nodes, batch, output_size]
|
| 381 |
+
output_grid_nodes = self._run_mesh2grid_gnn(
|
| 382 |
+
updated_latent_mesh_nodes, latent_grid_nodes)
|
| 383 |
+
|
| 384 |
+
# Conver output flat vectors for the grid nodes to the format of the output.
|
| 385 |
+
# [num_grid_nodes, batch, output_size] ->
|
| 386 |
+
# xarray (batch, one time step, lat, lon, level, multiple vars)
|
| 387 |
+
return self._grid_node_outputs_to_prediction(
|
| 388 |
+
output_grid_nodes, targets_template)
|
| 389 |
+
|
| 390 |
+
def loss_and_predictions( # pytype: disable=signature-mismatch # jax-ndarray
|
| 391 |
+
self,
|
| 392 |
+
inputs: xarray.Dataset,
|
| 393 |
+
targets: xarray.Dataset,
|
| 394 |
+
forcings: xarray.Dataset,
|
| 395 |
+
) -> tuple[predictor_base.LossAndDiagnostics, xarray.Dataset]:
|
| 396 |
+
# Forward pass.
|
| 397 |
+
predictions = self(
|
| 398 |
+
inputs, targets_template=targets, forcings=forcings, is_training=True)
|
| 399 |
+
# Compute loss.
|
| 400 |
+
loss = losses.weighted_mse_per_level(
|
| 401 |
+
predictions, targets,
|
| 402 |
+
per_variable_weights={
|
| 403 |
+
# Any variables not specified here are weighted as 1.0.
|
| 404 |
+
# A single-level variable, but an important headline variable
|
| 405 |
+
# and also one which we have struggled to get good performance
|
| 406 |
+
# on at short lead times, so leaving it weighted at 1.0, equal
|
| 407 |
+
# to the multi-level variables:
|
| 408 |
+
"2m_temperature": 1.0,
|
| 409 |
+
# New single-level variables, which we don't weight too highly
|
| 410 |
+
# to avoid hurting performance on other variables.
|
| 411 |
+
"10m_u_component_of_wind": 0.1,
|
| 412 |
+
"10m_v_component_of_wind": 0.1,
|
| 413 |
+
"mean_sea_level_pressure": 0.1,
|
| 414 |
+
"total_precipitation_6hr": 0.1,
|
| 415 |
+
})
|
| 416 |
+
return loss, predictions # pytype: disable=bad-return-type # jax-ndarray
|
| 417 |
+
|
| 418 |
+
def loss( # pytype: disable=signature-mismatch # jax-ndarray
|
| 419 |
+
self,
|
| 420 |
+
inputs: xarray.Dataset,
|
| 421 |
+
targets: xarray.Dataset,
|
| 422 |
+
forcings: xarray.Dataset,
|
| 423 |
+
) -> predictor_base.LossAndDiagnostics:
|
| 424 |
+
loss, _ = self.loss_and_predictions(inputs, targets, forcings)
|
| 425 |
+
return loss # pytype: disable=bad-return-type # jax-ndarray
|
| 426 |
+
|
| 427 |
+
def _maybe_init(self, sample_inputs: xarray.Dataset):
|
| 428 |
+
"""Inits everything that has a dependency on the input coordinates."""
|
| 429 |
+
if not self._initialized:
|
| 430 |
+
self._init_mesh_properties()
|
| 431 |
+
self._init_grid_properties(
|
| 432 |
+
grid_lat=sample_inputs.lat, grid_lon=sample_inputs.lon)
|
| 433 |
+
self._grid2mesh_graph_structure = self._init_grid2mesh_graph()
|
| 434 |
+
self._mesh_graph_structure = self._init_mesh_graph()
|
| 435 |
+
self._mesh2grid_graph_structure = self._init_mesh2grid_graph()
|
| 436 |
+
|
| 437 |
+
self._initialized = True
|
| 438 |
+
|
| 439 |
+
def _init_mesh_properties(self):
|
| 440 |
+
"""Inits static properties that have to do with mesh nodes."""
|
| 441 |
+
self._num_mesh_nodes = self._finest_mesh.vertices.shape[0]
|
| 442 |
+
mesh_phi, mesh_theta = model_utils.cartesian_to_spherical(
|
| 443 |
+
self._finest_mesh.vertices[:, 0],
|
| 444 |
+
self._finest_mesh.vertices[:, 1],
|
| 445 |
+
self._finest_mesh.vertices[:, 2])
|
| 446 |
+
(
|
| 447 |
+
mesh_nodes_lat,
|
| 448 |
+
mesh_nodes_lon,
|
| 449 |
+
) = model_utils.spherical_to_lat_lon(
|
| 450 |
+
phi=mesh_phi, theta=mesh_theta)
|
| 451 |
+
# Convert to f32 to ensure the lat/lon features aren't in f64.
|
| 452 |
+
self._mesh_nodes_lat = mesh_nodes_lat.astype(np.float32)
|
| 453 |
+
self._mesh_nodes_lon = mesh_nodes_lon.astype(np.float32)
|
| 454 |
+
|
| 455 |
+
def _init_grid_properties(self, grid_lat: np.ndarray, grid_lon: np.ndarray):
|
| 456 |
+
"""Inits static properties that have to do with grid nodes."""
|
| 457 |
+
self._grid_lat = grid_lat.astype(np.float32)
|
| 458 |
+
self._grid_lon = grid_lon.astype(np.float32)
|
| 459 |
+
# Initialized the counters.
|
| 460 |
+
self._num_grid_nodes = grid_lat.shape[0] * grid_lon.shape[0]
|
| 461 |
+
|
| 462 |
+
# Initialize lat and lon for the grid.
|
| 463 |
+
grid_nodes_lon, grid_nodes_lat = np.meshgrid(grid_lon, grid_lat)
|
| 464 |
+
self._grid_nodes_lon = grid_nodes_lon.reshape([-1]).astype(np.float32)
|
| 465 |
+
self._grid_nodes_lat = grid_nodes_lat.reshape([-1]).astype(np.float32)
|
| 466 |
+
|
| 467 |
+
def _init_grid2mesh_graph(self) -> typed_graph.TypedGraph:
|
| 468 |
+
"""Build Grid2Mesh graph."""
|
| 469 |
+
|
| 470 |
+
# Create some edges according to distance between mesh and grid nodes.
|
| 471 |
+
assert self._grid_lat is not None and self._grid_lon is not None
|
| 472 |
+
(grid_indices, mesh_indices) = grid_mesh_connectivity.radius_query_indices(
|
| 473 |
+
grid_latitude=self._grid_lat,
|
| 474 |
+
grid_longitude=self._grid_lon,
|
| 475 |
+
mesh=self._finest_mesh,
|
| 476 |
+
radius=self._query_radius)
|
| 477 |
+
|
| 478 |
+
# Edges sending info from grid to mesh.
|
| 479 |
+
senders = grid_indices
|
| 480 |
+
receivers = mesh_indices
|
| 481 |
+
|
| 482 |
+
# Precompute structural node and edge features according to config options.
|
| 483 |
+
# Structural features are those that depend on the fixed values of the
|
| 484 |
+
# latitude and longitudes of the nodes.
|
| 485 |
+
(senders_node_features, receivers_node_features,
|
| 486 |
+
edge_features) = model_utils.get_bipartite_graph_spatial_features(
|
| 487 |
+
senders_node_lat=self._grid_nodes_lat,
|
| 488 |
+
senders_node_lon=self._grid_nodes_lon,
|
| 489 |
+
receivers_node_lat=self._mesh_nodes_lat,
|
| 490 |
+
receivers_node_lon=self._mesh_nodes_lon,
|
| 491 |
+
senders=senders,
|
| 492 |
+
receivers=receivers,
|
| 493 |
+
edge_normalization_factor=None,
|
| 494 |
+
**self._spatial_features_kwargs,
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
n_grid_node = np.array([self._num_grid_nodes])
|
| 498 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 499 |
+
n_edge = np.array([mesh_indices.shape[0]])
|
| 500 |
+
grid_node_set = typed_graph.NodeSet(
|
| 501 |
+
n_node=n_grid_node, features=senders_node_features)
|
| 502 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 503 |
+
n_node=n_mesh_node, features=receivers_node_features)
|
| 504 |
+
edge_set = typed_graph.EdgeSet(
|
| 505 |
+
n_edge=n_edge,
|
| 506 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 507 |
+
features=edge_features)
|
| 508 |
+
nodes = {"grid_nodes": grid_node_set, "mesh_nodes": mesh_node_set}
|
| 509 |
+
edges = {
|
| 510 |
+
typed_graph.EdgeSetKey("grid2mesh", ("grid_nodes", "mesh_nodes")):
|
| 511 |
+
edge_set
|
| 512 |
+
}
|
| 513 |
+
grid2mesh_graph = typed_graph.TypedGraph(
|
| 514 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 515 |
+
nodes=nodes,
|
| 516 |
+
edges=edges)
|
| 517 |
+
return grid2mesh_graph
|
| 518 |
+
|
| 519 |
+
def _init_mesh_graph(self) -> typed_graph.TypedGraph:
|
| 520 |
+
"""Build Mesh graph."""
|
| 521 |
+
merged_mesh = icosahedral_mesh.merge_meshes(self._meshes)
|
| 522 |
+
|
| 523 |
+
# Work simply on the mesh edges.
|
| 524 |
+
senders, receivers = icosahedral_mesh.faces_to_edges(merged_mesh.faces)
|
| 525 |
+
|
| 526 |
+
# Precompute structural node and edge features according to config options.
|
| 527 |
+
# Structural features are those that depend on the fixed values of the
|
| 528 |
+
# latitude and longitudes of the nodes.
|
| 529 |
+
assert self._mesh_nodes_lat is not None and self._mesh_nodes_lon is not None
|
| 530 |
+
node_features, edge_features = model_utils.get_graph_spatial_features(
|
| 531 |
+
node_lat=self._mesh_nodes_lat,
|
| 532 |
+
node_lon=self._mesh_nodes_lon,
|
| 533 |
+
senders=senders,
|
| 534 |
+
receivers=receivers,
|
| 535 |
+
**self._spatial_features_kwargs,
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 539 |
+
n_edge = np.array([senders.shape[0]])
|
| 540 |
+
assert n_mesh_node == len(node_features)
|
| 541 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 542 |
+
n_node=n_mesh_node, features=node_features)
|
| 543 |
+
edge_set = typed_graph.EdgeSet(
|
| 544 |
+
n_edge=n_edge,
|
| 545 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 546 |
+
features=edge_features)
|
| 547 |
+
nodes = {"mesh_nodes": mesh_node_set}
|
| 548 |
+
edges = {
|
| 549 |
+
typed_graph.EdgeSetKey("mesh", ("mesh_nodes", "mesh_nodes")): edge_set
|
| 550 |
+
}
|
| 551 |
+
mesh_graph = typed_graph.TypedGraph(
|
| 552 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 553 |
+
nodes=nodes,
|
| 554 |
+
edges=edges)
|
| 555 |
+
|
| 556 |
+
return mesh_graph
|
| 557 |
+
|
| 558 |
+
def _init_mesh2grid_graph(self) -> typed_graph.TypedGraph:
|
| 559 |
+
"""Build Mesh2Grid graph."""
|
| 560 |
+
|
| 561 |
+
# Create some edges according to how the grid nodes are contained by
|
| 562 |
+
# mesh triangles.
|
| 563 |
+
(grid_indices,
|
| 564 |
+
mesh_indices) = grid_mesh_connectivity.in_mesh_triangle_indices(
|
| 565 |
+
grid_latitude=self._grid_lat,
|
| 566 |
+
grid_longitude=self._grid_lon,
|
| 567 |
+
mesh=self._finest_mesh)
|
| 568 |
+
|
| 569 |
+
# Edges sending info from mesh to grid.
|
| 570 |
+
senders = mesh_indices
|
| 571 |
+
receivers = grid_indices
|
| 572 |
+
|
| 573 |
+
# Precompute structural node and edge features according to config options.
|
| 574 |
+
assert self._mesh_nodes_lat is not None and self._mesh_nodes_lon is not None
|
| 575 |
+
(senders_node_features, receivers_node_features,
|
| 576 |
+
edge_features) = model_utils.get_bipartite_graph_spatial_features(
|
| 577 |
+
senders_node_lat=self._mesh_nodes_lat,
|
| 578 |
+
senders_node_lon=self._mesh_nodes_lon,
|
| 579 |
+
receivers_node_lat=self._grid_nodes_lat,
|
| 580 |
+
receivers_node_lon=self._grid_nodes_lon,
|
| 581 |
+
senders=senders,
|
| 582 |
+
receivers=receivers,
|
| 583 |
+
edge_normalization_factor=self._mesh2grid_edge_normalization_factor,
|
| 584 |
+
**self._spatial_features_kwargs,
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
n_grid_node = np.array([self._num_grid_nodes])
|
| 588 |
+
n_mesh_node = np.array([self._num_mesh_nodes])
|
| 589 |
+
n_edge = np.array([senders.shape[0]])
|
| 590 |
+
grid_node_set = typed_graph.NodeSet(
|
| 591 |
+
n_node=n_grid_node, features=receivers_node_features)
|
| 592 |
+
mesh_node_set = typed_graph.NodeSet(
|
| 593 |
+
n_node=n_mesh_node, features=senders_node_features)
|
| 594 |
+
edge_set = typed_graph.EdgeSet(
|
| 595 |
+
n_edge=n_edge,
|
| 596 |
+
indices=typed_graph.EdgesIndices(senders=senders, receivers=receivers),
|
| 597 |
+
features=edge_features)
|
| 598 |
+
nodes = {"grid_nodes": grid_node_set, "mesh_nodes": mesh_node_set}
|
| 599 |
+
edges = {
|
| 600 |
+
typed_graph.EdgeSetKey("mesh2grid", ("mesh_nodes", "grid_nodes")):
|
| 601 |
+
edge_set
|
| 602 |
+
}
|
| 603 |
+
mesh2grid_graph = typed_graph.TypedGraph(
|
| 604 |
+
context=typed_graph.Context(n_graph=np.array([1]), features=()),
|
| 605 |
+
nodes=nodes,
|
| 606 |
+
edges=edges)
|
| 607 |
+
return mesh2grid_graph
|
| 608 |
+
|
| 609 |
+
def _run_grid2mesh_gnn(self, grid_node_features: chex.Array,
|
| 610 |
+
) -> tuple[chex.Array, chex.Array]:
|
| 611 |
+
"""Runs the grid2mesh_gnn, extracting latent mesh and grid nodes."""
|
| 612 |
+
|
| 613 |
+
# Concatenate node structural features with input features.
|
| 614 |
+
batch_size = grid_node_features.shape[1]
|
| 615 |
+
|
| 616 |
+
grid2mesh_graph = self._grid2mesh_graph_structure
|
| 617 |
+
assert grid2mesh_graph is not None
|
| 618 |
+
grid_nodes = grid2mesh_graph.nodes["grid_nodes"]
|
| 619 |
+
mesh_nodes = grid2mesh_graph.nodes["mesh_nodes"]
|
| 620 |
+
new_grid_nodes = grid_nodes._replace(
|
| 621 |
+
features=jnp.concatenate([
|
| 622 |
+
grid_node_features,
|
| 623 |
+
_add_batch_second_axis(
|
| 624 |
+
grid_nodes.features.astype(grid_node_features.dtype),
|
| 625 |
+
batch_size)
|
| 626 |
+
],
|
| 627 |
+
axis=-1))
|
| 628 |
+
|
| 629 |
+
# To make sure capacity of the embedded is identical for the grid nodes and
|
| 630 |
+
# the mesh nodes, we also append some dummy zero input features for the
|
| 631 |
+
# mesh nodes.
|
| 632 |
+
dummy_mesh_node_features = jnp.zeros(
|
| 633 |
+
(self._num_mesh_nodes,) + grid_node_features.shape[1:],
|
| 634 |
+
dtype=grid_node_features.dtype)
|
| 635 |
+
new_mesh_nodes = mesh_nodes._replace(
|
| 636 |
+
features=jnp.concatenate([
|
| 637 |
+
dummy_mesh_node_features,
|
| 638 |
+
_add_batch_second_axis(
|
| 639 |
+
mesh_nodes.features.astype(dummy_mesh_node_features.dtype),
|
| 640 |
+
batch_size)
|
| 641 |
+
],
|
| 642 |
+
axis=-1))
|
| 643 |
+
|
| 644 |
+
# Broadcast edge structural features to the required batch size.
|
| 645 |
+
grid2mesh_edges_key = grid2mesh_graph.edge_key_by_name("grid2mesh")
|
| 646 |
+
edges = grid2mesh_graph.edges[grid2mesh_edges_key]
|
| 647 |
+
|
| 648 |
+
new_edges = edges._replace(
|
| 649 |
+
features=_add_batch_second_axis(
|
| 650 |
+
edges.features.astype(dummy_mesh_node_features.dtype), batch_size))
|
| 651 |
+
|
| 652 |
+
input_graph = self._grid2mesh_graph_structure._replace(
|
| 653 |
+
edges={grid2mesh_edges_key: new_edges},
|
| 654 |
+
nodes={
|
| 655 |
+
"grid_nodes": new_grid_nodes,
|
| 656 |
+
"mesh_nodes": new_mesh_nodes
|
| 657 |
+
})
|
| 658 |
+
|
| 659 |
+
# Run the GNN.
|
| 660 |
+
grid2mesh_out = self._grid2mesh_gnn(input_graph)
|
| 661 |
+
latent_mesh_nodes = grid2mesh_out.nodes["mesh_nodes"].features
|
| 662 |
+
latent_grid_nodes = grid2mesh_out.nodes["grid_nodes"].features
|
| 663 |
+
return latent_mesh_nodes, latent_grid_nodes
|
| 664 |
+
|
| 665 |
+
def _run_mesh_gnn(self, latent_mesh_nodes: chex.Array) -> chex.Array:
|
| 666 |
+
"""Runs the mesh_gnn, extracting updated latent mesh nodes."""
|
| 667 |
+
|
| 668 |
+
# Add the structural edge features of this graph. Note we don't need
|
| 669 |
+
# to add the structural node features, because these are already part of
|
| 670 |
+
# the latent state, via the original Grid2Mesh gnn, however, we need
|
| 671 |
+
# the edge ones, because it is the first time we are seeing this particular
|
| 672 |
+
# set of edges.
|
| 673 |
+
batch_size = latent_mesh_nodes.shape[1]
|
| 674 |
+
|
| 675 |
+
mesh_graph = self._mesh_graph_structure
|
| 676 |
+
assert mesh_graph is not None
|
| 677 |
+
mesh_edges_key = mesh_graph.edge_key_by_name("mesh")
|
| 678 |
+
edges = mesh_graph.edges[mesh_edges_key]
|
| 679 |
+
|
| 680 |
+
# We are assuming here that the mesh gnn uses a single set of edge keys
|
| 681 |
+
# named "mesh" for the edges and that it uses a single set of nodes named
|
| 682 |
+
# "mesh_nodes"
|
| 683 |
+
msg = ("The setup currently requires to only have one kind of edge in the"
|
| 684 |
+
" mesh GNN.")
|
| 685 |
+
assert len(mesh_graph.edges) == 1, msg
|
| 686 |
+
|
| 687 |
+
new_edges = edges._replace(
|
| 688 |
+
features=_add_batch_second_axis(
|
| 689 |
+
edges.features.astype(latent_mesh_nodes.dtype), batch_size))
|
| 690 |
+
|
| 691 |
+
nodes = mesh_graph.nodes["mesh_nodes"]
|
| 692 |
+
nodes = nodes._replace(features=latent_mesh_nodes)
|
| 693 |
+
|
| 694 |
+
input_graph = mesh_graph._replace(
|
| 695 |
+
edges={mesh_edges_key: new_edges}, nodes={"mesh_nodes": nodes})
|
| 696 |
+
|
| 697 |
+
# Run the GNN.
|
| 698 |
+
return self._mesh_gnn(input_graph).nodes["mesh_nodes"].features
|
| 699 |
+
|
| 700 |
+
def _run_mesh2grid_gnn(self,
|
| 701 |
+
updated_latent_mesh_nodes: chex.Array,
|
| 702 |
+
latent_grid_nodes: chex.Array,
|
| 703 |
+
) -> chex.Array:
|
| 704 |
+
"""Runs the mesh2grid_gnn, extracting the output grid nodes."""
|
| 705 |
+
|
| 706 |
+
# Add the structural edge features of this graph. Note we don't need
|
| 707 |
+
# to add the structural node features, because these are already part of
|
| 708 |
+
# the latent state, via the original Grid2Mesh gnn, however, we need
|
| 709 |
+
# the edge ones, because it is the first time we are seeing this particular
|
| 710 |
+
# set of edges.
|
| 711 |
+
batch_size = updated_latent_mesh_nodes.shape[1]
|
| 712 |
+
|
| 713 |
+
mesh2grid_graph = self._mesh2grid_graph_structure
|
| 714 |
+
assert mesh2grid_graph is not None
|
| 715 |
+
mesh_nodes = mesh2grid_graph.nodes["mesh_nodes"]
|
| 716 |
+
grid_nodes = mesh2grid_graph.nodes["grid_nodes"]
|
| 717 |
+
new_mesh_nodes = mesh_nodes._replace(features=updated_latent_mesh_nodes)
|
| 718 |
+
new_grid_nodes = grid_nodes._replace(features=latent_grid_nodes)
|
| 719 |
+
mesh2grid_key = mesh2grid_graph.edge_key_by_name("mesh2grid")
|
| 720 |
+
edges = mesh2grid_graph.edges[mesh2grid_key]
|
| 721 |
+
|
| 722 |
+
new_edges = edges._replace(
|
| 723 |
+
features=_add_batch_second_axis(
|
| 724 |
+
edges.features.astype(latent_grid_nodes.dtype), batch_size))
|
| 725 |
+
|
| 726 |
+
input_graph = mesh2grid_graph._replace(
|
| 727 |
+
edges={mesh2grid_key: new_edges},
|
| 728 |
+
nodes={
|
| 729 |
+
"mesh_nodes": new_mesh_nodes,
|
| 730 |
+
"grid_nodes": new_grid_nodes
|
| 731 |
+
})
|
| 732 |
+
|
| 733 |
+
# Run the GNN.
|
| 734 |
+
output_graph = self._mesh2grid_gnn(input_graph)
|
| 735 |
+
output_grid_nodes = output_graph.nodes["grid_nodes"].features
|
| 736 |
+
|
| 737 |
+
return output_grid_nodes
|
| 738 |
+
|
| 739 |
+
def _inputs_to_grid_node_features(
|
| 740 |
+
self,
|
| 741 |
+
inputs: xarray.Dataset,
|
| 742 |
+
forcings: xarray.Dataset,
|
| 743 |
+
) -> chex.Array:
|
| 744 |
+
"""xarrays -> [num_grid_nodes, batch, num_channels]."""
|
| 745 |
+
|
| 746 |
+
# xarray `Dataset` (batch, time, lat, lon, level, multiple vars)
|
| 747 |
+
# to xarray `DataArray` (batch, lat, lon, channels)
|
| 748 |
+
stacked_inputs = model_utils.dataset_to_stacked(inputs)
|
| 749 |
+
stacked_forcings = model_utils.dataset_to_stacked(forcings)
|
| 750 |
+
stacked_inputs = xarray.concat(
|
| 751 |
+
[stacked_inputs, stacked_forcings], dim="channels")
|
| 752 |
+
|
| 753 |
+
# xarray `DataArray` (batch, lat, lon, channels)
|
| 754 |
+
# to single numpy array with shape [lat_lon_node, batch, channels]
|
| 755 |
+
grid_xarray_lat_lon_leading = model_utils.lat_lon_to_leading_axes(
|
| 756 |
+
stacked_inputs)
|
| 757 |
+
return xarray_jax.unwrap(grid_xarray_lat_lon_leading.data).reshape(
|
| 758 |
+
(-1,) + grid_xarray_lat_lon_leading.data.shape[2:])
|
| 759 |
+
|
| 760 |
+
def _grid_node_outputs_to_prediction(
|
| 761 |
+
self,
|
| 762 |
+
grid_node_outputs: chex.Array,
|
| 763 |
+
targets_template: xarray.Dataset,
|
| 764 |
+
) -> xarray.Dataset:
|
| 765 |
+
"""[num_grid_nodes, batch, num_outputs] -> xarray."""
|
| 766 |
+
|
| 767 |
+
# numpy array with shape [lat_lon_node, batch, channels]
|
| 768 |
+
# to xarray `DataArray` (batch, lat, lon, channels)
|
| 769 |
+
assert self._grid_lat is not None and self._grid_lon is not None
|
| 770 |
+
grid_shape = (self._grid_lat.shape[0], self._grid_lon.shape[0])
|
| 771 |
+
grid_outputs_lat_lon_leading = grid_node_outputs.reshape(
|
| 772 |
+
grid_shape + grid_node_outputs.shape[1:])
|
| 773 |
+
dims = ("lat", "lon", "batch", "channels")
|
| 774 |
+
grid_xarray_lat_lon_leading = xarray_jax.DataArray(
|
| 775 |
+
data=grid_outputs_lat_lon_leading,
|
| 776 |
+
dims=dims)
|
| 777 |
+
grid_xarray = model_utils.restore_leading_axes(grid_xarray_lat_lon_leading)
|
| 778 |
+
|
| 779 |
+
# xarray `DataArray` (batch, lat, lon, channels)
|
| 780 |
+
# to xarray `Dataset` (batch, one time step, lat, lon, level, multiple vars)
|
| 781 |
+
return model_utils.stacked_to_dataset(
|
| 782 |
+
grid_xarray.variable, targets_template)
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def _add_batch_second_axis(data, batch_size):
|
| 786 |
+
# data [leading_dim, trailing_dim]
|
| 787 |
+
assert data.ndim == 2
|
| 788 |
+
ones = jnp.ones([batch_size, 1], dtype=data.dtype)
|
| 789 |
+
return data[:, None] * ones # [leading_dim, batch, trailing_dim]
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
def _get_max_edge_distance(mesh):
|
| 793 |
+
senders, receivers = icosahedral_mesh.faces_to_edges(mesh.faces)
|
| 794 |
+
edge_distances = np.linalg.norm(
|
| 795 |
+
mesh.vertices[senders] - mesh.vertices[receivers], axis=-1)
|
| 796 |
+
return edge_distances.max()
|
model/graphcast/grid_mesh_connectivity.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Tools for converting from regular grids on a sphere, to triangular meshes."""
|
| 15 |
+
|
| 16 |
+
from . import icosahedral_mesh
|
| 17 |
+
import numpy as np
|
| 18 |
+
import scipy
|
| 19 |
+
import trimesh
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _grid_lat_lon_to_coordinates(
|
| 23 |
+
grid_latitude: np.ndarray, grid_longitude: np.ndarray) -> np.ndarray:
|
| 24 |
+
"""Lat [num_lat] lon [num_lon] to 3d coordinates [num_lat, num_lon, 3]."""
|
| 25 |
+
# Convert to spherical coordinates phi and theta defined in the grid.
|
| 26 |
+
# Each [num_latitude_points, num_longitude_points]
|
| 27 |
+
phi_grid, theta_grid = np.meshgrid(
|
| 28 |
+
np.deg2rad(grid_longitude),
|
| 29 |
+
np.deg2rad(90 - grid_latitude))
|
| 30 |
+
|
| 31 |
+
# [num_latitude_points, num_longitude_points, 3]
|
| 32 |
+
# Note this assumes unit radius, since for now we model the earth as a
|
| 33 |
+
# sphere of unit radius, and keep any vertical dimension as a regular grid.
|
| 34 |
+
return np.stack(
|
| 35 |
+
[np.cos(phi_grid)*np.sin(theta_grid),
|
| 36 |
+
np.sin(phi_grid)*np.sin(theta_grid),
|
| 37 |
+
np.cos(theta_grid)], axis=-1)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def radius_query_indices(
|
| 41 |
+
*,
|
| 42 |
+
grid_latitude: np.ndarray,
|
| 43 |
+
grid_longitude: np.ndarray,
|
| 44 |
+
mesh: icosahedral_mesh.TriangularMesh,
|
| 45 |
+
radius: float) -> tuple[np.ndarray, np.ndarray]:
|
| 46 |
+
"""Returns mesh-grid edge indices for radius query.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
grid_latitude: Latitude values for the grid [num_lat_points]
|
| 50 |
+
grid_longitude: Longitude values for the grid [num_lon_points]
|
| 51 |
+
mesh: Mesh object.
|
| 52 |
+
radius: Radius of connectivity in R3. for a sphere of unit radius.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
tuple with `grid_indices` and `mesh_indices` indicating edges between the
|
| 56 |
+
grid and the mesh such that the distances in a straight line (not geodesic)
|
| 57 |
+
are smaller than or equal to `radius`.
|
| 58 |
+
* grid_indices: Indices of shape [num_edges], that index into a
|
| 59 |
+
[num_lat_points, num_lon_points] grid, after flattening the leading axes.
|
| 60 |
+
* mesh_indices: Indices of shape [num_edges], that index into mesh.vertices.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
# [num_grid_points=num_lat_points * num_lon_points, 3]
|
| 64 |
+
grid_positions = _grid_lat_lon_to_coordinates(
|
| 65 |
+
grid_latitude, grid_longitude).reshape([-1, 3])
|
| 66 |
+
|
| 67 |
+
# [num_mesh_points, 3]
|
| 68 |
+
mesh_positions = mesh.vertices
|
| 69 |
+
kd_tree = scipy.spatial.cKDTree(mesh_positions)
|
| 70 |
+
|
| 71 |
+
# [num_grid_points, num_mesh_points_per_grid_point]
|
| 72 |
+
# Note `num_mesh_points_per_grid_point` is not constant, so this is a list
|
| 73 |
+
# of arrays, rather than a 2d array.
|
| 74 |
+
query_indices = kd_tree.query_ball_point(x=grid_positions, r=radius)
|
| 75 |
+
|
| 76 |
+
grid_edge_indices = []
|
| 77 |
+
mesh_edge_indices = []
|
| 78 |
+
for grid_index, mesh_neighbors in enumerate(query_indices):
|
| 79 |
+
grid_edge_indices.append(np.repeat(grid_index, len(mesh_neighbors)))
|
| 80 |
+
mesh_edge_indices.append(mesh_neighbors)
|
| 81 |
+
|
| 82 |
+
# [num_edges]
|
| 83 |
+
grid_edge_indices = np.concatenate(grid_edge_indices, axis=0).astype(int)
|
| 84 |
+
mesh_edge_indices = np.concatenate(mesh_edge_indices, axis=0).astype(int)
|
| 85 |
+
|
| 86 |
+
return grid_edge_indices, mesh_edge_indices
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def in_mesh_triangle_indices(
|
| 90 |
+
*,
|
| 91 |
+
grid_latitude: np.ndarray,
|
| 92 |
+
grid_longitude: np.ndarray,
|
| 93 |
+
mesh: icosahedral_mesh.TriangularMesh) -> tuple[np.ndarray, np.ndarray]:
|
| 94 |
+
"""Returns mesh-grid edge indices for grid points contained in mesh triangles.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
grid_latitude: Latitude values for the grid [num_lat_points]
|
| 98 |
+
grid_longitude: Longitude values for the grid [num_lon_points]
|
| 99 |
+
mesh: Mesh object.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
tuple with `grid_indices` and `mesh_indices` indicating edges between the
|
| 103 |
+
grid and the mesh vertices of the triangle that contain each grid point.
|
| 104 |
+
The number of edges is always num_lat_points * num_lon_points * 3
|
| 105 |
+
* grid_indices: Indices of shape [num_edges], that index into a
|
| 106 |
+
[num_lat_points, num_lon_points] grid, after flattening the leading axes.
|
| 107 |
+
* mesh_indices: Indices of shape [num_edges], that index into mesh.vertices.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
# [num_grid_points=num_lat_points * num_lon_points, 3]
|
| 111 |
+
grid_positions = _grid_lat_lon_to_coordinates(
|
| 112 |
+
grid_latitude, grid_longitude).reshape([-1, 3])
|
| 113 |
+
|
| 114 |
+
mesh_trimesh = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces)
|
| 115 |
+
|
| 116 |
+
# [num_grid_points] with mesh face indices for each grid point.
|
| 117 |
+
_, _, query_face_indices = trimesh.proximity.closest_point(
|
| 118 |
+
mesh_trimesh, grid_positions)
|
| 119 |
+
|
| 120 |
+
# [num_grid_points, 3] with mesh node indices for each grid point.
|
| 121 |
+
mesh_edge_indices = mesh.faces[query_face_indices]
|
| 122 |
+
|
| 123 |
+
# [num_grid_points, 3] with grid node indices, where every row simply contains
|
| 124 |
+
# the row (grid_point) index.
|
| 125 |
+
grid_indices = np.arange(grid_positions.shape[0])
|
| 126 |
+
grid_edge_indices = np.tile(grid_indices.reshape([-1, 1]), [1, 3])
|
| 127 |
+
|
| 128 |
+
# Flatten to get a regular list.
|
| 129 |
+
# [num_edges=num_grid_points*3]
|
| 130 |
+
mesh_edge_indices = mesh_edge_indices.reshape([-1])
|
| 131 |
+
grid_edge_indices = grid_edge_indices.reshape([-1])
|
| 132 |
+
|
| 133 |
+
return grid_edge_indices, mesh_edge_indices
|
model/graphcast/icosahedral_mesh.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utils for creating icosahedral meshes."""
|
| 15 |
+
|
| 16 |
+
import itertools
|
| 17 |
+
from typing import List, NamedTuple, Sequence, Tuple
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
from scipy.spatial import transform
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TriangularMesh(NamedTuple):
|
| 24 |
+
"""Data structure for triangular meshes.
|
| 25 |
+
|
| 26 |
+
Attributes:
|
| 27 |
+
vertices: spatial positions of the vertices of the mesh of shape
|
| 28 |
+
[num_vertices, num_dims].
|
| 29 |
+
faces: triangular faces of the mesh of shape [num_faces, 3]. Contains
|
| 30 |
+
integer indices into `vertices`.
|
| 31 |
+
|
| 32 |
+
"""
|
| 33 |
+
vertices: np.ndarray
|
| 34 |
+
faces: np.ndarray
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def merge_meshes(
|
| 38 |
+
mesh_list: Sequence[TriangularMesh]) -> TriangularMesh:
|
| 39 |
+
"""Merges all meshes into one. Assumes the last mesh is the finest.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
mesh_list: Sequence of meshes, from coarse to fine refinement levels. The
|
| 43 |
+
vertices and faces may contain those from preceding, coarser levels.
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
`TriangularMesh` for which the vertices correspond to the highest
|
| 47 |
+
resolution mesh in the hierarchy, and the faces are the join set of the
|
| 48 |
+
faces at all levels of the hierarchy.
|
| 49 |
+
"""
|
| 50 |
+
for mesh_i, mesh_ip1 in itertools.pairwise(mesh_list):
|
| 51 |
+
num_nodes_mesh_i = mesh_i.vertices.shape[0]
|
| 52 |
+
assert np.allclose(mesh_i.vertices, mesh_ip1.vertices[:num_nodes_mesh_i])
|
| 53 |
+
|
| 54 |
+
return TriangularMesh(
|
| 55 |
+
vertices=mesh_list[-1].vertices,
|
| 56 |
+
faces=np.concatenate([mesh.faces for mesh in mesh_list], axis=0))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_hierarchy_of_triangular_meshes_for_sphere(
|
| 60 |
+
splits: int) -> List[TriangularMesh]:
|
| 61 |
+
"""Returns a sequence of meshes, each with triangularization sphere.
|
| 62 |
+
|
| 63 |
+
Starting with a regular icosahedron (12 vertices, 20 faces, 30 edges) with
|
| 64 |
+
circumscribed unit sphere. Then, each triangular face is iteratively
|
| 65 |
+
subdivided into 4 triangular faces `splits` times. The new vertices are then
|
| 66 |
+
projected back onto the unit sphere. All resulting meshes are returned in a
|
| 67 |
+
list, from lowest to highest resolution.
|
| 68 |
+
|
| 69 |
+
The vertices in each face are specified in counter-clockwise order as
|
| 70 |
+
observed from the outside the icosahedron.
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
splits: How many times to split each triangle.
|
| 74 |
+
Returns:
|
| 75 |
+
Sequence of `TriangularMesh`s of length `splits + 1` each with:
|
| 76 |
+
|
| 77 |
+
vertices: [num_vertices, 3] vertex positions in 3D, all with unit norm.
|
| 78 |
+
faces: [num_faces, 3] with triangular faces joining sets of 3 vertices.
|
| 79 |
+
Each row contains three indices into the vertices array, indicating
|
| 80 |
+
the vertices adjacent to the face. Always with positive orientation
|
| 81 |
+
(counterclock-wise when looking from the outside).
|
| 82 |
+
"""
|
| 83 |
+
current_mesh = get_icosahedron()
|
| 84 |
+
output_meshes = [current_mesh]
|
| 85 |
+
for _ in range(splits):
|
| 86 |
+
current_mesh = _two_split_unit_sphere_triangle_faces(current_mesh)
|
| 87 |
+
output_meshes.append(current_mesh)
|
| 88 |
+
return output_meshes
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def get_icosahedron() -> TriangularMesh:
|
| 92 |
+
"""Returns a regular icosahedral mesh with circumscribed unit sphere.
|
| 93 |
+
|
| 94 |
+
See https://en.wikipedia.org/wiki/Regular_icosahedron#Cartesian_coordinates
|
| 95 |
+
for details on the construction of the regular icosahedron.
|
| 96 |
+
|
| 97 |
+
The vertices in each face are specified in counter-clockwise order as observed
|
| 98 |
+
from the outside of the icosahedron.
|
| 99 |
+
|
| 100 |
+
Returns:
|
| 101 |
+
TriangularMesh with:
|
| 102 |
+
|
| 103 |
+
vertices: [num_vertices=12, 3] vertex positions in 3D, all with unit norm.
|
| 104 |
+
faces: [num_faces=20, 3] with triangular faces joining sets of 3 vertices.
|
| 105 |
+
Each row contains three indices into the vertices array, indicating
|
| 106 |
+
the vertices adjacent to the face. Always with positive orientation (
|
| 107 |
+
counterclock-wise when looking from the outside).
|
| 108 |
+
|
| 109 |
+
"""
|
| 110 |
+
phi = (1 + np.sqrt(5)) / 2
|
| 111 |
+
vertices = []
|
| 112 |
+
for c1 in [1., -1.]:
|
| 113 |
+
for c2 in [phi, -phi]:
|
| 114 |
+
vertices.append((c1, c2, 0.))
|
| 115 |
+
vertices.append((0., c1, c2))
|
| 116 |
+
vertices.append((c2, 0., c1))
|
| 117 |
+
|
| 118 |
+
vertices = np.array(vertices, dtype=np.float32)
|
| 119 |
+
vertices /= np.linalg.norm([1., phi])
|
| 120 |
+
|
| 121 |
+
# I did this manually, checking the orientation one by one.
|
| 122 |
+
faces = [(0, 1, 2),
|
| 123 |
+
(0, 6, 1),
|
| 124 |
+
(8, 0, 2),
|
| 125 |
+
(8, 4, 0),
|
| 126 |
+
(3, 8, 2),
|
| 127 |
+
(3, 2, 7),
|
| 128 |
+
(7, 2, 1),
|
| 129 |
+
(0, 4, 6),
|
| 130 |
+
(4, 11, 6),
|
| 131 |
+
(6, 11, 5),
|
| 132 |
+
(1, 5, 7),
|
| 133 |
+
(4, 10, 11),
|
| 134 |
+
(4, 8, 10),
|
| 135 |
+
(10, 8, 3),
|
| 136 |
+
(10, 3, 9),
|
| 137 |
+
(11, 10, 9),
|
| 138 |
+
(11, 9, 5),
|
| 139 |
+
(5, 9, 7),
|
| 140 |
+
(9, 3, 7),
|
| 141 |
+
(1, 6, 5),
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
# By default the top is an aris parallel to the Y axis.
|
| 145 |
+
# Need to rotate around the y axis by half the supplementary to the
|
| 146 |
+
# angle between faces divided by two to get the desired orientation.
|
| 147 |
+
# /O\ (top arist)
|
| 148 |
+
# / \ Z
|
| 149 |
+
# (adjacent face)/ \ (adjacent face) ^
|
| 150 |
+
# / angle_between_faces \ |
|
| 151 |
+
# / \ |
|
| 152 |
+
# / \ YO-----> X
|
| 153 |
+
# This results in:
|
| 154 |
+
# (adjacent faceis now top plane)
|
| 155 |
+
# ----------------------O\ (top arist)
|
| 156 |
+
# \
|
| 157 |
+
# \
|
| 158 |
+
# \ (adjacent face)
|
| 159 |
+
# \
|
| 160 |
+
# \
|
| 161 |
+
# \
|
| 162 |
+
|
| 163 |
+
angle_between_faces = 2 * np.arcsin(phi / np.sqrt(3))
|
| 164 |
+
rotation_angle = (np.pi - angle_between_faces) / 2
|
| 165 |
+
rotation = transform.Rotation.from_euler(seq="y", angles=rotation_angle)
|
| 166 |
+
rotation_matrix = rotation.as_matrix()
|
| 167 |
+
vertices = np.dot(vertices, rotation_matrix)
|
| 168 |
+
|
| 169 |
+
return TriangularMesh(vertices=vertices.astype(np.float32),
|
| 170 |
+
faces=np.array(faces, dtype=np.int32))
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _two_split_unit_sphere_triangle_faces(
|
| 174 |
+
triangular_mesh: TriangularMesh) -> TriangularMesh:
|
| 175 |
+
"""Splits each triangular face into 4 triangles keeping the orientation."""
|
| 176 |
+
|
| 177 |
+
# Every time we split a triangle into 4 we will be adding 3 extra vertices,
|
| 178 |
+
# located at the edge centres.
|
| 179 |
+
# This class handles the positioning of the new vertices, and avoids creating
|
| 180 |
+
# duplicates.
|
| 181 |
+
new_vertices_builder = _ChildVerticesBuilder(triangular_mesh.vertices)
|
| 182 |
+
|
| 183 |
+
new_faces = []
|
| 184 |
+
for ind1, ind2, ind3 in triangular_mesh.faces:
|
| 185 |
+
# Transform each triangular face into 4 triangles,
|
| 186 |
+
# preserving the orientation.
|
| 187 |
+
# ind3
|
| 188 |
+
# / \
|
| 189 |
+
# / \
|
| 190 |
+
# / #3 \
|
| 191 |
+
# / \
|
| 192 |
+
# ind31 -------------- ind23
|
| 193 |
+
# / \ / \
|
| 194 |
+
# / \ #4 / \
|
| 195 |
+
# / #1 \ / #2 \
|
| 196 |
+
# / \ / \
|
| 197 |
+
# ind1 ------------ ind12 ------------ ind2
|
| 198 |
+
ind12 = new_vertices_builder.get_new_child_vertex_index((ind1, ind2))
|
| 199 |
+
ind23 = new_vertices_builder.get_new_child_vertex_index((ind2, ind3))
|
| 200 |
+
ind31 = new_vertices_builder.get_new_child_vertex_index((ind3, ind1))
|
| 201 |
+
# Note how each of the 4 triangular new faces specifies the order of the
|
| 202 |
+
# vertices to preserve the orientation of the original face. As the input
|
| 203 |
+
# face should always be counter-clockwise as specified in the diagram,
|
| 204 |
+
# this means child faces should also be counter-clockwise.
|
| 205 |
+
new_faces.extend([[ind1, ind12, ind31], # 1
|
| 206 |
+
[ind12, ind2, ind23], # 2
|
| 207 |
+
[ind31, ind23, ind3], # 3
|
| 208 |
+
[ind12, ind23, ind31], # 4
|
| 209 |
+
])
|
| 210 |
+
return TriangularMesh(vertices=new_vertices_builder.get_all_vertices(),
|
| 211 |
+
faces=np.array(new_faces, dtype=np.int32))
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class _ChildVerticesBuilder(object):
|
| 215 |
+
"""Bookkeeping of new child vertices added to an existing set of vertices."""
|
| 216 |
+
|
| 217 |
+
def __init__(self, parent_vertices):
|
| 218 |
+
|
| 219 |
+
# Because the same new vertex will be required when splitting adjacent
|
| 220 |
+
# triangles (which share an edge) we keep them in a hash table indexed by
|
| 221 |
+
# sorted indices of the vertices adjacent to the edge, to avoid creating
|
| 222 |
+
# duplicated child vertices.
|
| 223 |
+
self._child_vertices_index_mapping = {}
|
| 224 |
+
self._parent_vertices = parent_vertices
|
| 225 |
+
# We start with all previous vertices.
|
| 226 |
+
self._all_vertices_list = list(parent_vertices)
|
| 227 |
+
|
| 228 |
+
def _get_child_vertex_key(self, parent_vertex_indices):
|
| 229 |
+
return tuple(sorted(parent_vertex_indices))
|
| 230 |
+
|
| 231 |
+
def _create_child_vertex(self, parent_vertex_indices):
|
| 232 |
+
"""Creates a new vertex."""
|
| 233 |
+
# Position for new vertex is the middle point, between the parent points,
|
| 234 |
+
# projected to unit sphere.
|
| 235 |
+
child_vertex_position = self._parent_vertices[
|
| 236 |
+
list(parent_vertex_indices)].mean(0)
|
| 237 |
+
child_vertex_position /= np.linalg.norm(child_vertex_position)
|
| 238 |
+
|
| 239 |
+
# Add the vertex to the output list. The index for this new vertex will
|
| 240 |
+
# match the length of the list before adding it.
|
| 241 |
+
child_vertex_key = self._get_child_vertex_key(parent_vertex_indices)
|
| 242 |
+
self._child_vertices_index_mapping[child_vertex_key] = len(
|
| 243 |
+
self._all_vertices_list)
|
| 244 |
+
self._all_vertices_list.append(child_vertex_position)
|
| 245 |
+
|
| 246 |
+
def get_new_child_vertex_index(self, parent_vertex_indices):
|
| 247 |
+
"""Returns index for a child vertex, creating it if necessary."""
|
| 248 |
+
# Get the key to see if we already have a new vertex in the middle.
|
| 249 |
+
child_vertex_key = self._get_child_vertex_key(parent_vertex_indices)
|
| 250 |
+
if child_vertex_key not in self._child_vertices_index_mapping:
|
| 251 |
+
self._create_child_vertex(parent_vertex_indices)
|
| 252 |
+
return self._child_vertices_index_mapping[child_vertex_key]
|
| 253 |
+
|
| 254 |
+
def get_all_vertices(self):
|
| 255 |
+
"""Returns an array with old vertices."""
|
| 256 |
+
return np.array(self._all_vertices_list)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def faces_to_edges(faces: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
| 260 |
+
"""Transforms polygonal faces to sender and receiver indices.
|
| 261 |
+
|
| 262 |
+
It does so by transforming every face into N_i edges. Such if the triangular
|
| 263 |
+
face has indices [0, 1, 2], three edges are added 0->1, 1->2, and 2->0.
|
| 264 |
+
|
| 265 |
+
If all faces have consistent orientation, and the surface represented by the
|
| 266 |
+
faces is closed, then every edge in a polygon with a certain orientation
|
| 267 |
+
is also part of another polygon with the opposite orientation. In this
|
| 268 |
+
situation, the edges returned by the method are always bidirectional.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
faces: Integer array of shape [num_faces, 3]. Contains node indices
|
| 272 |
+
adjacent to each face.
|
| 273 |
+
Returns:
|
| 274 |
+
Tuple with sender/receiver indices, each of shape [num_edges=num_faces*3].
|
| 275 |
+
|
| 276 |
+
"""
|
| 277 |
+
assert faces.ndim == 2
|
| 278 |
+
assert faces.shape[-1] == 3
|
| 279 |
+
senders = np.concatenate([faces[:, 0], faces[:, 1], faces[:, 2]])
|
| 280 |
+
receivers = np.concatenate([faces[:, 1], faces[:, 2], faces[:, 0]])
|
| 281 |
+
return senders, receivers
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def get_last_triangular_mesh_for_sphere(splits: int) -> TriangularMesh:
|
| 285 |
+
return get_hierarchy_of_triangular_meshes_for_sphere(splits=splits)[-1]
|
model/graphcast/losses.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Loss functions (and terms for use in loss functions) used for weather."""
|
| 15 |
+
|
| 16 |
+
from typing import Mapping
|
| 17 |
+
|
| 18 |
+
from . import xarray_tree
|
| 19 |
+
import numpy as np
|
| 20 |
+
from typing_extensions import Protocol
|
| 21 |
+
import xarray
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
LossAndDiagnostics = tuple[xarray.DataArray, xarray.Dataset]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class LossFunction(Protocol):
|
| 28 |
+
"""A loss function.
|
| 29 |
+
|
| 30 |
+
This is a protocol so it's fine to use a plain function which 'quacks like'
|
| 31 |
+
this. This is just to document the interface.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __call__(self,
|
| 35 |
+
predictions: xarray.Dataset,
|
| 36 |
+
targets: xarray.Dataset,
|
| 37 |
+
**optional_kwargs) -> LossAndDiagnostics:
|
| 38 |
+
"""Computes a loss function.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
predictions: Dataset of predictions.
|
| 42 |
+
targets: Dataset of targets.
|
| 43 |
+
**optional_kwargs: Implementations may support extra optional kwargs.
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
loss: A DataArray with dimensions ('batch',) containing losses for each
|
| 47 |
+
element of the batch. These will be averaged to give the final
|
| 48 |
+
loss, locally and across replicas.
|
| 49 |
+
diagnostics: Mapping of additional quantities to log by name alongside the
|
| 50 |
+
loss. These will will typically correspond to terms in the loss. They
|
| 51 |
+
should also have dimensions ('batch',) and will be averaged over the
|
| 52 |
+
batch before logging.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def weighted_mse_per_level(
|
| 57 |
+
predictions: xarray.Dataset,
|
| 58 |
+
targets: xarray.Dataset,
|
| 59 |
+
per_variable_weights: Mapping[str, float],
|
| 60 |
+
) -> LossAndDiagnostics:
|
| 61 |
+
"""Latitude- and pressure-level-weighted MSE loss."""
|
| 62 |
+
def loss(prediction, target):
|
| 63 |
+
loss = (prediction - target)**2
|
| 64 |
+
loss *= normalized_latitude_weights(target).astype(loss.dtype)
|
| 65 |
+
if 'level' in target.dims:
|
| 66 |
+
loss *= normalized_level_weights(target).astype(loss.dtype)
|
| 67 |
+
return _mean_preserving_batch(loss)
|
| 68 |
+
|
| 69 |
+
losses = xarray_tree.map_structure(loss, predictions, targets)
|
| 70 |
+
return sum_per_variable_losses(losses, per_variable_weights)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _mean_preserving_batch(x: xarray.DataArray) -> xarray.DataArray:
|
| 74 |
+
return x.mean([d for d in x.dims if d != 'batch'], skipna=False)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def sum_per_variable_losses(
|
| 78 |
+
per_variable_losses: Mapping[str, xarray.DataArray],
|
| 79 |
+
weights: Mapping[str, float],
|
| 80 |
+
) -> LossAndDiagnostics:
|
| 81 |
+
"""Weighted sum of per-variable losses."""
|
| 82 |
+
if not set(weights.keys()).issubset(set(per_variable_losses.keys())):
|
| 83 |
+
raise ValueError(
|
| 84 |
+
'Passing a weight that does not correspond to any variable '
|
| 85 |
+
f'{set(weights.keys())-set(per_variable_losses.keys())}')
|
| 86 |
+
|
| 87 |
+
weighted_per_variable_losses = {
|
| 88 |
+
name: loss * weights.get(name, 1)
|
| 89 |
+
for name, loss in per_variable_losses.items()
|
| 90 |
+
}
|
| 91 |
+
total = xarray.concat(
|
| 92 |
+
weighted_per_variable_losses.values(), dim='variable', join='exact').sum(
|
| 93 |
+
'variable', skipna=False)
|
| 94 |
+
return total, per_variable_losses # pytype: disable=bad-return-type
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def normalized_level_weights(data: xarray.DataArray) -> xarray.DataArray:
|
| 98 |
+
"""Weights proportional to pressure at each level."""
|
| 99 |
+
level = data.coords['level']
|
| 100 |
+
return level / level.mean(skipna=False)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def normalized_latitude_weights(data: xarray.DataArray) -> xarray.DataArray:
|
| 104 |
+
"""Weights based on latitude, roughly proportional to grid cell area.
|
| 105 |
+
|
| 106 |
+
This method supports two use cases only (both for equispaced values):
|
| 107 |
+
* Latitude values such that the closest value to the pole is at latitude
|
| 108 |
+
(90 - d_lat/2), where d_lat is the difference between contiguous latitudes.
|
| 109 |
+
For example: [-89, -87, -85, ..., 85, 87, 89]) (d_lat = 2)
|
| 110 |
+
In this case each point with `lat` value represents a sphere slice between
|
| 111 |
+
`lat - d_lat/2` and `lat + d_lat/2`, and the area of this slice would be
|
| 112 |
+
proportional to:
|
| 113 |
+
`sin(lat + d_lat/2) - sin(lat - d_lat/2) = 2 * sin(d_lat/2) * cos(lat)`, and
|
| 114 |
+
we can simply omit the term `2 * sin(d_lat/2)` which is just a constant
|
| 115 |
+
that cancels during normalization.
|
| 116 |
+
* Latitude values that fall exactly at the poles.
|
| 117 |
+
For example: [-90, -88, -86, ..., 86, 88, 90]) (d_lat = 2)
|
| 118 |
+
In this case each point with `lat` value also represents
|
| 119 |
+
a sphere slice between `lat - d_lat/2` and `lat + d_lat/2`,
|
| 120 |
+
except for the points at the poles, that represent a slice between
|
| 121 |
+
`90 - d_lat/2` and `90` or, `-90` and `-90 + d_lat/2`.
|
| 122 |
+
The areas of the first type of point are still proportional to:
|
| 123 |
+
* sin(lat + d_lat/2) - sin(lat - d_lat/2) = 2 * sin(d_lat/2) * cos(lat)
|
| 124 |
+
but for the points at the poles now is:
|
| 125 |
+
* sin(90) - sin(90 - d_lat/2) = 2 * sin(d_lat/4) ^ 2
|
| 126 |
+
and we will be using these weights, depending on whether we are looking at
|
| 127 |
+
pole cells, or non-pole cells (omitting the common factor of 2 which will be
|
| 128 |
+
absorbed by the normalization).
|
| 129 |
+
|
| 130 |
+
It can be shown via a limit, or simple geometry, that in the small angles
|
| 131 |
+
regime, the proportion of area per pole-point is equal to 1/8th
|
| 132 |
+
the proportion of area covered by each of the nearest non-pole point, and we
|
| 133 |
+
test for this in the test.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
data: `DataArray` with latitude coordinates.
|
| 137 |
+
Returns:
|
| 138 |
+
Unit mean latitude weights.
|
| 139 |
+
"""
|
| 140 |
+
latitude = data.coords['lat']
|
| 141 |
+
|
| 142 |
+
if np.any(np.isclose(np.abs(latitude), 90.)):
|
| 143 |
+
weights = _weight_for_latitude_vector_with_poles(latitude)
|
| 144 |
+
else:
|
| 145 |
+
weights = _weight_for_latitude_vector_without_poles(latitude)
|
| 146 |
+
|
| 147 |
+
return weights / weights.mean(skipna=False)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _weight_for_latitude_vector_without_poles(latitude):
|
| 151 |
+
"""Weights for uniform latitudes of the form [+-90-+d/2, ..., -+90+-d/2]."""
|
| 152 |
+
delta_latitude = np.abs(_check_uniform_spacing_and_get_delta(latitude))
|
| 153 |
+
if (not np.isclose(np.max(latitude), 90 - delta_latitude/2) or
|
| 154 |
+
not np.isclose(np.min(latitude), -90 + delta_latitude/2)):
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f'Latitude vector {latitude} does not start/end at '
|
| 157 |
+
'+- (90 - delta_latitude/2) degrees.')
|
| 158 |
+
return np.cos(np.deg2rad(latitude))
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _weight_for_latitude_vector_with_poles(latitude):
|
| 162 |
+
"""Weights for uniform latitudes of the form [+- 90, ..., -+90]."""
|
| 163 |
+
delta_latitude = np.abs(_check_uniform_spacing_and_get_delta(latitude))
|
| 164 |
+
if (not np.isclose(np.max(latitude), 90.) or
|
| 165 |
+
not np.isclose(np.min(latitude), -90.)):
|
| 166 |
+
raise ValueError(
|
| 167 |
+
f'Latitude vector {latitude} does not start/end at +- 90 degrees.')
|
| 168 |
+
weights = np.cos(np.deg2rad(latitude)) * np.sin(np.deg2rad(delta_latitude/2))
|
| 169 |
+
# The two checks above enough to guarantee that latitudes are sorted, so
|
| 170 |
+
# the extremes are the poles
|
| 171 |
+
weights[[0, -1]] = np.sin(np.deg2rad(delta_latitude/4)) ** 2
|
| 172 |
+
return weights
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _check_uniform_spacing_and_get_delta(vector):
|
| 176 |
+
diff = np.diff(vector)
|
| 177 |
+
if not np.all(np.isclose(diff[0], diff)):
|
| 178 |
+
raise ValueError(f'Vector {diff} is not uniformly spaced.')
|
| 179 |
+
return diff[0]
|
model/graphcast/mlp.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Constructors for MLPs."""
|
| 15 |
+
|
| 16 |
+
import haiku as hk
|
| 17 |
+
import jax
|
| 18 |
+
import jax.numpy as jnp
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# TODO(aelkadi): Move the mlp factory here from `deep_typed_graph_net.py`.
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class LinearNormConditioning(hk.Module):
|
| 25 |
+
"""Module for norm conditioning.
|
| 26 |
+
|
| 27 |
+
Conditions the normalization of "inputs" by applying a linear layer to the
|
| 28 |
+
"norm_conditioning" which produces the scale and variance which are applied to
|
| 29 |
+
each channel (across the last dim) of "inputs".
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(self, name="norm_conditioning"):
|
| 33 |
+
super().__init__(name=name)
|
| 34 |
+
|
| 35 |
+
def __call__(self, inputs: jax.Array, norm_conditioning: jax.Array):
|
| 36 |
+
|
| 37 |
+
feature_size = inputs.shape[-1]
|
| 38 |
+
conditional_linear_layer = hk.Linear(
|
| 39 |
+
output_size=2 * feature_size,
|
| 40 |
+
w_init=hk.initializers.TruncatedNormal(stddev=1e-8),
|
| 41 |
+
)
|
| 42 |
+
conditional_scale_offset = conditional_linear_layer(norm_conditioning)
|
| 43 |
+
scale_minus_one, offset = jnp.split(conditional_scale_offset, 2, axis=-1)
|
| 44 |
+
scale = scale_minus_one + 1.
|
| 45 |
+
return inputs * scale + offset
|
model/graphcast/model_utils.py
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utilities for building models."""
|
| 15 |
+
|
| 16 |
+
from typing import Any, Mapping, Optional, Tuple
|
| 17 |
+
|
| 18 |
+
import jax.numpy as jnp
|
| 19 |
+
import numpy as np
|
| 20 |
+
from scipy.spatial import transform
|
| 21 |
+
import xarray
|
| 22 |
+
|
| 23 |
+
NumpyInterface = Any
|
| 24 |
+
TransformInterface = Any
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def get_graph_spatial_features(
|
| 28 |
+
*, node_lat: np.ndarray, node_lon: np.ndarray,
|
| 29 |
+
senders: np.ndarray, receivers: np.ndarray,
|
| 30 |
+
add_node_positions: bool,
|
| 31 |
+
add_node_latitude: bool,
|
| 32 |
+
add_node_longitude: bool,
|
| 33 |
+
add_relative_positions: bool,
|
| 34 |
+
edge_normalization_factor: Optional[float] = None,
|
| 35 |
+
relative_longitude_local_coordinates: bool,
|
| 36 |
+
relative_latitude_local_coordinates: bool,
|
| 37 |
+
sine_cosine_encoding: bool = False,
|
| 38 |
+
encoding_num_freqs: int = 10,
|
| 39 |
+
encoding_multiplicative_factor: float = 1.2,
|
| 40 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 41 |
+
"""Computes spatial features for the nodes.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
node_lat: Latitudes in the [-90, 90] interval of shape [num_nodes]
|
| 45 |
+
node_lon: Longitudes in the [0, 360] interval of shape [num_nodes]
|
| 46 |
+
senders: Sender indices of shape [num_edges]
|
| 47 |
+
receivers: Receiver indices of shape [num_edges]
|
| 48 |
+
add_node_positions: Add unit norm absolute positions.
|
| 49 |
+
add_node_latitude: Add a feature for latitude (cos(90 - lat))
|
| 50 |
+
Note even if this is set to False, the model may be able to infer the
|
| 51 |
+
longitude from relative features, unless
|
| 52 |
+
`relative_latitude_local_coordinates` is also True, or if there is any
|
| 53 |
+
bias on the relative edge sizes for different longitudes.
|
| 54 |
+
add_node_longitude: Add features for longitude (cos(lon), sin(lon)).
|
| 55 |
+
Note even if this is set to False, the model may be able to infer the
|
| 56 |
+
longitude from relative features, unless
|
| 57 |
+
`relative_longitude_local_coordinates` is also True, or if there is any
|
| 58 |
+
bias on the relative edge sizes for different longitudes.
|
| 59 |
+
add_relative_positions: Whether to relative positions in R3 to the edges.
|
| 60 |
+
edge_normalization_factor: Allows explicitly controlling edge normalization.
|
| 61 |
+
If None, defaults to max edge length. This supports using pre-trained
|
| 62 |
+
model weights with a different graph structure to what it was trained.
|
| 63 |
+
relative_longitude_local_coordinates: If True, relative positions are
|
| 64 |
+
computed in a local space where the receiver is at 0 longitude.
|
| 65 |
+
relative_latitude_local_coordinates: If True, relative positions are
|
| 66 |
+
computed in a local space where the receiver is at 0 latitude.
|
| 67 |
+
sine_cosine_encoding: If True, we will transform the node/edge features
|
| 68 |
+
with sine and cosine functions, similar to NERF.
|
| 69 |
+
encoding_num_freqs: frequency parameter
|
| 70 |
+
encoding_multiplicative_factor: used for calculating the frequency.
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
Arrays of shape: [num_nodes, num_features] and [num_edges, num_features].
|
| 74 |
+
with node and edge features.
|
| 75 |
+
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
num_nodes = node_lat.shape[0]
|
| 79 |
+
num_edges = senders.shape[0]
|
| 80 |
+
dtype = node_lat.dtype
|
| 81 |
+
node_phi, node_theta = lat_lon_deg_to_spherical(node_lat, node_lon)
|
| 82 |
+
|
| 83 |
+
# Computing some node features.
|
| 84 |
+
node_features = []
|
| 85 |
+
if add_node_positions:
|
| 86 |
+
# Already in [-1, 1.] range.
|
| 87 |
+
node_features.extend(spherical_to_cartesian(node_phi, node_theta))
|
| 88 |
+
|
| 89 |
+
if add_node_latitude:
|
| 90 |
+
# Using the cos of theta.
|
| 91 |
+
# From 1. (north pole) to -1 (south pole).
|
| 92 |
+
node_features.append(np.cos(node_theta))
|
| 93 |
+
|
| 94 |
+
if add_node_longitude:
|
| 95 |
+
# Using the cos and sin, which is already normalized.
|
| 96 |
+
node_features.append(np.cos(node_phi))
|
| 97 |
+
node_features.append(np.sin(node_phi))
|
| 98 |
+
|
| 99 |
+
if not node_features:
|
| 100 |
+
node_features = np.zeros([num_nodes, 0], dtype=dtype)
|
| 101 |
+
else:
|
| 102 |
+
node_features = np.stack(node_features, axis=-1)
|
| 103 |
+
|
| 104 |
+
# Computing some edge features.
|
| 105 |
+
edge_features = []
|
| 106 |
+
|
| 107 |
+
if add_relative_positions:
|
| 108 |
+
|
| 109 |
+
relative_position = get_relative_position_in_receiver_local_coordinates(
|
| 110 |
+
node_phi=node_phi,
|
| 111 |
+
node_theta=node_theta,
|
| 112 |
+
senders=senders,
|
| 113 |
+
receivers=receivers,
|
| 114 |
+
latitude_local_coordinates=relative_latitude_local_coordinates,
|
| 115 |
+
longitude_local_coordinates=relative_longitude_local_coordinates
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Note this is L2 distance in 3d space, rather than geodesic distance.
|
| 119 |
+
relative_edge_distances = np.linalg.norm(
|
| 120 |
+
relative_position, axis=-1, keepdims=True)
|
| 121 |
+
|
| 122 |
+
if edge_normalization_factor is None:
|
| 123 |
+
# Normalize to the maximum edge distance. Note that we expect to always
|
| 124 |
+
# have an edge that goes in the opposite direction of any given edge
|
| 125 |
+
# so the distribution of relative positions should be symmetric around
|
| 126 |
+
# zero. So by scaling by the maximum length, we expect all relative
|
| 127 |
+
# positions to fall in the [-1., 1.] interval, and all relative distances
|
| 128 |
+
# to fall in the [0., 1.] interval.
|
| 129 |
+
edge_normalization_factor = relative_edge_distances.max()
|
| 130 |
+
edge_features.append(relative_edge_distances / edge_normalization_factor)
|
| 131 |
+
edge_features.append(relative_position / edge_normalization_factor)
|
| 132 |
+
|
| 133 |
+
if not edge_features:
|
| 134 |
+
edge_features = np.zeros([num_edges, 0], dtype=dtype)
|
| 135 |
+
else:
|
| 136 |
+
edge_features = np.concatenate(edge_features, axis=-1)
|
| 137 |
+
|
| 138 |
+
if sine_cosine_encoding:
|
| 139 |
+
def sine_cosine_transform(x: np.ndarray) -> np.ndarray:
|
| 140 |
+
freqs = encoding_multiplicative_factor**np.arange(encoding_num_freqs)
|
| 141 |
+
phases = freqs * x[..., None]
|
| 142 |
+
x_sin = np.sin(phases)
|
| 143 |
+
x_cos = np.cos(phases)
|
| 144 |
+
x_cat = np.concatenate([x_sin, x_cos], axis=-1)
|
| 145 |
+
return x_cat.reshape([x.shape[0], -1])
|
| 146 |
+
|
| 147 |
+
node_features = sine_cosine_transform(node_features)
|
| 148 |
+
edge_features = sine_cosine_transform(edge_features)
|
| 149 |
+
|
| 150 |
+
return node_features, edge_features
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def lat_lon_to_leading_axes(
|
| 154 |
+
grid_xarray: xarray.DataArray) -> xarray.DataArray:
|
| 155 |
+
"""Reorders xarray so lat/lon axes come first."""
|
| 156 |
+
# leading + ["lat", "lon"] + trailing
|
| 157 |
+
# to
|
| 158 |
+
# ["lat", "lon"] + leading + trailing
|
| 159 |
+
return grid_xarray.transpose("lat", "lon", ...)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def restore_leading_axes(grid_xarray: xarray.DataArray) -> xarray.DataArray:
|
| 163 |
+
"""Reorders xarray so batch/time/level axes come first (if present)."""
|
| 164 |
+
|
| 165 |
+
# ["lat", "lon"] + [(batch,) (time,) (level,)] + trailing
|
| 166 |
+
# to
|
| 167 |
+
# [(batch,) (time,) (level,)] + ["lat", "lon"] + trailing
|
| 168 |
+
|
| 169 |
+
input_dims = list(grid_xarray.dims)
|
| 170 |
+
output_dims = list(input_dims)
|
| 171 |
+
for leading_key in ["level", "time", "batch"]: # reverse order for insert
|
| 172 |
+
if leading_key in input_dims:
|
| 173 |
+
output_dims.remove(leading_key)
|
| 174 |
+
output_dims.insert(0, leading_key)
|
| 175 |
+
return grid_xarray.transpose(*output_dims)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def lat_lon_deg_to_spherical(node_lat: np.ndarray,
|
| 179 |
+
node_lon: np.ndarray,
|
| 180 |
+
np_: NumpyInterface = np,
|
| 181 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 182 |
+
phi = np_.deg2rad(node_lon)
|
| 183 |
+
theta = np_.deg2rad(90 - node_lat)
|
| 184 |
+
return phi, theta
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def spherical_to_lat_lon(phi: np.ndarray,
|
| 188 |
+
theta: np.ndarray,
|
| 189 |
+
np_: NumpyInterface = np,
|
| 190 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 191 |
+
lon = np_.mod(np_.rad2deg(phi), 360)
|
| 192 |
+
lat = 90 - np_.rad2deg(theta)
|
| 193 |
+
return lat, lon
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def cartesian_to_spherical(x: np.ndarray,
|
| 197 |
+
y: np.ndarray,
|
| 198 |
+
z: np.ndarray,
|
| 199 |
+
np_: NumpyInterface = np,
|
| 200 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 201 |
+
phi = np_.arctan2(y, x)
|
| 202 |
+
with np.errstate(invalid="ignore"): # circumventing b/253179568
|
| 203 |
+
theta = np_.arccos(z) # Assuming unit radius.
|
| 204 |
+
return phi, theta
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def spherical_to_cartesian(
|
| 208 |
+
phi: np.ndarray, theta: np.ndarray,
|
| 209 |
+
np_: NumpyInterface = np,
|
| 210 |
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 211 |
+
# Assuming unit radius.
|
| 212 |
+
return (np_.cos(phi)*np_.sin(theta),
|
| 213 |
+
np_.sin(phi)*np_.sin(theta),
|
| 214 |
+
np_.cos(theta))
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def lat_lon_to_cartesian(
|
| 218 |
+
lat: np.ndarray, lon: np.ndarray,
|
| 219 |
+
np_: NumpyInterface = np,
|
| 220 |
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 221 |
+
return spherical_to_cartesian(
|
| 222 |
+
*lat_lon_deg_to_spherical(lat, lon, np_=np_), np_=np_)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def cartesian_to_lat_lon(
|
| 226 |
+
x: np.ndarray,
|
| 227 |
+
y: np.ndarray,
|
| 228 |
+
z: np.ndarray,
|
| 229 |
+
np_: NumpyInterface = np,
|
| 230 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 231 |
+
return spherical_to_lat_lon(
|
| 232 |
+
*cartesian_to_spherical(x, y, z, np_=np_), np_=np_)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def get_relative_position_in_receiver_local_coordinates(
|
| 236 |
+
node_phi: np.ndarray,
|
| 237 |
+
node_theta: np.ndarray,
|
| 238 |
+
senders: np.ndarray,
|
| 239 |
+
receivers: np.ndarray,
|
| 240 |
+
latitude_local_coordinates: bool,
|
| 241 |
+
longitude_local_coordinates: bool,
|
| 242 |
+
np_: NumpyInterface = np,
|
| 243 |
+
transform_: TransformInterface = transform,
|
| 244 |
+
) -> np.ndarray:
|
| 245 |
+
"""Returns relative position features for the edges.
|
| 246 |
+
|
| 247 |
+
The relative positions will be computed in a rotated space for a local
|
| 248 |
+
coordinate system as defined by the receiver. The relative positions are
|
| 249 |
+
simply obtained by subtracting sender position minues receiver position in
|
| 250 |
+
that local coordinate system after the rotation in R^3.
|
| 251 |
+
|
| 252 |
+
Args:
|
| 253 |
+
node_phi: [num_nodes] with polar angles.
|
| 254 |
+
node_theta: [num_nodes] with azimuthal angles.
|
| 255 |
+
senders: [num_edges] with indices.
|
| 256 |
+
receivers: [num_edges] with indices.
|
| 257 |
+
latitude_local_coordinates: Whether to rotate edges such that in the
|
| 258 |
+
positions are computed such that the receiver is always at latitude 0.
|
| 259 |
+
longitude_local_coordinates: Whether to rotate edges such that in the
|
| 260 |
+
positions are computed such that the receiver is always at longitude 0.
|
| 261 |
+
np_: Numpy library interface.
|
| 262 |
+
transform_: scipy.transform library interface.
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
Array of relative positions in R3 [num_edges, 3]
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
node_pos = np_.stack(
|
| 269 |
+
spherical_to_cartesian(node_phi, node_theta, np_=np_), axis=-1)
|
| 270 |
+
|
| 271 |
+
# No rotation in this case.
|
| 272 |
+
if not (latitude_local_coordinates or longitude_local_coordinates):
|
| 273 |
+
return node_pos[senders] - node_pos[receivers]
|
| 274 |
+
|
| 275 |
+
# Get rotation matrices for the local space space for every node.
|
| 276 |
+
rotation_matrices = get_rotation_matrices_to_local_coordinates(
|
| 277 |
+
reference_phi=node_phi,
|
| 278 |
+
reference_theta=node_theta,
|
| 279 |
+
rotate_latitude=latitude_local_coordinates,
|
| 280 |
+
rotate_longitude=longitude_local_coordinates,
|
| 281 |
+
np_=np_,
|
| 282 |
+
transform_=transform_)
|
| 283 |
+
|
| 284 |
+
# Each edge will be rotated according to the rotation matrix of its receiver
|
| 285 |
+
# node.
|
| 286 |
+
edge_rotation_matrices = rotation_matrices[receivers]
|
| 287 |
+
|
| 288 |
+
# Rotate all nodes to the rotated space of the corresponding edge.
|
| 289 |
+
# Note for receivers we can also do the matmul first and the gather second:
|
| 290 |
+
# ```
|
| 291 |
+
# receiver_pos_in_rotated_space = rotate_with_matrices(
|
| 292 |
+
# rotation_matrices, node_pos)[receivers]
|
| 293 |
+
# ```
|
| 294 |
+
# which is more efficient, however, we do gather first to keep it more
|
| 295 |
+
# symmetric with the sender computation.
|
| 296 |
+
receiver_pos_in_rotated_space = rotate_with_matrices(
|
| 297 |
+
edge_rotation_matrices, node_pos[receivers], np_=np_)
|
| 298 |
+
sender_pos_in_in_rotated_space = rotate_with_matrices(
|
| 299 |
+
edge_rotation_matrices, node_pos[senders], np_=np_)
|
| 300 |
+
# Note, here, that because the rotated space is chosen according to the
|
| 301 |
+
# receiver, if:
|
| 302 |
+
# * latitude_local_coordinates = True: latitude for the receivers will be
|
| 303 |
+
# 0, that is the z coordinate will always be 0.
|
| 304 |
+
# * longitude_local_coordinates = True: longitude for the receivers will be
|
| 305 |
+
# 0, that is the y coordinate will be 0.
|
| 306 |
+
|
| 307 |
+
# Now we can just subtract.
|
| 308 |
+
# Note we are rotating to a local coordinate system, where the y-z axes are
|
| 309 |
+
# parallel to a tangent plane to the sphere, but still remain in a 3d space.
|
| 310 |
+
# Note that if both `latitude_local_coordinates` and
|
| 311 |
+
# `longitude_local_coordinates` are True, and edges are short,
|
| 312 |
+
# then the difference in x coordinate between sender and receiver
|
| 313 |
+
# should be small, so we could consider dropping the new x coordinate if
|
| 314 |
+
# we wanted to the tangent plane, however in doing so
|
| 315 |
+
# we would lose information about the curvature of the mesh, which may be
|
| 316 |
+
# important for very coarse meshes.
|
| 317 |
+
return sender_pos_in_in_rotated_space - receiver_pos_in_rotated_space
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def get_rotation_matrices_to_local_coordinates(
|
| 321 |
+
reference_phi: np.ndarray,
|
| 322 |
+
reference_theta: np.ndarray,
|
| 323 |
+
rotate_latitude: bool,
|
| 324 |
+
rotate_longitude: bool,
|
| 325 |
+
np_: NumpyInterface = np,
|
| 326 |
+
transform_: TransformInterface = transform,
|
| 327 |
+
) -> np.ndarray:
|
| 328 |
+
"""Returns a rotation matrix to rotate to a point based on a reference vector.
|
| 329 |
+
|
| 330 |
+
The rotation matrix is build such that, a vector in the
|
| 331 |
+
same coordinate system at the reference point that points towards the pole
|
| 332 |
+
before the rotation, continues to point towards the pole after the rotation.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
reference_phi: [leading_axis] Polar angles of the reference.
|
| 336 |
+
reference_theta: [leading_axis] Azimuthal angles of the reference.
|
| 337 |
+
rotate_latitude: Whether to produce a rotation matrix that would rotate
|
| 338 |
+
R^3 vectors to zero latitude.
|
| 339 |
+
rotate_longitude: Whether to produce a rotation matrix that would rotate
|
| 340 |
+
R^3 vectors to zero longitude.
|
| 341 |
+
np_: Numpy library interface.
|
| 342 |
+
transform_: scipy.transform library interface.
|
| 343 |
+
|
| 344 |
+
Returns:
|
| 345 |
+
Matrices of shape [leading_axis] such that when applied to the reference
|
| 346 |
+
position with `rotate_with_matrices(rotation_matrices, reference_pos)`
|
| 347 |
+
|
| 348 |
+
* phi goes to 0. if "rotate_longitude" is True.
|
| 349 |
+
|
| 350 |
+
* theta goes to np.pi / 2 if "rotate_latitude" is True.
|
| 351 |
+
|
| 352 |
+
The rotation consists of:
|
| 353 |
+
* rotate_latitude = False, rotate_longitude = True:
|
| 354 |
+
Latitude preserving rotation.
|
| 355 |
+
* rotate_latitude = True, rotate_longitude = True:
|
| 356 |
+
Latitude preserving rotation, followed by longitude preserving
|
| 357 |
+
rotation.
|
| 358 |
+
* rotate_latitude = True, rotate_longitude = False:
|
| 359 |
+
Latitude preserving rotation, followed by longitude preserving
|
| 360 |
+
rotation, and the inverse of the latitude preserving rotation. Note
|
| 361 |
+
this is computationally different from rotating the longitude only
|
| 362 |
+
and is. We do it like this, so the polar geodesic curve, continues
|
| 363 |
+
to be aligned with one of the axis after the rotation.
|
| 364 |
+
|
| 365 |
+
"""
|
| 366 |
+
|
| 367 |
+
# Azimuthal angle we need to apply to move to zero longitude.
|
| 368 |
+
azimuthal_rotation = - reference_phi
|
| 369 |
+
|
| 370 |
+
# Polar angle we need to apply to move from "theta" to zero latitude.
|
| 371 |
+
polar_rotation = - reference_theta + np.pi/2
|
| 372 |
+
|
| 373 |
+
if rotate_longitude and rotate_latitude:
|
| 374 |
+
# We first rotate to zero longitude around the z axis, and then, when the
|
| 375 |
+
# point is at x=0 we can simply apply the polar rotation around the y axis.
|
| 376 |
+
return transform_.Rotation.from_euler(
|
| 377 |
+
"zy", np_.stack([azimuthal_rotation, polar_rotation],
|
| 378 |
+
axis=1)).as_matrix()
|
| 379 |
+
elif rotate_longitude:
|
| 380 |
+
# Just like the previous case, but applying only the azimuthal rotation,
|
| 381 |
+
# leaving the latitude unchanged. Even though it is a single rotation, we
|
| 382 |
+
# need a "sequence" axis of size 1 (hence the expand_dims).
|
| 383 |
+
return transform_.Rotation.from_euler(
|
| 384 |
+
"z", np_.expand_dims(azimuthal_rotation, axis=1)).as_matrix()
|
| 385 |
+
elif rotate_latitude:
|
| 386 |
+
# We want to apply the polar rotation only, but we don't know the rotation
|
| 387 |
+
# axis to apply a polar rotation. The simplest way to achieve this is to
|
| 388 |
+
# first rotate all the way to longitude 0, then apply the polar rotation
|
| 389 |
+
# arond the y axis, and then rotate back to the original longitude.
|
| 390 |
+
return transform_.Rotation.from_euler(
|
| 391 |
+
"zyz", np_.stack(
|
| 392 |
+
[azimuthal_rotation, polar_rotation, -azimuthal_rotation]
|
| 393 |
+
, axis=1)).as_matrix()
|
| 394 |
+
else:
|
| 395 |
+
raise ValueError(
|
| 396 |
+
"At least one of longitude and latitude should be rotated.")
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def rotate_with_matrices(rotation_matrices: np.ndarray, positions: np.ndarray,
|
| 400 |
+
np_: NumpyInterface = np) -> np.ndarray:
|
| 401 |
+
return np_.einsum("...ji,...i->...j", rotation_matrices, positions)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def get_bipartite_graph_spatial_features(
|
| 405 |
+
*,
|
| 406 |
+
senders_node_lat: np.ndarray,
|
| 407 |
+
senders_node_lon: np.ndarray,
|
| 408 |
+
senders: np.ndarray,
|
| 409 |
+
receivers_node_lat: np.ndarray,
|
| 410 |
+
receivers_node_lon: np.ndarray,
|
| 411 |
+
receivers: np.ndarray,
|
| 412 |
+
add_node_positions: bool,
|
| 413 |
+
add_node_latitude: bool,
|
| 414 |
+
add_node_longitude: bool,
|
| 415 |
+
add_relative_positions: bool,
|
| 416 |
+
edge_normalization_factor: Optional[float] = None,
|
| 417 |
+
relative_longitude_local_coordinates: bool,
|
| 418 |
+
relative_latitude_local_coordinates: bool,
|
| 419 |
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 420 |
+
"""Computes spatial features for the nodes.
|
| 421 |
+
|
| 422 |
+
This function is almost identical to `get_graph_spatial_features`. The only
|
| 423 |
+
difference is that sender nodes and receiver nodes can be in different arrays.
|
| 424 |
+
This is necessary to enable combination with typed Graph.
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
senders_node_lat: Latitudes in the [-90, 90] interval of shape
|
| 428 |
+
[num_sender_nodes]
|
| 429 |
+
senders_node_lon: Longitudes in the [0, 360] interval of shape
|
| 430 |
+
[num_sender_nodes]
|
| 431 |
+
senders: Sender indices of shape [num_edges], indices in [0,
|
| 432 |
+
num_sender_nodes)
|
| 433 |
+
receivers_node_lat: Latitudes in the [-90, 90] interval of shape
|
| 434 |
+
[num_receiver_nodes]
|
| 435 |
+
receivers_node_lon: Longitudes in the [0, 360] interval of shape
|
| 436 |
+
[num_receiver_nodes]
|
| 437 |
+
receivers: Receiver indices of shape [num_edges], indices in [0,
|
| 438 |
+
num_receiver_nodes)
|
| 439 |
+
add_node_positions: Add unit norm absolute positions.
|
| 440 |
+
add_node_latitude: Add a feature for latitude (cos(90 - lat)) Note even if
|
| 441 |
+
this is set to False, the model may be able to infer the longitude from
|
| 442 |
+
relative features, unless `relative_latitude_local_coordinates` is also
|
| 443 |
+
True, or if there is any bias on the relative edge sizes for different
|
| 444 |
+
longitudes.
|
| 445 |
+
add_node_longitude: Add features for longitude (cos(lon), sin(lon)). Note
|
| 446 |
+
even if this is set to False, the model may be able to infer the longitude
|
| 447 |
+
from relative features, unless `relative_longitude_local_coordinates` is
|
| 448 |
+
also True, or if there is any bias on the relative edge sizes for
|
| 449 |
+
different longitudes.
|
| 450 |
+
add_relative_positions: Whether to relative positions in R3 to the edges.
|
| 451 |
+
edge_normalization_factor: Allows explicitly controlling edge normalization.
|
| 452 |
+
If None, defaults to max edge length. This supports using pre-trained
|
| 453 |
+
model weights with a different graph structure to what it was trained on.
|
| 454 |
+
relative_longitude_local_coordinates: If True, relative positions are
|
| 455 |
+
computed in a local space where the receiver is at 0 longitude.
|
| 456 |
+
relative_latitude_local_coordinates: If True, relative positions are
|
| 457 |
+
computed in a local space where the receiver is at 0 latitude.
|
| 458 |
+
|
| 459 |
+
Returns:
|
| 460 |
+
Arrays of shape: [num_nodes, num_features] and [num_edges, num_features].
|
| 461 |
+
with node and edge features.
|
| 462 |
+
|
| 463 |
+
"""
|
| 464 |
+
|
| 465 |
+
num_senders = senders_node_lat.shape[0]
|
| 466 |
+
num_receivers = receivers_node_lat.shape[0]
|
| 467 |
+
num_edges = senders.shape[0]
|
| 468 |
+
dtype = senders_node_lat.dtype
|
| 469 |
+
assert receivers_node_lat.dtype == dtype
|
| 470 |
+
senders_node_phi, senders_node_theta = lat_lon_deg_to_spherical(
|
| 471 |
+
senders_node_lat, senders_node_lon)
|
| 472 |
+
receivers_node_phi, receivers_node_theta = lat_lon_deg_to_spherical(
|
| 473 |
+
receivers_node_lat, receivers_node_lon)
|
| 474 |
+
|
| 475 |
+
# Computing some node features.
|
| 476 |
+
senders_node_features = []
|
| 477 |
+
receivers_node_features = []
|
| 478 |
+
if add_node_positions:
|
| 479 |
+
# Already in [-1, 1.] range.
|
| 480 |
+
senders_node_features.extend(
|
| 481 |
+
spherical_to_cartesian(senders_node_phi, senders_node_theta))
|
| 482 |
+
receivers_node_features.extend(
|
| 483 |
+
spherical_to_cartesian(receivers_node_phi, receivers_node_theta))
|
| 484 |
+
|
| 485 |
+
if add_node_latitude:
|
| 486 |
+
# Using the cos of theta.
|
| 487 |
+
# From 1. (north pole) to -1 (south pole).
|
| 488 |
+
senders_node_features.append(np.cos(senders_node_theta))
|
| 489 |
+
receivers_node_features.append(np.cos(receivers_node_theta))
|
| 490 |
+
|
| 491 |
+
if add_node_longitude:
|
| 492 |
+
# Using the cos and sin, which is already normalized.
|
| 493 |
+
senders_node_features.append(np.cos(senders_node_phi))
|
| 494 |
+
senders_node_features.append(np.sin(senders_node_phi))
|
| 495 |
+
|
| 496 |
+
receivers_node_features.append(np.cos(receivers_node_phi))
|
| 497 |
+
receivers_node_features.append(np.sin(receivers_node_phi))
|
| 498 |
+
|
| 499 |
+
if not senders_node_features:
|
| 500 |
+
senders_node_features = np.zeros([num_senders, 0], dtype=dtype)
|
| 501 |
+
receivers_node_features = np.zeros([num_receivers, 0], dtype=dtype)
|
| 502 |
+
else:
|
| 503 |
+
senders_node_features = np.stack(senders_node_features, axis=-1)
|
| 504 |
+
receivers_node_features = np.stack(receivers_node_features, axis=-1)
|
| 505 |
+
|
| 506 |
+
# Computing some edge features.
|
| 507 |
+
edge_features = []
|
| 508 |
+
|
| 509 |
+
if add_relative_positions:
|
| 510 |
+
|
| 511 |
+
relative_position = get_bipartite_relative_position_in_receiver_local_coordinates( # pylint: disable=line-too-long
|
| 512 |
+
senders_node_phi=senders_node_phi,
|
| 513 |
+
senders_node_theta=senders_node_theta,
|
| 514 |
+
receivers_node_phi=receivers_node_phi,
|
| 515 |
+
receivers_node_theta=receivers_node_theta,
|
| 516 |
+
senders=senders,
|
| 517 |
+
receivers=receivers,
|
| 518 |
+
latitude_local_coordinates=relative_latitude_local_coordinates,
|
| 519 |
+
longitude_local_coordinates=relative_longitude_local_coordinates)
|
| 520 |
+
|
| 521 |
+
# Note this is L2 distance in 3d space, rather than geodesic distance.
|
| 522 |
+
relative_edge_distances = np.linalg.norm(
|
| 523 |
+
relative_position, axis=-1, keepdims=True)
|
| 524 |
+
|
| 525 |
+
if edge_normalization_factor is None:
|
| 526 |
+
# Normalize to the maximum edge distance. Note that we expect to always
|
| 527 |
+
# have an edge that goes in the opposite direction of any given edge
|
| 528 |
+
# so the distribution of relative positions should be symmetric around
|
| 529 |
+
# zero. So by scaling by the maximum length, we expect all relative
|
| 530 |
+
# positions to fall in the [-1., 1.] interval, and all relative distances
|
| 531 |
+
# to fall in the [0., 1.] interval.
|
| 532 |
+
edge_normalization_factor = relative_edge_distances.max()
|
| 533 |
+
|
| 534 |
+
edge_features.append(relative_edge_distances / edge_normalization_factor)
|
| 535 |
+
edge_features.append(relative_position / edge_normalization_factor)
|
| 536 |
+
|
| 537 |
+
if not edge_features:
|
| 538 |
+
edge_features = np.zeros([num_edges, 0], dtype=dtype)
|
| 539 |
+
else:
|
| 540 |
+
edge_features = np.concatenate(edge_features, axis=-1)
|
| 541 |
+
|
| 542 |
+
return senders_node_features, receivers_node_features, edge_features
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def get_bipartite_relative_position_in_receiver_local_coordinates(
|
| 546 |
+
senders_node_phi: np.ndarray,
|
| 547 |
+
senders_node_theta: np.ndarray,
|
| 548 |
+
senders: np.ndarray,
|
| 549 |
+
receivers_node_phi: np.ndarray,
|
| 550 |
+
receivers_node_theta: np.ndarray,
|
| 551 |
+
receivers: np.ndarray,
|
| 552 |
+
latitude_local_coordinates: bool,
|
| 553 |
+
longitude_local_coordinates: bool,
|
| 554 |
+
np_: NumpyInterface = np,
|
| 555 |
+
transform_: TransformInterface = transform,
|
| 556 |
+
) -> np.ndarray:
|
| 557 |
+
"""Returns relative position features for the edges.
|
| 558 |
+
|
| 559 |
+
This function is equivalent to
|
| 560 |
+
`get_relative_position_in_receiver_local_coordinates`, but adapted to work
|
| 561 |
+
with bipartite typed graphs.
|
| 562 |
+
|
| 563 |
+
The relative positions will be computed in a rotated space for a local
|
| 564 |
+
coordinate system as defined by the receiver. The relative positions are
|
| 565 |
+
simply obtained by subtracting sender position minues receiver position in
|
| 566 |
+
that local coordinate system after the rotation in R^3.
|
| 567 |
+
|
| 568 |
+
Args:
|
| 569 |
+
senders_node_phi: [num_sender_nodes] with polar angles.
|
| 570 |
+
senders_node_theta: [num_sender_nodes] with azimuthal angles.
|
| 571 |
+
senders: [num_edges] with indices into sender nodes.
|
| 572 |
+
receivers_node_phi: [num_sender_nodes] with polar angles.
|
| 573 |
+
receivers_node_theta: [num_sender_nodes] with azimuthal angles.
|
| 574 |
+
receivers: [num_edges] with indices into receiver nodes.
|
| 575 |
+
latitude_local_coordinates: Whether to rotate edges such that in the
|
| 576 |
+
positions are computed such that the receiver is always at latitude 0.
|
| 577 |
+
longitude_local_coordinates: Whether to rotate edges such that in the
|
| 578 |
+
positions are computed such that the receiver is always at longitude 0.
|
| 579 |
+
np_: Numpy library interface.
|
| 580 |
+
transform_: scipy.transform library interface.
|
| 581 |
+
|
| 582 |
+
Returns:
|
| 583 |
+
Array of relative positions in R3 [num_edges, 3]
|
| 584 |
+
"""
|
| 585 |
+
|
| 586 |
+
senders_node_pos = np_.stack(
|
| 587 |
+
spherical_to_cartesian(
|
| 588 |
+
senders_node_phi, senders_node_theta, np_=np_), axis=-1)
|
| 589 |
+
|
| 590 |
+
receivers_node_pos = np_.stack(
|
| 591 |
+
spherical_to_cartesian(
|
| 592 |
+
receivers_node_phi, receivers_node_theta, np_=np_), axis=-1)
|
| 593 |
+
|
| 594 |
+
# No rotation in this case.
|
| 595 |
+
if not (latitude_local_coordinates or longitude_local_coordinates):
|
| 596 |
+
return senders_node_pos[senders] - receivers_node_pos[receivers]
|
| 597 |
+
|
| 598 |
+
# Get rotation matrices for the local space space for every receiver node.
|
| 599 |
+
receiver_rotation_matrices = get_rotation_matrices_to_local_coordinates(
|
| 600 |
+
reference_phi=receivers_node_phi,
|
| 601 |
+
reference_theta=receivers_node_theta,
|
| 602 |
+
rotate_latitude=latitude_local_coordinates,
|
| 603 |
+
rotate_longitude=longitude_local_coordinates,
|
| 604 |
+
np_=np_,
|
| 605 |
+
transform_=transform_)
|
| 606 |
+
|
| 607 |
+
# Each edge will be rotated according to the rotation matrix of its receiver
|
| 608 |
+
# node.
|
| 609 |
+
edge_rotation_matrices = receiver_rotation_matrices[receivers]
|
| 610 |
+
|
| 611 |
+
# Rotate all nodes to the rotated space of the corresponding edge.
|
| 612 |
+
# Note for receivers we can also do the matmul first and the gather second:
|
| 613 |
+
# ```
|
| 614 |
+
# receiver_pos_in_rotated_space = rotate_with_matrices(
|
| 615 |
+
# rotation_matrices, node_pos)[receivers]
|
| 616 |
+
# ```
|
| 617 |
+
# which is more efficient, however, we do gather first to keep it more
|
| 618 |
+
# symmetric with the sender computation.
|
| 619 |
+
receiver_pos_in_rotated_space = rotate_with_matrices(
|
| 620 |
+
edge_rotation_matrices, receivers_node_pos[receivers], np_=np_)
|
| 621 |
+
sender_pos_in_in_rotated_space = rotate_with_matrices(
|
| 622 |
+
edge_rotation_matrices, senders_node_pos[senders], np_=np_)
|
| 623 |
+
# Note, here, that because the rotated space is chosen according to the
|
| 624 |
+
# receiver, if:
|
| 625 |
+
# * latitude_local_coordinates = True: latitude for the receivers will be
|
| 626 |
+
# 0, that is the z coordinate will always be 0.
|
| 627 |
+
# * longitude_local_coordinates = True: longitude for the receivers will be
|
| 628 |
+
# 0, that is the y coordinate will be 0.
|
| 629 |
+
|
| 630 |
+
# Now we can just subtract.
|
| 631 |
+
# Note we are rotating to a local coordinate system, where the y-z axes are
|
| 632 |
+
# parallel to a tangent plane to the sphere, but still remain in a 3d space.
|
| 633 |
+
# Note that if both `latitude_local_coordinates` and
|
| 634 |
+
# `longitude_local_coordinates` are True, and edges are short,
|
| 635 |
+
# then the difference in x coordinate between sender and receiver
|
| 636 |
+
# should be small, so we could consider dropping the new x coordinate if
|
| 637 |
+
# we wanted to the tangent plane, however in doing so
|
| 638 |
+
# we would lose information about the curvature of the mesh, which may be
|
| 639 |
+
# important for very coarse meshes.
|
| 640 |
+
return sender_pos_in_in_rotated_space - receiver_pos_in_rotated_space
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
def variable_to_stacked(
|
| 644 |
+
variable: xarray.Variable,
|
| 645 |
+
sizes: Mapping[str, int],
|
| 646 |
+
preserved_dims: Tuple[str, ...] = ("batch", "lat", "lon"),
|
| 647 |
+
) -> xarray.Variable:
|
| 648 |
+
"""Converts an xarray.Variable to preserved_dims + ("channels",).
|
| 649 |
+
|
| 650 |
+
Any dimensions other than those included in preserved_dims get stacked into a
|
| 651 |
+
final "channels" dimension. If any of the preserved_dims are missing then they
|
| 652 |
+
are added, with the data broadcast/tiled to match the sizes specified in
|
| 653 |
+
`sizes`.
|
| 654 |
+
|
| 655 |
+
Args:
|
| 656 |
+
variable: An xarray.Variable.
|
| 657 |
+
sizes: Mapping including sizes for any dimensions which are not present in
|
| 658 |
+
`variable` but are needed for the output. This may be needed for example
|
| 659 |
+
for a static variable with only ("lat", "lon") dims, or if you want to
|
| 660 |
+
encode just the latitude coordinates (a variable with dims ("lat",)).
|
| 661 |
+
preserved_dims: dimensions of variable to not be folded in channels.
|
| 662 |
+
|
| 663 |
+
Returns:
|
| 664 |
+
An xarray.Variable with dimensions preserved_dims + ("channels",).
|
| 665 |
+
"""
|
| 666 |
+
stack_to_channels_dims = [
|
| 667 |
+
d for d in variable.dims if d not in preserved_dims]
|
| 668 |
+
if stack_to_channels_dims:
|
| 669 |
+
variable = variable.stack(channels=stack_to_channels_dims)
|
| 670 |
+
dims = {dim: variable.sizes.get(dim) or sizes[dim] for dim in preserved_dims}
|
| 671 |
+
dims["channels"] = variable.sizes.get("channels", 1)
|
| 672 |
+
return variable.set_dims(dims)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def dataset_to_stacked(
|
| 676 |
+
dataset: xarray.Dataset,
|
| 677 |
+
sizes: Optional[Mapping[str, int]] = None,
|
| 678 |
+
preserved_dims: Tuple[str, ...] = ("batch", "lat", "lon"),
|
| 679 |
+
) -> xarray.DataArray:
|
| 680 |
+
"""Converts an xarray.Dataset to a single stacked array.
|
| 681 |
+
|
| 682 |
+
This takes each consistuent data_var, converts it into BHWC layout
|
| 683 |
+
using `variable_to_stacked`, then concats them all along the channels axis.
|
| 684 |
+
|
| 685 |
+
Args:
|
| 686 |
+
dataset: An xarray.Dataset.
|
| 687 |
+
sizes: Mapping including sizes for any dimensions which are not present in
|
| 688 |
+
the `dataset` but are needed for the output. See variable_to_stacked.
|
| 689 |
+
preserved_dims: dimensions from the dataset that should not be folded in
|
| 690 |
+
the predictions channels.
|
| 691 |
+
|
| 692 |
+
Returns:
|
| 693 |
+
An xarray.DataArray with dimensions preserved_dims + ("channels",).
|
| 694 |
+
Existing coordinates for preserved_dims axes will be preserved, however
|
| 695 |
+
there will be no coordinates for "channels".
|
| 696 |
+
"""
|
| 697 |
+
data_vars = [
|
| 698 |
+
variable_to_stacked(dataset.variables[name], sizes or dataset.sizes,
|
| 699 |
+
preserved_dims)
|
| 700 |
+
for name in sorted(dataset.data_vars.keys())
|
| 701 |
+
]
|
| 702 |
+
coords = {
|
| 703 |
+
dim: coord
|
| 704 |
+
for dim, coord in dataset.coords.items()
|
| 705 |
+
if dim in preserved_dims
|
| 706 |
+
}
|
| 707 |
+
return xarray.DataArray(
|
| 708 |
+
data=xarray.Variable.concat(data_vars, dim="channels"), coords=coords)
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
def stacked_to_dataset(
|
| 712 |
+
stacked_array: xarray.Variable,
|
| 713 |
+
template_dataset: xarray.Dataset,
|
| 714 |
+
preserved_dims: Tuple[str, ...] = ("batch", "lat", "lon"),
|
| 715 |
+
) -> xarray.Dataset:
|
| 716 |
+
"""The inverse of dataset_to_stacked.
|
| 717 |
+
|
| 718 |
+
Requires a template dataset to demonstrate the variables/shapes/coordinates
|
| 719 |
+
required.
|
| 720 |
+
All variables must have preserved_dims dimensions.
|
| 721 |
+
|
| 722 |
+
Args:
|
| 723 |
+
stacked_array: Data in BHWC layout, encoded the same as dataset_to_stacked
|
| 724 |
+
would if it was asked to encode `template_dataset`.
|
| 725 |
+
template_dataset: A template Dataset (or other mapping of DataArrays)
|
| 726 |
+
demonstrating the shape of output required (variables, shapes,
|
| 727 |
+
coordinates etc).
|
| 728 |
+
preserved_dims: dimensions from the target_template that were not folded in
|
| 729 |
+
the predictions channels. The preserved_dims need to be a subset of the
|
| 730 |
+
dims of all the variables of template_dataset.
|
| 731 |
+
|
| 732 |
+
Returns:
|
| 733 |
+
An xarray.Dataset (or other mapping of DataArrays) with the same shape and
|
| 734 |
+
type as template_dataset.
|
| 735 |
+
"""
|
| 736 |
+
unstack_from_channels_sizes = {}
|
| 737 |
+
var_names = sorted(template_dataset.keys())
|
| 738 |
+
for name in var_names:
|
| 739 |
+
template_var = template_dataset[name]
|
| 740 |
+
if not all(dim in template_var.dims for dim in preserved_dims):
|
| 741 |
+
raise ValueError(
|
| 742 |
+
f"stacked_to_dataset requires all Variables to have {preserved_dims} "
|
| 743 |
+
f"dimensions, but found only {template_var.dims}.")
|
| 744 |
+
unstack_from_channels_sizes[name] = {
|
| 745 |
+
dim: size for dim, size in template_var.sizes.items()
|
| 746 |
+
if dim not in preserved_dims}
|
| 747 |
+
|
| 748 |
+
channels = {name: np.prod(list(unstack_sizes.values()), dtype=np.int64)
|
| 749 |
+
for name, unstack_sizes in unstack_from_channels_sizes.items()}
|
| 750 |
+
total_expected_channels = sum(channels.values())
|
| 751 |
+
found_channels = stacked_array.sizes["channels"]
|
| 752 |
+
if total_expected_channels != found_channels:
|
| 753 |
+
raise ValueError(
|
| 754 |
+
f"Expected {total_expected_channels} channels but found "
|
| 755 |
+
f"{found_channels}, when trying to convert a stacked array of shape "
|
| 756 |
+
f"{stacked_array.sizes} to a dataset of shape {template_dataset}.")
|
| 757 |
+
|
| 758 |
+
data_vars = {}
|
| 759 |
+
index = 0
|
| 760 |
+
for name in var_names:
|
| 761 |
+
template_var = template_dataset[name]
|
| 762 |
+
var = stacked_array.isel({"channels": slice(index, index + channels[name])})
|
| 763 |
+
index += channels[name]
|
| 764 |
+
var = var.unstack({"channels": unstack_from_channels_sizes[name]})
|
| 765 |
+
var = var.transpose(*template_var.dims)
|
| 766 |
+
data_vars[name] = xarray.DataArray(
|
| 767 |
+
data=var,
|
| 768 |
+
coords=template_var.coords,
|
| 769 |
+
# This might not always be the same as the name it's keyed under; it
|
| 770 |
+
# will refer to the original variable name, whereas the key might be
|
| 771 |
+
# some alias e.g. temperature_850 under which it should be logged:
|
| 772 |
+
name=template_var.name,
|
| 773 |
+
)
|
| 774 |
+
return type(template_dataset)(data_vars) # pytype:disable=not-callable,wrong-arg-count
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def fourier_features(
|
| 778 |
+
values: jnp.ndarray,
|
| 779 |
+
base_period: float,
|
| 780 |
+
num_frequencies: int,
|
| 781 |
+
) -> jnp.ndarray:
|
| 782 |
+
"""Maps values to sin/cos features for a range of frequencies.
|
| 783 |
+
|
| 784 |
+
Args:
|
| 785 |
+
values: Values to compute Fourier features for.
|
| 786 |
+
base_period: The base period to use. This should be greater or equal to the
|
| 787 |
+
range of the values, or to the period if the values have periodic
|
| 788 |
+
semantics (e.g. 2pi if they represent angles). Frequencies used will be
|
| 789 |
+
integer multiples of 1/base_period.
|
| 790 |
+
num_frequencies: The number of frequencies to use, we will use integer
|
| 791 |
+
multiples of 1/base_period from 1 up to num_frequencies inclusive. (We
|
| 792 |
+
don't include a zero frequency as this would just give constant features
|
| 793 |
+
which are redundant if a bias term is present).
|
| 794 |
+
|
| 795 |
+
Returns:
|
| 796 |
+
Array with same shape as values except with an extra trailing dimension
|
| 797 |
+
of size 2*num_frequencies, which contains a sin and a cos feature for each
|
| 798 |
+
frequency.
|
| 799 |
+
"""
|
| 800 |
+
frequencies = np.arange(1, num_frequencies + 1) / base_period
|
| 801 |
+
angular_frequencies = jnp.array(2 * np.pi * frequencies, dtype=values.dtype)
|
| 802 |
+
values_times_angular_freqs = values[..., None] * angular_frequencies
|
| 803 |
+
return jnp.concatenate(
|
| 804 |
+
[jnp.cos(values_times_angular_freqs),
|
| 805 |
+
jnp.sin(values_times_angular_freqs)],
|
| 806 |
+
axis=-1)
|
| 807 |
+
|
model/graphcast/nan_cleaning.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Wrappers for Predictors which allow them to work with data cleaned of NaNs.
|
| 15 |
+
|
| 16 |
+
The Predictor which is wrapped sees inputs and targets without NaNs, and makes
|
| 17 |
+
NaNless predictions.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from typing import Optional, Tuple
|
| 21 |
+
|
| 22 |
+
from . import predictor_base as base
|
| 23 |
+
import numpy as np
|
| 24 |
+
import xarray
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class NaNCleaner(base.Predictor):
|
| 28 |
+
"""A predictor wrapper than removes NaNs from ingested data.
|
| 29 |
+
|
| 30 |
+
The Predictor which is wrapped sees inputs and targets without NaNs.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def __init__(
|
| 34 |
+
self,
|
| 35 |
+
predictor: base.Predictor,
|
| 36 |
+
var_to_clean: str,
|
| 37 |
+
fill_value: xarray.Dataset,
|
| 38 |
+
reintroduce_nans: bool = False,
|
| 39 |
+
):
|
| 40 |
+
"""Initializes the NaNCleaner."""
|
| 41 |
+
self._predictor = predictor
|
| 42 |
+
self._fill_value = fill_value[var_to_clean]
|
| 43 |
+
self._var_to_clean = var_to_clean
|
| 44 |
+
self._reintroduce_nans = reintroduce_nans
|
| 45 |
+
|
| 46 |
+
def _clean(self, dataset: xarray.Dataset) -> xarray.Dataset:
|
| 47 |
+
"""Cleans the dataset of NaNs."""
|
| 48 |
+
data_array = dataset[self._var_to_clean]
|
| 49 |
+
dataset = dataset.assign(
|
| 50 |
+
{self._var_to_clean: data_array.fillna(self._fill_value)}
|
| 51 |
+
)
|
| 52 |
+
return dataset
|
| 53 |
+
|
| 54 |
+
def _maybe_reintroduce_nans(
|
| 55 |
+
self, stale_inputs: xarray.Dataset, predictions: xarray.Dataset
|
| 56 |
+
) -> xarray.Dataset:
|
| 57 |
+
# NaN positions don't change between input frames, if they do then
|
| 58 |
+
# we should be more careful about re-introducing them.
|
| 59 |
+
if self._var_to_clean in predictions.keys():
|
| 60 |
+
nan_mask = np.isnan(stale_inputs[self._var_to_clean]).any(dim='time')
|
| 61 |
+
with_nan_values = predictions[self._var_to_clean].where(~nan_mask, np.nan)
|
| 62 |
+
predictions = predictions.assign({self._var_to_clean: with_nan_values})
|
| 63 |
+
return predictions
|
| 64 |
+
|
| 65 |
+
def __call__(
|
| 66 |
+
self,
|
| 67 |
+
inputs: xarray.Dataset,
|
| 68 |
+
targets_template: xarray.Dataset,
|
| 69 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 70 |
+
**kwargs,
|
| 71 |
+
) -> xarray.Dataset:
|
| 72 |
+
if self._reintroduce_nans:
|
| 73 |
+
# Copy inputs before cleaning so that we can reintroduce NaNs later.
|
| 74 |
+
original_inputs = inputs.copy()
|
| 75 |
+
if self._var_to_clean in inputs.keys():
|
| 76 |
+
inputs = self._clean(inputs)
|
| 77 |
+
if forcings and self._var_to_clean in forcings.keys():
|
| 78 |
+
forcings = self._clean(forcings)
|
| 79 |
+
predictions = self._predictor(
|
| 80 |
+
inputs, targets_template, forcings, **kwargs
|
| 81 |
+
)
|
| 82 |
+
if self._reintroduce_nans:
|
| 83 |
+
predictions = self._maybe_reintroduce_nans(original_inputs, predictions)
|
| 84 |
+
return predictions
|
| 85 |
+
|
| 86 |
+
def loss(
|
| 87 |
+
self,
|
| 88 |
+
inputs: xarray.Dataset,
|
| 89 |
+
targets: xarray.Dataset,
|
| 90 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 91 |
+
**kwargs,
|
| 92 |
+
) -> base.LossAndDiagnostics:
|
| 93 |
+
if self._var_to_clean in inputs.keys():
|
| 94 |
+
inputs = self._clean(inputs)
|
| 95 |
+
if self._var_to_clean in targets.keys():
|
| 96 |
+
targets = self._clean(targets)
|
| 97 |
+
if forcings and self._var_to_clean in forcings.keys():
|
| 98 |
+
forcings = self._clean(forcings)
|
| 99 |
+
return self._predictor.loss(
|
| 100 |
+
inputs, targets, forcings, **kwargs
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def loss_and_predictions(
|
| 104 |
+
self,
|
| 105 |
+
inputs: xarray.Dataset,
|
| 106 |
+
targets: xarray.Dataset,
|
| 107 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 108 |
+
**kwargs,
|
| 109 |
+
) -> Tuple[base.LossAndDiagnostics, xarray.Dataset]:
|
| 110 |
+
if self._reintroduce_nans:
|
| 111 |
+
# Copy inputs before cleaning so that we can reintroduce NaNs later.
|
| 112 |
+
original_inputs = inputs.copy()
|
| 113 |
+
if self._var_to_clean in inputs.keys():
|
| 114 |
+
inputs = self._clean(inputs)
|
| 115 |
+
if self._var_to_clean in targets.keys():
|
| 116 |
+
targets = self._clean(targets)
|
| 117 |
+
if forcings and self._var_to_clean in forcings.keys():
|
| 118 |
+
forcings = self._clean(forcings)
|
| 119 |
+
|
| 120 |
+
loss, predictions = self._predictor.loss_and_predictions(
|
| 121 |
+
inputs, targets, forcings, **kwargs
|
| 122 |
+
)
|
| 123 |
+
if self._reintroduce_nans:
|
| 124 |
+
predictions = self._maybe_reintroduce_nans(original_inputs, predictions)
|
| 125 |
+
return loss, predictions
|
model/graphcast/normalization.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Wrappers for Predictors which allow them to work with normalized data.
|
| 15 |
+
|
| 16 |
+
The Predictor which is wrapped sees normalized inputs and targets, and makes
|
| 17 |
+
normalized predictions. The wrapper handles translating the predictions back
|
| 18 |
+
to the original domain.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import logging
|
| 22 |
+
from typing import Optional, Tuple
|
| 23 |
+
|
| 24 |
+
from . import predictor_base
|
| 25 |
+
from . import xarray_tree
|
| 26 |
+
import xarray
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def normalize(values: xarray.Dataset,
|
| 30 |
+
scales: xarray.Dataset,
|
| 31 |
+
locations: Optional[xarray.Dataset],
|
| 32 |
+
) -> xarray.Dataset:
|
| 33 |
+
"""Normalize variables using the given scales and (optionally) locations."""
|
| 34 |
+
def normalize_array(array):
|
| 35 |
+
if array.name is None:
|
| 36 |
+
raise ValueError(
|
| 37 |
+
"Can't look up normalization constants because array has no name.")
|
| 38 |
+
if locations is not None:
|
| 39 |
+
if array.name in locations:
|
| 40 |
+
array = array - locations[array.name].astype(array.dtype)
|
| 41 |
+
else:
|
| 42 |
+
logging.warning('No normalization location found for %s', array.name)
|
| 43 |
+
if array.name in scales:
|
| 44 |
+
array = array / scales[array.name].astype(array.dtype)
|
| 45 |
+
else:
|
| 46 |
+
logging.warning('No normalization scale found for %s', array.name)
|
| 47 |
+
return array
|
| 48 |
+
return xarray_tree.map_structure(normalize_array, values)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def unnormalize(values: xarray.Dataset,
|
| 52 |
+
scales: xarray.Dataset,
|
| 53 |
+
locations: Optional[xarray.Dataset],
|
| 54 |
+
) -> xarray.Dataset:
|
| 55 |
+
"""Unnormalize variables using the given scales and (optionally) locations."""
|
| 56 |
+
def unnormalize_array(array):
|
| 57 |
+
if array.name is None:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
"Can't look up normalization constants because array has no name.")
|
| 60 |
+
if array.name in scales:
|
| 61 |
+
array = array * scales[array.name].astype(array.dtype)
|
| 62 |
+
else:
|
| 63 |
+
logging.warning('No normalization scale found for %s', array.name)
|
| 64 |
+
if locations is not None:
|
| 65 |
+
if array.name in locations:
|
| 66 |
+
array = array + locations[array.name].astype(array.dtype)
|
| 67 |
+
else:
|
| 68 |
+
logging.warning('No normalization location found for %s', array.name)
|
| 69 |
+
return array
|
| 70 |
+
return xarray_tree.map_structure(unnormalize_array, values)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class InputsAndResiduals(predictor_base.Predictor):
|
| 74 |
+
"""Wraps with a residual connection, normalizing inputs and target residuals.
|
| 75 |
+
|
| 76 |
+
The inner predictor is given inputs that are normalized using `locations`
|
| 77 |
+
and `scales` to roughly zero-mean unit variance.
|
| 78 |
+
|
| 79 |
+
For target variables that are present in the inputs, the inner predictor is
|
| 80 |
+
trained to predict residuals (target - last_frame_of_input) that have been
|
| 81 |
+
normalized using `residual_scales` (and optionally `residual_locations`) to
|
| 82 |
+
roughly unit variance / zero mean.
|
| 83 |
+
|
| 84 |
+
This replaces `residual.Predictor` in the case where you want normalization
|
| 85 |
+
that's based on the scales of the residuals.
|
| 86 |
+
|
| 87 |
+
Since we return the underlying predictor's loss on the normalized residuals,
|
| 88 |
+
if the underlying predictor is a sum of per-variable losses, the normalization
|
| 89 |
+
will affect the relative weighting of the per-variable loss terms (hopefully
|
| 90 |
+
in a good way).
|
| 91 |
+
|
| 92 |
+
For target variables *not* present in the inputs, the inner predictor is
|
| 93 |
+
trained to predict targets directly, that have been normalized in the same
|
| 94 |
+
way as the inputs.
|
| 95 |
+
|
| 96 |
+
The transforms applied to the targets (the residual connection and the
|
| 97 |
+
normalization) are applied in reverse to the predictions before returning
|
| 98 |
+
them.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(
|
| 102 |
+
self,
|
| 103 |
+
predictor: predictor_base.Predictor,
|
| 104 |
+
stddev_by_level: xarray.Dataset,
|
| 105 |
+
mean_by_level: xarray.Dataset,
|
| 106 |
+
diffs_stddev_by_level: xarray.Dataset):
|
| 107 |
+
self._predictor = predictor
|
| 108 |
+
self._scales = stddev_by_level
|
| 109 |
+
self._locations = mean_by_level
|
| 110 |
+
self._residual_scales = diffs_stddev_by_level
|
| 111 |
+
self._residual_locations = None
|
| 112 |
+
|
| 113 |
+
def _unnormalize_prediction_and_add_input(self, inputs, norm_prediction):
|
| 114 |
+
if norm_prediction.sizes.get('time') != 1:
|
| 115 |
+
raise ValueError(
|
| 116 |
+
'normalization.InputsAndResiduals only supports predicting a '
|
| 117 |
+
'single timestep.')
|
| 118 |
+
if norm_prediction.name in inputs:
|
| 119 |
+
# Residuals are assumed to be predicted as normalized (unit variance),
|
| 120 |
+
# but the scale and location they need mapping to is that of the residuals
|
| 121 |
+
# not of the values themselves.
|
| 122 |
+
prediction = unnormalize(
|
| 123 |
+
norm_prediction, self._residual_scales, self._residual_locations)
|
| 124 |
+
# A prediction for which we have a corresponding input -- we are
|
| 125 |
+
# predicting the residual:
|
| 126 |
+
last_input = inputs[norm_prediction.name].isel(time=-1)
|
| 127 |
+
prediction = prediction + last_input
|
| 128 |
+
return prediction
|
| 129 |
+
else:
|
| 130 |
+
# A predicted variable which is not an input variable. We are predicting
|
| 131 |
+
# it directly, so unnormalize it directly to the target scale/location:
|
| 132 |
+
return unnormalize(norm_prediction, self._scales, self._locations)
|
| 133 |
+
|
| 134 |
+
def _subtract_input_and_normalize_target(self, inputs, target):
|
| 135 |
+
if target.sizes.get('time') != 1:
|
| 136 |
+
raise ValueError(
|
| 137 |
+
'normalization.InputsAndResiduals only supports wrapping predictors'
|
| 138 |
+
'that predict a single timestep.')
|
| 139 |
+
if target.name in inputs:
|
| 140 |
+
target_residual = target
|
| 141 |
+
last_input = inputs[target.name].isel(time=-1)
|
| 142 |
+
target_residual = target_residual - last_input
|
| 143 |
+
return normalize(
|
| 144 |
+
target_residual, self._residual_scales, self._residual_locations)
|
| 145 |
+
else:
|
| 146 |
+
return normalize(target, self._scales, self._locations)
|
| 147 |
+
|
| 148 |
+
def __call__(self,
|
| 149 |
+
inputs: xarray.Dataset,
|
| 150 |
+
targets_template: xarray.Dataset,
|
| 151 |
+
forcings: xarray.Dataset,
|
| 152 |
+
**kwargs
|
| 153 |
+
) -> xarray.Dataset:
|
| 154 |
+
norm_inputs = normalize(inputs, self._scales, self._locations)
|
| 155 |
+
norm_forcings = normalize(forcings, self._scales, self._locations)
|
| 156 |
+
norm_predictions = self._predictor(
|
| 157 |
+
norm_inputs, targets_template, forcings=norm_forcings, **kwargs)
|
| 158 |
+
return xarray_tree.map_structure(
|
| 159 |
+
lambda pred: self._unnormalize_prediction_and_add_input(inputs, pred),
|
| 160 |
+
norm_predictions)
|
| 161 |
+
|
| 162 |
+
def loss(self,
|
| 163 |
+
inputs: xarray.Dataset,
|
| 164 |
+
targets: xarray.Dataset,
|
| 165 |
+
forcings: xarray.Dataset,
|
| 166 |
+
**kwargs,
|
| 167 |
+
) -> predictor_base.LossAndDiagnostics:
|
| 168 |
+
"""Returns the loss computed on normalized inputs and targets."""
|
| 169 |
+
norm_inputs = normalize(inputs, self._scales, self._locations)
|
| 170 |
+
norm_forcings = normalize(forcings, self._scales, self._locations)
|
| 171 |
+
norm_target_residuals = xarray_tree.map_structure(
|
| 172 |
+
lambda t: self._subtract_input_and_normalize_target(inputs, t),
|
| 173 |
+
targets)
|
| 174 |
+
return self._predictor.loss(
|
| 175 |
+
norm_inputs, norm_target_residuals, forcings=norm_forcings, **kwargs)
|
| 176 |
+
|
| 177 |
+
def loss_and_predictions( # pytype: disable=signature-mismatch # jax-ndarray
|
| 178 |
+
self,
|
| 179 |
+
inputs: xarray.Dataset,
|
| 180 |
+
targets: xarray.Dataset,
|
| 181 |
+
forcings: xarray.Dataset,
|
| 182 |
+
**kwargs,
|
| 183 |
+
) -> Tuple[predictor_base.LossAndDiagnostics,
|
| 184 |
+
xarray.Dataset]:
|
| 185 |
+
"""The loss computed on normalized data, with unnormalized predictions."""
|
| 186 |
+
norm_inputs = normalize(inputs, self._scales, self._locations)
|
| 187 |
+
norm_forcings = normalize(forcings, self._scales, self._locations)
|
| 188 |
+
norm_target_residuals = xarray_tree.map_structure(
|
| 189 |
+
lambda t: self._subtract_input_and_normalize_target(inputs, t),
|
| 190 |
+
targets)
|
| 191 |
+
(loss, scalars), norm_predictions = self._predictor.loss_and_predictions(
|
| 192 |
+
norm_inputs, norm_target_residuals, forcings=norm_forcings, **kwargs)
|
| 193 |
+
predictions = xarray_tree.map_structure(
|
| 194 |
+
lambda pred: self._unnormalize_prediction_and_add_input(inputs, pred),
|
| 195 |
+
norm_predictions)
|
| 196 |
+
return (loss, scalars), predictions
|
model/graphcast/predictor_base.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Abstract base classes for an xarray-based Predictor API."""
|
| 15 |
+
|
| 16 |
+
import abc
|
| 17 |
+
|
| 18 |
+
from typing import Tuple
|
| 19 |
+
|
| 20 |
+
from . import losses
|
| 21 |
+
from . import xarray_jax
|
| 22 |
+
import jax.numpy as jnp
|
| 23 |
+
import xarray
|
| 24 |
+
|
| 25 |
+
LossAndDiagnostics = losses.LossAndDiagnostics
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Predictor(abc.ABC):
|
| 29 |
+
"""A possibly-trainable predictor of weather, exposing an xarray-based API.
|
| 30 |
+
|
| 31 |
+
Typically wraps an underlying JAX model and handles translating the xarray
|
| 32 |
+
Dataset values to and from plain JAX arrays that are convenient for input to
|
| 33 |
+
(and output from) the underlying model.
|
| 34 |
+
|
| 35 |
+
Different subclasses may exist to wrap different kinds of underlying model,
|
| 36 |
+
e.g. models taking stacked inputs/outputs, models taking separate 2D and 3D
|
| 37 |
+
inputs/outputs, autoregressive models.
|
| 38 |
+
|
| 39 |
+
You can also implement a specific model directly as a Predictor if you want,
|
| 40 |
+
for example if it has quite specific/unique requirements for its input/output
|
| 41 |
+
or loss function, or if it's convenient to implement directly using xarray.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
@abc.abstractmethod
|
| 45 |
+
def __call__(self,
|
| 46 |
+
inputs: xarray.Dataset,
|
| 47 |
+
targets_template: xarray.Dataset,
|
| 48 |
+
forcings: xarray.Dataset,
|
| 49 |
+
**optional_kwargs
|
| 50 |
+
) -> xarray.Dataset:
|
| 51 |
+
"""Makes predictions.
|
| 52 |
+
|
| 53 |
+
This is only used by the Experiment for inference / evaluation, with
|
| 54 |
+
training going via the .loss method. So it should default to making
|
| 55 |
+
predictions for evaluation, although you can also support making predictions
|
| 56 |
+
for use in the loss via an is_training argument -- see
|
| 57 |
+
LossFunctionPredictor which helps with that.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
inputs: An xarray.Dataset of inputs.
|
| 61 |
+
targets_template: An xarray.Dataset or other mapping of xarray.DataArrays,
|
| 62 |
+
with the same shape as the targets, to demonstrate what kind of
|
| 63 |
+
predictions are required. You can use this to determine which variables,
|
| 64 |
+
levels and lead times must be predicted.
|
| 65 |
+
You are free to raise an error if you don't support predicting what is
|
| 66 |
+
requested.
|
| 67 |
+
forcings: An xarray.Dataset of forcings terms. Forcings are variables
|
| 68 |
+
that can be fed to the model, but do not need to be predicted. This is
|
| 69 |
+
often because this variable can be computed analytically (e.g. the toa
|
| 70 |
+
radiation of the sun is mostly a function of geometry) or are considered
|
| 71 |
+
to be controlled for the experiment (e.g., impose a scenario of C02
|
| 72 |
+
emission into the atmosphere). Unlike `inputs`, the `forcings` can
|
| 73 |
+
include information "from the future", that is, information at target
|
| 74 |
+
times specified in the `targets_template`.
|
| 75 |
+
**optional_kwargs: Implementations may support extra optional kwargs,
|
| 76 |
+
provided they set appropriate defaults for them.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Predictions, as an xarray.Dataset or other mapping of DataArrays which
|
| 80 |
+
is capable of being evaluated against targets with shape given by
|
| 81 |
+
targets_template.
|
| 82 |
+
For probabilistic predictors which can return multiple samples from a
|
| 83 |
+
predictive distribution, these should (by convention) be returned along
|
| 84 |
+
an additional 'sample' dimension.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def loss(self,
|
| 88 |
+
inputs: xarray.Dataset,
|
| 89 |
+
targets: xarray.Dataset,
|
| 90 |
+
forcings: xarray.Dataset,
|
| 91 |
+
**optional_kwargs,
|
| 92 |
+
) -> LossAndDiagnostics:
|
| 93 |
+
"""Computes a training loss, for predictors that are trainable.
|
| 94 |
+
|
| 95 |
+
Why make this the Predictor's responsibility, rather than letting callers
|
| 96 |
+
compute their own loss function using predictions obtained from
|
| 97 |
+
Predictor.__call__?
|
| 98 |
+
|
| 99 |
+
Doing it this way gives Predictors more control over their training setup.
|
| 100 |
+
For example, some predictors may wish to train using different targets to
|
| 101 |
+
the ones they predict at evaluation time -- perhaps different lead times and
|
| 102 |
+
variables, perhaps training to predict transformed versions of targets
|
| 103 |
+
where the transform needs to be inverted at evaluation time, etc.
|
| 104 |
+
|
| 105 |
+
It's also necessary for generative models (VAEs, GANs, ...) where the
|
| 106 |
+
training loss is more complex and isn't expressible as a parameter-free
|
| 107 |
+
function of predictions and targets.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
inputs: An xarray.Dataset.
|
| 111 |
+
targets: An xarray.Dataset or other mapping of xarray.DataArrays. See
|
| 112 |
+
docs on __call__ for an explanation about the targets.
|
| 113 |
+
forcings: xarray.Dataset of forcing terms.
|
| 114 |
+
**optional_kwargs: Implementations may support extra optional kwargs,
|
| 115 |
+
provided they set appropriate defaults for them.
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
loss: A DataArray with dimensions ('batch',) containing losses for each
|
| 119 |
+
element of the batch. These will be averaged to give the final
|
| 120 |
+
loss, locally and across replicas.
|
| 121 |
+
diagnostics: Mapping of additional quantities to log by name alongside the
|
| 122 |
+
loss. These will will typically correspond to terms in the loss. They
|
| 123 |
+
should also have dimensions ('batch',) and will be averaged over the
|
| 124 |
+
batch before logging.
|
| 125 |
+
You need not include the loss itself in this dict; it will be added for
|
| 126 |
+
you.
|
| 127 |
+
"""
|
| 128 |
+
del targets, forcings, optional_kwargs
|
| 129 |
+
batch_size = inputs.sizes['batch']
|
| 130 |
+
dummy_loss = xarray_jax.DataArray(jnp.zeros(batch_size), dims=('batch',))
|
| 131 |
+
return dummy_loss, {} # pytype: disable=bad-return-type
|
| 132 |
+
|
| 133 |
+
def loss_and_predictions(
|
| 134 |
+
self,
|
| 135 |
+
inputs: xarray.Dataset,
|
| 136 |
+
targets: xarray.Dataset,
|
| 137 |
+
forcings: xarray.Dataset,
|
| 138 |
+
**optional_kwargs,
|
| 139 |
+
) -> Tuple[LossAndDiagnostics, xarray.Dataset]:
|
| 140 |
+
"""Like .loss but also returns corresponding predictions.
|
| 141 |
+
|
| 142 |
+
Implementing this is optional as it's not used directly by the Experiment,
|
| 143 |
+
but it is required by autoregressive.Predictor when applying an inner
|
| 144 |
+
Predictor autoregressively at training time; we need a loss at each step but
|
| 145 |
+
also predictions to feed back in for the next step.
|
| 146 |
+
|
| 147 |
+
Note the loss itself may not be directly regressing the predictions towards
|
| 148 |
+
targets, the loss may be computed in terms of transformed predictions and
|
| 149 |
+
targets (or in some other way). For this reason we can't always cleanly
|
| 150 |
+
separate this into step 1: get predictions, step 2: compute loss from them,
|
| 151 |
+
hence the need for this combined method.
|
| 152 |
+
|
| 153 |
+
Args:
|
| 154 |
+
inputs:
|
| 155 |
+
targets:
|
| 156 |
+
forcings:
|
| 157 |
+
**optional_kwargs:
|
| 158 |
+
As for self.loss.
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
(loss, diagnostics)
|
| 162 |
+
As for self.loss
|
| 163 |
+
predictions:
|
| 164 |
+
The predictions which the loss relates to. These should be of the same
|
| 165 |
+
shape as what you would get from
|
| 166 |
+
`self.__call__(inputs, targets_template=targets)`, and should be in the
|
| 167 |
+
same 'domain' as the inputs (i.e. they shouldn't be transformed
|
| 168 |
+
differently to how the predictor expects its inputs).
|
| 169 |
+
"""
|
| 170 |
+
raise NotImplementedError
|
model/graphcast/rollout.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utils for rolling out models."""
|
| 15 |
+
|
| 16 |
+
from typing import Iterator, Optional, Sequence
|
| 17 |
+
|
| 18 |
+
from absl import logging
|
| 19 |
+
import chex
|
| 20 |
+
import dask.array
|
| 21 |
+
from . import xarray_jax
|
| 22 |
+
from . import xarray_tree
|
| 23 |
+
import jax
|
| 24 |
+
import jax.numpy as jnp
|
| 25 |
+
import numpy as np
|
| 26 |
+
import typing_extensions
|
| 27 |
+
import xarray
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _device_put_sharded(data_list, devices, axis_name):
|
| 31 |
+
"""Stack data and put on devices with consistent sharding.
|
| 32 |
+
|
| 33 |
+
Creates a mesh with axis_name to ensure JIT cache consistency with pmap.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
data_list: List of data to stack and put on devices.
|
| 37 |
+
devices: List of devices to put the data on.
|
| 38 |
+
axis_name: Name of the axis to use for sharding.
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Data put on devices with consistent sharding.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
mesh = jax.sharding.Mesh(np.array(devices), (axis_name,))
|
| 45 |
+
sharding = jax.NamedSharding(mesh, jax.P(axis_name))
|
| 46 |
+
stack_fn = (
|
| 47 |
+
jnp.stack
|
| 48 |
+
if all(isinstance(x, jax.Array) for x in data_list)
|
| 49 |
+
else np.stack
|
| 50 |
+
)
|
| 51 |
+
stacked = stack_fn(data_list, axis=0)
|
| 52 |
+
return jax.device_put(stacked, sharding)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class PredictorFn(typing_extensions.Protocol):
|
| 56 |
+
"""Functional version of base.Predictor.__call__ with explicit rng."""
|
| 57 |
+
|
| 58 |
+
def __call__(
|
| 59 |
+
self, rng: chex.PRNGKey, inputs: xarray.Dataset,
|
| 60 |
+
targets_template: xarray.Dataset,
|
| 61 |
+
forcings: xarray.Dataset,
|
| 62 |
+
**optional_kwargs,
|
| 63 |
+
) -> xarray.Dataset:
|
| 64 |
+
...
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _replicate_dataset(
|
| 68 |
+
data: xarray.Dataset, replica_dim: str,
|
| 69 |
+
replicate_to_device: bool,
|
| 70 |
+
devices: Sequence[jax.Device],
|
| 71 |
+
) -> xarray.Dataset:
|
| 72 |
+
"""Used to prepare for xarray_jax.pmap."""
|
| 73 |
+
|
| 74 |
+
def replicate_variable(variable: xarray.Variable) -> xarray.Variable:
|
| 75 |
+
if replica_dim in variable.dims:
|
| 76 |
+
# TODO(pricei): call device_put_replicated when replicate_to_device==True
|
| 77 |
+
return variable.transpose(replica_dim, ...)
|
| 78 |
+
else:
|
| 79 |
+
data = len(devices) * [variable.data]
|
| 80 |
+
if replicate_to_device:
|
| 81 |
+
assert devices is not None
|
| 82 |
+
data = _device_put_sharded(data, devices, replica_dim)
|
| 83 |
+
else:
|
| 84 |
+
data = np.stack(data, axis=0)
|
| 85 |
+
return xarray_jax.Variable(
|
| 86 |
+
data=data, dims=(replica_dim,) + variable.dims, attrs=variable.attrs
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
def replicate_dataset(dataset: xarray.Dataset) -> xarray.Dataset:
|
| 90 |
+
if dataset is None:
|
| 91 |
+
return None
|
| 92 |
+
data_variables = {
|
| 93 |
+
name: replicate_variable(var)
|
| 94 |
+
for name, var in dataset.data_vars.variables.items()
|
| 95 |
+
}
|
| 96 |
+
coords = {name: coord.variable for name, coord in dataset.coords.items()}
|
| 97 |
+
return xarray.Dataset(data_variables, coords=coords, attrs=dataset.attrs)
|
| 98 |
+
|
| 99 |
+
return replicate_dataset(data)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def chunked_prediction_generator_multiple_runs(
|
| 103 |
+
predictor_fn: PredictorFn,
|
| 104 |
+
rngs: chex.PRNGKey,
|
| 105 |
+
inputs: xarray.Dataset,
|
| 106 |
+
targets_template: xarray.Dataset,
|
| 107 |
+
forcings: Optional[xarray.Dataset],
|
| 108 |
+
num_samples: Optional[int],
|
| 109 |
+
pmap_devices: Optional[Sequence[jax.Device]] = None,
|
| 110 |
+
**chunked_prediction_kwargs,
|
| 111 |
+
) -> Iterator[xarray.Dataset]:
|
| 112 |
+
"""Outputs a trajectory of multiple samples by yielding chunked predictions.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
predictor_fn: Function to use to make predictions for each chunk.
|
| 116 |
+
rngs: RNG sequence to be used for each ensemble member.
|
| 117 |
+
inputs: Inputs for the model.
|
| 118 |
+
targets_template: Template for the target prediction, requires targets
|
| 119 |
+
equispaced in time.
|
| 120 |
+
forcings: Optional forcing for the model.
|
| 121 |
+
num_samples: The number of runs / samples to rollout.
|
| 122 |
+
pmap_devices: List of devices over which predictor_fn is pmapped, or None if
|
| 123 |
+
it is not pmapped.
|
| 124 |
+
**chunked_prediction_kwargs:
|
| 125 |
+
See chunked_prediction, some of these are required arguments.
|
| 126 |
+
|
| 127 |
+
Yields:
|
| 128 |
+
The predictions for each chunked step of the chunked rollout, such that
|
| 129 |
+
if all predictions are concatenated in time and sample dimension squeezed,
|
| 130 |
+
this would match the targets template in structure.
|
| 131 |
+
|
| 132 |
+
"""
|
| 133 |
+
if pmap_devices is not None:
|
| 134 |
+
assert (
|
| 135 |
+
num_samples % len(pmap_devices) == 0
|
| 136 |
+
), "num_samples must be a multiple of len(pmap_devices)"
|
| 137 |
+
|
| 138 |
+
def predictor_fn_pmap_named_args(rng, inputs, targets_template, forcings):
|
| 139 |
+
targets_template = _replicate_dataset(
|
| 140 |
+
targets_template,
|
| 141 |
+
replica_dim="sample",
|
| 142 |
+
replicate_to_device=True,
|
| 143 |
+
devices=pmap_devices,
|
| 144 |
+
)
|
| 145 |
+
return predictor_fn(rng, inputs, targets_template, forcings)
|
| 146 |
+
|
| 147 |
+
for i in range(0, num_samples, len(pmap_devices)):
|
| 148 |
+
sample_idx = slice(i, i + len(pmap_devices))
|
| 149 |
+
logging.info("Samples %s out of %s", sample_idx, num_samples)
|
| 150 |
+
logging.flush()
|
| 151 |
+
sample_group_rngs = _device_put_sharded(
|
| 152 |
+
rngs[sample_idx], pmap_devices, "sample")
|
| 153 |
+
|
| 154 |
+
if "sample" not in inputs.dims:
|
| 155 |
+
sample_inputs = inputs
|
| 156 |
+
else:
|
| 157 |
+
sample_inputs = inputs.isel(sample=sample_idx, drop=True)
|
| 158 |
+
|
| 159 |
+
sample_inputs = _replicate_dataset(
|
| 160 |
+
sample_inputs,
|
| 161 |
+
replica_dim="sample",
|
| 162 |
+
replicate_to_device=True,
|
| 163 |
+
devices=pmap_devices,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
if forcings is not None:
|
| 167 |
+
if "sample" not in forcings.dims:
|
| 168 |
+
sample_forcings = forcings
|
| 169 |
+
else:
|
| 170 |
+
sample_forcings = forcings.isel(sample=sample_idx, drop=True)
|
| 171 |
+
|
| 172 |
+
# TODO(pricei): We are replicating the full forcings for all rollout
|
| 173 |
+
# timesteps here, rather than inside `predictor_fn_pmap_named_args` like
|
| 174 |
+
# the targets_template above, because the forcings are concatenated with
|
| 175 |
+
# the inputs which will already be replicated. We should refactor this
|
| 176 |
+
# so that chunked prediction is aware of whether it is being run with
|
| 177 |
+
# pmap, and if so do the replication and device_put only of the
|
| 178 |
+
# necessary timesteps, as part of the chunked prediction function.
|
| 179 |
+
sample_forcings = _replicate_dataset(
|
| 180 |
+
sample_forcings,
|
| 181 |
+
replica_dim="sample",
|
| 182 |
+
replicate_to_device=False,
|
| 183 |
+
devices=pmap_devices,
|
| 184 |
+
)
|
| 185 |
+
else:
|
| 186 |
+
sample_forcings = None
|
| 187 |
+
|
| 188 |
+
for prediction_chunk in chunked_prediction_generator(
|
| 189 |
+
predictor_fn=predictor_fn_pmap_named_args,
|
| 190 |
+
rng=sample_group_rngs,
|
| 191 |
+
inputs=sample_inputs,
|
| 192 |
+
targets_template=targets_template,
|
| 193 |
+
forcings=sample_forcings,
|
| 194 |
+
pmap_devices=pmap_devices,
|
| 195 |
+
replica_axis="sample",
|
| 196 |
+
**chunked_prediction_kwargs,
|
| 197 |
+
):
|
| 198 |
+
prediction_chunk.coords["sample"] = np.arange(
|
| 199 |
+
sample_idx.start, sample_idx.stop, sample_idx.step
|
| 200 |
+
)
|
| 201 |
+
yield prediction_chunk
|
| 202 |
+
del prediction_chunk
|
| 203 |
+
else:
|
| 204 |
+
for i in range(num_samples):
|
| 205 |
+
logging.info("Sample %d/%d", i, num_samples)
|
| 206 |
+
logging.flush()
|
| 207 |
+
this_sample_rng = rngs[i]
|
| 208 |
+
|
| 209 |
+
if "sample" in inputs.dims:
|
| 210 |
+
sample_inputs = inputs.isel(sample=i, drop=True)
|
| 211 |
+
else:
|
| 212 |
+
sample_inputs = inputs
|
| 213 |
+
|
| 214 |
+
sample_forcings = forcings
|
| 215 |
+
if sample_forcings is not None:
|
| 216 |
+
if "sample" in sample_forcings.dims:
|
| 217 |
+
sample_forcings = sample_forcings.isel(sample=i, drop=True)
|
| 218 |
+
|
| 219 |
+
for prediction_chunk in chunked_prediction_generator(
|
| 220 |
+
predictor_fn=predictor_fn,
|
| 221 |
+
rng=this_sample_rng,
|
| 222 |
+
inputs=sample_inputs,
|
| 223 |
+
targets_template=targets_template,
|
| 224 |
+
forcings=sample_forcings,
|
| 225 |
+
**chunked_prediction_kwargs):
|
| 226 |
+
prediction_chunk.coords["sample"] = i
|
| 227 |
+
yield prediction_chunk
|
| 228 |
+
del prediction_chunk
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def chunked_prediction(
|
| 232 |
+
predictor_fn: PredictorFn,
|
| 233 |
+
rng: chex.PRNGKey,
|
| 234 |
+
inputs: xarray.Dataset,
|
| 235 |
+
targets_template: xarray.Dataset,
|
| 236 |
+
forcings: xarray.Dataset,
|
| 237 |
+
num_steps_per_chunk: int = 1,
|
| 238 |
+
verbose: bool = False,
|
| 239 |
+
) -> xarray.Dataset:
|
| 240 |
+
"""Outputs a long trajectory by iteratively concatenating chunked predictions.
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
predictor_fn: Function to use to make predictions for each chunk.
|
| 244 |
+
rng: Random key.
|
| 245 |
+
inputs: Inputs for the model.
|
| 246 |
+
targets_template: Template for the target prediction, requires targets
|
| 247 |
+
equispaced in time.
|
| 248 |
+
forcings: Optional forcing for the model.
|
| 249 |
+
num_steps_per_chunk: How many of the steps in `targets_template` to predict
|
| 250 |
+
at each call of `predictor_fn`. It must evenly divide the number of
|
| 251 |
+
steps in `targets_template`.
|
| 252 |
+
verbose: Whether to log the current chunk being predicted.
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
Predictions for the targets template.
|
| 256 |
+
|
| 257 |
+
"""
|
| 258 |
+
chunks_list = []
|
| 259 |
+
for prediction_chunk in chunked_prediction_generator(
|
| 260 |
+
predictor_fn=predictor_fn,
|
| 261 |
+
rng=rng,
|
| 262 |
+
inputs=inputs,
|
| 263 |
+
targets_template=targets_template,
|
| 264 |
+
forcings=forcings,
|
| 265 |
+
num_steps_per_chunk=num_steps_per_chunk,
|
| 266 |
+
verbose=verbose,
|
| 267 |
+
):
|
| 268 |
+
chunks_list.append(jax.device_get(prediction_chunk))
|
| 269 |
+
return xarray.concat(chunks_list, dim="time")
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def chunked_prediction_generator(
|
| 273 |
+
predictor_fn: PredictorFn,
|
| 274 |
+
rng: chex.PRNGKey,
|
| 275 |
+
inputs: xarray.Dataset,
|
| 276 |
+
targets_template: xarray.Dataset,
|
| 277 |
+
forcings: xarray.Dataset,
|
| 278 |
+
num_steps_per_chunk: int = 1,
|
| 279 |
+
verbose: bool = False,
|
| 280 |
+
pmap_devices: Sequence[jax.Device] | None = None,
|
| 281 |
+
replica_axis: str | None = None,
|
| 282 |
+
) -> Iterator[xarray.Dataset]:
|
| 283 |
+
"""Outputs a long trajectory by yielding chunked predictions.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
predictor_fn: Function to use to make predictions for each chunk.
|
| 287 |
+
rng: Random key.
|
| 288 |
+
inputs: Inputs for the model.
|
| 289 |
+
targets_template: Template for the target prediction, requires targets
|
| 290 |
+
equispaced in time.
|
| 291 |
+
forcings: Optional forcing for the model.
|
| 292 |
+
num_steps_per_chunk: How many of the steps in `targets_template` to predict
|
| 293 |
+
at each call of `predictor_fn`. It must evenly divide the number of
|
| 294 |
+
steps in `targets_template`.
|
| 295 |
+
verbose: Whether to log the current chunk being predicted.
|
| 296 |
+
pmap_devices: List of devices over which predictor_fn is pmapped, or None if
|
| 297 |
+
it is not pmapped.
|
| 298 |
+
replica_axis: Dimension name to use for the replicas.
|
| 299 |
+
|
| 300 |
+
Yields:
|
| 301 |
+
The predictions for each chunked step of the chunked rollout, such as
|
| 302 |
+
if all predictions are concatenated in time this would match the targets
|
| 303 |
+
template in structure.
|
| 304 |
+
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
if pmap_devices is not None and replica_axis is None:
|
| 308 |
+
raise ValueError("Must provide replica_axis when pmap_devices is provided.")
|
| 309 |
+
|
| 310 |
+
# Create copies to avoid mutating inputs.
|
| 311 |
+
inputs = inputs.copy()
|
| 312 |
+
targets_template = targets_template.copy()
|
| 313 |
+
forcings = forcings.copy()
|
| 314 |
+
|
| 315 |
+
if "datetime" in inputs.coords:
|
| 316 |
+
del inputs.coords["datetime"]
|
| 317 |
+
|
| 318 |
+
if "datetime" in targets_template.coords:
|
| 319 |
+
output_datetime = targets_template.coords["datetime"]
|
| 320 |
+
del targets_template.coords["datetime"]
|
| 321 |
+
else:
|
| 322 |
+
output_datetime = None
|
| 323 |
+
|
| 324 |
+
if "datetime" in forcings.coords:
|
| 325 |
+
del forcings.coords["datetime"]
|
| 326 |
+
|
| 327 |
+
num_target_steps = targets_template.dims["time"]
|
| 328 |
+
num_chunks, remainder = divmod(num_target_steps, num_steps_per_chunk)
|
| 329 |
+
if remainder != 0:
|
| 330 |
+
raise ValueError(
|
| 331 |
+
f"The number of steps per chunk {num_steps_per_chunk} must "
|
| 332 |
+
f"evenly divide the number of target steps {num_target_steps} ")
|
| 333 |
+
|
| 334 |
+
if len(np.unique(np.diff(targets_template.coords["time"].data))) > 1:
|
| 335 |
+
raise ValueError("The targets time coordinates must be evenly spaced")
|
| 336 |
+
|
| 337 |
+
# Our template targets will always have a time axis corresponding for the
|
| 338 |
+
# timedeltas for the first chunk.
|
| 339 |
+
targets_chunk_time = targets_template.time.isel(
|
| 340 |
+
time=slice(0, num_steps_per_chunk))
|
| 341 |
+
|
| 342 |
+
current_inputs = inputs
|
| 343 |
+
|
| 344 |
+
def split_rng_fn(rng):
|
| 345 |
+
# Note, this is *not* equivalent to `return jax.random.split(rng)`, because
|
| 346 |
+
# by assigning to a tuple, the single numpy array returned by
|
| 347 |
+
# `jax.random.split` actually gets split into two arrays, so when calling
|
| 348 |
+
# the function with pmap the output is Tuple[Array, Array], where the
|
| 349 |
+
# leading axis of each array is `num devices`.
|
| 350 |
+
rng1, rng2 = jax.random.split(rng)
|
| 351 |
+
return rng1, rng2
|
| 352 |
+
|
| 353 |
+
if pmap_devices is not None:
|
| 354 |
+
split_rng_fn = jax.pmap(
|
| 355 |
+
split_rng_fn, devices=pmap_devices, axis_name=replica_axis
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
for chunk_index in range(num_chunks):
|
| 359 |
+
if verbose:
|
| 360 |
+
logging.info("Chunk %d/%d", chunk_index, num_chunks)
|
| 361 |
+
logging.flush()
|
| 362 |
+
|
| 363 |
+
# Select targets for the time period that we are predicting for this chunk.
|
| 364 |
+
target_offset = num_steps_per_chunk * chunk_index
|
| 365 |
+
target_slice = slice(target_offset, target_offset + num_steps_per_chunk)
|
| 366 |
+
current_targets_template = targets_template.isel(time=target_slice)
|
| 367 |
+
|
| 368 |
+
# Replace the timedelta, by the one corresponding to the first chunk, so we
|
| 369 |
+
# don't recompile at every iteration, keeping the
|
| 370 |
+
actual_target_time = current_targets_template.coords["time"]
|
| 371 |
+
current_targets_template = current_targets_template.assign_coords(
|
| 372 |
+
time=targets_chunk_time).compute()
|
| 373 |
+
|
| 374 |
+
current_forcings = forcings.isel(time=target_slice)
|
| 375 |
+
current_forcings = current_forcings.assign_coords(time=targets_chunk_time)
|
| 376 |
+
current_forcings = current_forcings.compute()
|
| 377 |
+
# Make predictions for the chunk.
|
| 378 |
+
rng, this_rng = split_rng_fn(rng)
|
| 379 |
+
predictions = predictor_fn(
|
| 380 |
+
rng=this_rng,
|
| 381 |
+
inputs=current_inputs,
|
| 382 |
+
targets_template=current_targets_template,
|
| 383 |
+
forcings=current_forcings)
|
| 384 |
+
|
| 385 |
+
# In the pmapped case, profiling reveals that the predictions, forcings and
|
| 386 |
+
# inputs are all copied onto a single TPU, causing OOM. To avoid this
|
| 387 |
+
# we pull all of the input/output data off the devices. This will have
|
| 388 |
+
# some performance impact, but maximise the memory efficiency.
|
| 389 |
+
# TODO(aelkadi): Pmap `_get_next_inputs` when running under pmap, and
|
| 390 |
+
# remove the device_get.
|
| 391 |
+
if pmap_devices is not None:
|
| 392 |
+
predictions = jax.device_get(predictions)
|
| 393 |
+
current_forcings = jax.device_get(current_forcings)
|
| 394 |
+
current_inputs = jax.device_get(current_inputs)
|
| 395 |
+
|
| 396 |
+
if chunk_index == num_chunks - 1:
|
| 397 |
+
# No need to call `_get_next_inputs` on the last iteration.
|
| 398 |
+
current_inputs = None
|
| 399 |
+
else:
|
| 400 |
+
next_frame = xarray.merge([predictions, current_forcings])
|
| 401 |
+
next_inputs = _get_next_inputs(current_inputs, next_frame)
|
| 402 |
+
# Shift timedelta coordinates, so we don't recompile at every iteration.
|
| 403 |
+
next_inputs = next_inputs.assign_coords(
|
| 404 |
+
time=current_inputs.coords["time"])
|
| 405 |
+
current_inputs = next_inputs
|
| 406 |
+
|
| 407 |
+
# At this point we can assign the actual targets time coordinates.
|
| 408 |
+
predictions = predictions.assign_coords(time=actual_target_time)
|
| 409 |
+
if output_datetime is not None:
|
| 410 |
+
predictions.coords["datetime"] = output_datetime.isel(
|
| 411 |
+
time=target_slice)
|
| 412 |
+
yield predictions
|
| 413 |
+
del predictions
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _get_next_inputs(
|
| 417 |
+
prev_inputs: xarray.Dataset, next_frame: xarray.Dataset,
|
| 418 |
+
) -> xarray.Dataset:
|
| 419 |
+
"""Computes next inputs, from previous inputs and predictions."""
|
| 420 |
+
|
| 421 |
+
# Make sure are are predicting all inputs with a time axis.
|
| 422 |
+
non_predicted_or_forced_inputs = list(
|
| 423 |
+
set(prev_inputs.keys()) - set(next_frame.keys()))
|
| 424 |
+
if "time" in prev_inputs[non_predicted_or_forced_inputs].dims:
|
| 425 |
+
raise ValueError(
|
| 426 |
+
"Found an input with a time index that is not predicted or forced.")
|
| 427 |
+
|
| 428 |
+
# Keys we need to copy from predictions to inputs.
|
| 429 |
+
next_inputs_keys = list(
|
| 430 |
+
set(next_frame.keys()).intersection(set(prev_inputs.keys())))
|
| 431 |
+
next_inputs = next_frame[next_inputs_keys]
|
| 432 |
+
|
| 433 |
+
# Apply concatenate next frame with inputs, crop what we don't need.
|
| 434 |
+
num_inputs = prev_inputs.dims["time"]
|
| 435 |
+
return (
|
| 436 |
+
xarray.concat(
|
| 437 |
+
[prev_inputs, next_inputs], dim="time", data_vars="different")
|
| 438 |
+
.tail(time=num_inputs))
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def extend_targets_template(
|
| 442 |
+
targets_template: xarray.Dataset,
|
| 443 |
+
required_num_steps: int) -> xarray.Dataset:
|
| 444 |
+
"""Extends `targets_template` to `required_num_steps` with lazy arrays.
|
| 445 |
+
|
| 446 |
+
It uses lazy dask arrays of zeros, so it does not require instantiating the
|
| 447 |
+
array in memory.
|
| 448 |
+
|
| 449 |
+
Args:
|
| 450 |
+
targets_template: Input template to extend.
|
| 451 |
+
required_num_steps: Number of steps required in the returned template.
|
| 452 |
+
|
| 453 |
+
Returns:
|
| 454 |
+
`xarray.Dataset` identical in variables and timestep to `targets_template`
|
| 455 |
+
full of `dask.array.zeros` such that the time axis has `required_num_steps`.
|
| 456 |
+
|
| 457 |
+
"""
|
| 458 |
+
|
| 459 |
+
# Extend the "time" and "datetime" coordinates
|
| 460 |
+
time = targets_template.coords["time"]
|
| 461 |
+
|
| 462 |
+
# Assert the first target time corresponds to the timestep.
|
| 463 |
+
timestep = time[0].data
|
| 464 |
+
if time.shape[0] > 1:
|
| 465 |
+
assert np.all(timestep == time[1:] - time[:-1])
|
| 466 |
+
|
| 467 |
+
extended_time = (np.arange(required_num_steps) + 1) * timestep
|
| 468 |
+
|
| 469 |
+
if "datetime" in targets_template.coords:
|
| 470 |
+
datetime = targets_template.coords["datetime"]
|
| 471 |
+
extended_datetime = (datetime[0].data - timestep) + extended_time
|
| 472 |
+
else:
|
| 473 |
+
extended_datetime = None
|
| 474 |
+
|
| 475 |
+
# Replace the values with empty dask arrays extending the time coordinates.
|
| 476 |
+
datetime = targets_template.coords["time"]
|
| 477 |
+
|
| 478 |
+
def extend_time(data_array: xarray.DataArray) -> xarray.DataArray:
|
| 479 |
+
dims = data_array.dims
|
| 480 |
+
shape = list(data_array.shape)
|
| 481 |
+
shape[dims.index("time")] = required_num_steps
|
| 482 |
+
dask_data = dask.array.zeros(
|
| 483 |
+
shape=tuple(shape),
|
| 484 |
+
chunks=-1, # Will give chunk info directly to `ChunksToZarr``.
|
| 485 |
+
dtype=data_array.dtype)
|
| 486 |
+
|
| 487 |
+
coords = dict(data_array.coords)
|
| 488 |
+
coords["time"] = extended_time
|
| 489 |
+
|
| 490 |
+
if extended_datetime is not None:
|
| 491 |
+
coords["datetime"] = ("time", extended_datetime)
|
| 492 |
+
|
| 493 |
+
return xarray.DataArray(
|
| 494 |
+
dims=dims,
|
| 495 |
+
data=dask_data,
|
| 496 |
+
coords=coords)
|
| 497 |
+
|
| 498 |
+
return xarray_tree.map_structure(extend_time, targets_template)
|
model/graphcast/samplers_base.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Base class for diffusion samplers."""
|
| 15 |
+
|
| 16 |
+
import abc
|
| 17 |
+
from typing import Optional
|
| 18 |
+
|
| 19 |
+
from . import denoisers_base
|
| 20 |
+
import xarray
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Sampler(abc.ABC):
|
| 24 |
+
"""A sampling algorithm for a denoising diffusion model.
|
| 25 |
+
|
| 26 |
+
This is constructed with a denoising function, and uses it to draw samples.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
_denoiser: denoisers_base.Denoiser
|
| 30 |
+
|
| 31 |
+
def __init__(self, denoiser: denoisers_base.Denoiser):
|
| 32 |
+
"""Constructs Sampler.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
denoiser: A Denoiser which has been trained with an MSE loss to predict
|
| 36 |
+
the noise-free targets.
|
| 37 |
+
"""
|
| 38 |
+
self._denoiser = denoiser
|
| 39 |
+
|
| 40 |
+
@abc.abstractmethod
|
| 41 |
+
def __call__(
|
| 42 |
+
self,
|
| 43 |
+
inputs: xarray.Dataset,
|
| 44 |
+
targets_template: xarray.Dataset,
|
| 45 |
+
forcings: Optional[xarray.Dataset] = None,
|
| 46 |
+
**kwargs) -> xarray.Dataset:
|
| 47 |
+
"""Draws a sample using self._denoiser. Contract like Predictor.__call__."""
|
model/graphcast/samplers_utils.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utils for diffusion samplers. Makes use of dinosaur.spherical_harmonic."""
|
| 15 |
+
|
| 16 |
+
import dataclasses
|
| 17 |
+
import functools
|
| 18 |
+
from typing import Any, cast, Optional, Tuple
|
| 19 |
+
|
| 20 |
+
import chex
|
| 21 |
+
from dinosaur import spherical_harmonic
|
| 22 |
+
from . import xarray_jax
|
| 23 |
+
from . import xarray_tree
|
| 24 |
+
import haiku as hk
|
| 25 |
+
import jax
|
| 26 |
+
import jax.numpy as jnp
|
| 27 |
+
import numpy as np
|
| 28 |
+
import xarray
|
| 29 |
+
|
| 30 |
+
# Some useful constants useful when dealing with Earth's geometry.
|
| 31 |
+
# The earth isn't really a sphere so these are only approximate, this is the
|
| 32 |
+
# average radius according to https://en.wikipedia.org/wiki/Earth_radius,
|
| 33 |
+
# with the actual value varying from 6378 to 6357km.
|
| 34 |
+
EARTH_RADIUS_KM = 6371.
|
| 35 |
+
# And this is also approximate, but we've chosen to make it consistent with the
|
| 36 |
+
# radius above when modelling the earth as a sphere. This gives a value of
|
| 37 |
+
# around 40030; the actual value varies from 40008 to 40075.
|
| 38 |
+
EARTH_CIRCUMFERENCE_KM = EARTH_RADIUS_KM * 2 * np.pi
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@dataclasses.dataclass(frozen=True)
|
| 42 |
+
class _ArrayGrid:
|
| 43 |
+
"""A class that performs operations and transformations in the spectral basis.
|
| 44 |
+
|
| 45 |
+
Attributes:
|
| 46 |
+
longitude_wavenumbers: num of longitude wavenumbers in the spectral basis.
|
| 47 |
+
total_wavenumbers: number of total wavenumbers in the spectral basis.
|
| 48 |
+
longitude_nodes: number of quadrature nodes along the lon direction.
|
| 49 |
+
latitude_nodes: number of quadrature nodes along the lat direction.
|
| 50 |
+
latitude_spacing: either 'gauss' or 'equiangular'. This determines the
|
| 51 |
+
spacing of nodal grid points in the latitudinal (north-south) direction.
|
| 52 |
+
"""
|
| 53 |
+
longitude_wavenumbers: int
|
| 54 |
+
total_wavenumbers: int
|
| 55 |
+
longitude_nodes: int
|
| 56 |
+
latitude_nodes: int
|
| 57 |
+
latitude_spacing: str
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
def with_lat_lon(
|
| 61 |
+
cls,
|
| 62 |
+
lat: np.ndarray,
|
| 63 |
+
lon: np.ndarray,
|
| 64 |
+
) -> '_ArrayGrid':
|
| 65 |
+
"""_ArrayGrid for use with data in specified lat/lon grid (in degrees)."""
|
| 66 |
+
|
| 67 |
+
latitude_nodes = lat.shape[0]
|
| 68 |
+
longitude_nodes = lon.shape[0]
|
| 69 |
+
latitude_spacing = _infer_latitude_spacing(lat)
|
| 70 |
+
if latitude_spacing in ['equiangular', 'gauss']:
|
| 71 |
+
if longitude_nodes != 2 * latitude_nodes:
|
| 72 |
+
# Technically not a requirement but useful to ensure `max_wavenumber`
|
| 73 |
+
# makes sense.
|
| 74 |
+
raise ValueError(
|
| 75 |
+
'Unexpected number of longitude nodes. '
|
| 76 |
+
f'Expected {2 * latitude_nodes}, got {longitude_nodes}')
|
| 77 |
+
elif latitude_spacing == 'equiangular_with_poles':
|
| 78 |
+
if longitude_nodes != 2 * (latitude_nodes - 1):
|
| 79 |
+
# Technically not a requirement but useful to ensure `max_wavenumber`
|
| 80 |
+
# makes s
|
| 81 |
+
raise ValueError(
|
| 82 |
+
'Unexpected number of longitude nodes. '
|
| 83 |
+
f'Expected {2 * (latitude_nodes - 1)}, got {longitude_nodes}')
|
| 84 |
+
else:
|
| 85 |
+
raise ValueError(f'Unexpected latitude_spacing={latitude_spacing}')
|
| 86 |
+
max_wavenumber = int(longitude_nodes // 2) - 1
|
| 87 |
+
grid = cls(
|
| 88 |
+
longitude_wavenumbers=max_wavenumber+1,
|
| 89 |
+
# total_wavenumbers should be one larger than max_wavenumber as the
|
| 90 |
+
# wavenumbers go from 0 to max_wavenumber inclusive.
|
| 91 |
+
total_wavenumbers=max_wavenumber+1,
|
| 92 |
+
longitude_nodes=longitude_nodes,
|
| 93 |
+
latitude_nodes=latitude_nodes,
|
| 94 |
+
latitude_spacing=latitude_spacing,
|
| 95 |
+
)
|
| 96 |
+
_verify_nodal_axes(lat, lon, grid.nodal_axes)
|
| 97 |
+
return grid
|
| 98 |
+
|
| 99 |
+
@functools.cached_property
|
| 100 |
+
def _grid(self) -> spherical_harmonic.Grid:
|
| 101 |
+
return spherical_harmonic.Grid(
|
| 102 |
+
spherical_harmonics_impl=spherical_harmonic.RealSphericalHarmonics,
|
| 103 |
+
**dataclasses.asdict(self),
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
@functools.cached_property
|
| 107 |
+
def nodal_axes(self) -> Tuple[np.ndarray, np.ndarray]:
|
| 108 |
+
"""Longitude and sin(latitude) coordinates of the nodal basis."""
|
| 109 |
+
return self._grid.nodal_axes
|
| 110 |
+
|
| 111 |
+
@functools.cached_property
|
| 112 |
+
def modal_axes(self) -> Tuple[np.ndarray, np.ndarray]:
|
| 113 |
+
"""Longitudinal and total wavenumbers (m, l) of the modal basis."""
|
| 114 |
+
return self._grid.modal_axes
|
| 115 |
+
|
| 116 |
+
def to_nodal(self, x: chex.Array) -> chex.Array:
|
| 117 |
+
"""Maps `x` from a modal to nodal representation."""
|
| 118 |
+
return self._grid.to_nodal(x)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _infer_latitude_spacing(lat: np.ndarray) -> str:
|
| 122 |
+
"""Infers the type of latitude spacing given the latitude."""
|
| 123 |
+
if not np.all(np.diff(lat) > 0.):
|
| 124 |
+
raise ValueError('Latitude values are expected to be sorted.')
|
| 125 |
+
|
| 126 |
+
if np.allclose(np.diff(lat), lat[1] - lat[0]):
|
| 127 |
+
if np.isclose(max(lat), 90.):
|
| 128 |
+
spacing = 'equiangular_with_poles'
|
| 129 |
+
else:
|
| 130 |
+
spacing = 'equiangular'
|
| 131 |
+
else:
|
| 132 |
+
spacing = 'gauss'
|
| 133 |
+
return spacing
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _verify_nodal_axes(lat_coords: np.ndarray, lon_coords: np.ndarray,
|
| 137 |
+
nodal_axes: Tuple[np.ndarray, np.ndarray]):
|
| 138 |
+
nodal_axes_lon, nodal_axes_sin_lat = nodal_axes
|
| 139 |
+
if not np.allclose(nodal_axes_sin_lat, np.sin(np.deg2rad(lat_coords))):
|
| 140 |
+
raise ValueError(
|
| 141 |
+
"Latitude coords don't match those used by "
|
| 142 |
+
"spherical_harmonic.SphericalHarmonicBasis.")
|
| 143 |
+
if not np.allclose(nodal_axes_lon, np.deg2rad(lon_coords)):
|
| 144 |
+
raise ValueError(
|
| 145 |
+
"Longitude coords don't match those used by "
|
| 146 |
+
"spherical_harmonic.SphericalHarmonicBasis.")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class Grid:
|
| 150 |
+
"""xarray wrapper around _ArrayGrid."""
|
| 151 |
+
|
| 152 |
+
@classmethod
|
| 153 |
+
def for_nodal_data(
|
| 154 |
+
cls,
|
| 155 |
+
nodal_data: xarray.DataArray,
|
| 156 |
+
) -> 'Grid':
|
| 157 |
+
"""A Grid for use with a given shape of nodal (lat/lon grid) data.
|
| 158 |
+
|
| 159 |
+
This uses the maximum number of spherical harmonics that the grid is able
|
| 160 |
+
to resolve.
|
| 161 |
+
|
| 162 |
+
This class supports data arrays with latitude spacings as defined by
|
| 163 |
+
"dinosaur.spherical_harmonic". In summary:
|
| 164 |
+
* 'equiangular': equally spaced (by `d_lat`) values between -90 + d_lat and
|
| 165 |
+
90 - d_lat / 2. In our case, longitude must also be spaced by `d_lat`.
|
| 166 |
+
* 'equiangular_with_poles': equally spaced (by `d_lat`) values between -90
|
| 167 |
+
and 90. In our case, longitude must also be spaced by `d_lat`.
|
| 168 |
+
* 'gauss': Gauss-Legendre nodes.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
nodal_data: An xarray with 'lat' and 'lon' dimensions and coordinates in
|
| 172 |
+
degrees.
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
A grid with the specified latitude_nodes, with
|
| 176 |
+
longitude_nodes=2*latitude_nodes and max_wavenumber=latitude_nodes-1.
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
grid = _ArrayGrid.with_lat_lon(
|
| 180 |
+
nodal_data.coords['lat'].data,
|
| 181 |
+
nodal_data.coords['lon'].data)
|
| 182 |
+
return cls(grid,
|
| 183 |
+
nodal_data.coords['lat'].data,
|
| 184 |
+
nodal_data.coords['lon'].data)
|
| 185 |
+
|
| 186 |
+
def __init__(self,
|
| 187 |
+
grid: _ArrayGrid,
|
| 188 |
+
lat_coords: np.ndarray,
|
| 189 |
+
lon_coords: np.ndarray):
|
| 190 |
+
_verify_nodal_axes(lat_coords, lon_coords, grid.nodal_axes)
|
| 191 |
+
self._underlying = grid
|
| 192 |
+
# Record the exact original lat/lon coords so we can return them exactly
|
| 193 |
+
# from an inverse transform, avoiding any xarray merge issues if coordinates
|
| 194 |
+
# are off by a rounding error.
|
| 195 |
+
self._lat_coords = lat_coords
|
| 196 |
+
self._lon_coords = lon_coords
|
| 197 |
+
self._longitude_wavenumber_coords, self._total_wavenumber_coords = (
|
| 198 |
+
grid.modal_axes)
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def total_wavenumber_coords(self) -> xarray.DataArray:
|
| 202 |
+
"""Coords that must be used for 'total_wavenumber' dimension."""
|
| 203 |
+
return xarray.DataArray(
|
| 204 |
+
data=self._total_wavenumber_coords,
|
| 205 |
+
dims=('total_wavenumber',),
|
| 206 |
+
coords={'total_wavenumber': self._total_wavenumber_coords})
|
| 207 |
+
|
| 208 |
+
@property
|
| 209 |
+
def longitude_wavenumber_coords(self) -> xarray.DataArray:
|
| 210 |
+
"""Coords that must be used for 'longitude_wavenumber' dimension."""
|
| 211 |
+
return xarray.DataArray(
|
| 212 |
+
data=self._longitude_wavenumber_coords,
|
| 213 |
+
dims=('longitude_wavenumber',),
|
| 214 |
+
coords={'longitude_wavenumber': self._longitude_wavenumber_coords})
|
| 215 |
+
|
| 216 |
+
def to_nodal(
|
| 217 |
+
self, modal_data: xarray.DataArray) -> xarray.DataArray:
|
| 218 |
+
"""Applies the inverse spherical harmonic transform.
|
| 219 |
+
|
| 220 |
+
Args:
|
| 221 |
+
modal_data: A tree of xarray.DataArray with 'longitude_wavenumber' and
|
| 222 |
+
'total_wavenumber' dimensions with coords
|
| 223 |
+
`self.longitude_wavenumber_coords` and `self.total_wavenumber_coords`
|
| 224 |
+
respectively, and with the same sparsity pattern described under
|
| 225 |
+
`to_modal`.
|
| 226 |
+
|
| 227 |
+
Returns:
|
| 228 |
+
Corresponding tree where the 'longitude_wavenumber' and
|
| 229 |
+
'total_wavenumber' dimensions are replaced by 'lat', 'lon' dimensions.
|
| 230 |
+
"""
|
| 231 |
+
def inverse_transform(modal: xarray.DataArray) -> xarray.DataArray:
|
| 232 |
+
if (not np.all(modal.coords['longitude_wavenumber'] ==
|
| 233 |
+
self._longitude_wavenumber_coords) or
|
| 234 |
+
not np.all(modal.coords['total_wavenumber'] ==
|
| 235 |
+
self._total_wavenumber_coords)):
|
| 236 |
+
raise ValueError('Wavenumber coords don\'t follow required convention.')
|
| 237 |
+
|
| 238 |
+
return xarray_jax.apply_ufunc(
|
| 239 |
+
self._underlying.to_nodal, modal,
|
| 240 |
+
input_core_dims=[['longitude_wavenumber', 'total_wavenumber']],
|
| 241 |
+
output_core_dims=[['lon', 'lat']],
|
| 242 |
+
).assign_coords(
|
| 243 |
+
lon=self._lon_coords,
|
| 244 |
+
lat=self._lat_coords,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
return xarray_tree.map_structure(inverse_transform, modal_data)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def sample(
|
| 251 |
+
key: jnp.ndarray,
|
| 252 |
+
power_spectrum: xarray.DataArray,
|
| 253 |
+
template: xarray.DataArray,
|
| 254 |
+
grid: Optional[Grid] = None,
|
| 255 |
+
) -> xarray.DataArray:
|
| 256 |
+
"""Samples Gaussian Process noise on a sphere, with a given power spectrum.
|
| 257 |
+
|
| 258 |
+
This means the noise will have the given power spectrum *in expectation*; the
|
| 259 |
+
power spectrum of individual samples may vary.
|
| 260 |
+
|
| 261 |
+
The noise will be isotropic, meaning the distribution is invariant to
|
| 262 |
+
rotations of the sphere.
|
| 263 |
+
|
| 264 |
+
The marginal variance of the returned values will be equal to the total power,
|
| 265 |
+
i.e. the sum of power_spectrum. So if you want unit marginal variance, just
|
| 266 |
+
make sure to normalize the power_spectrum to sum to 1.
|
| 267 |
+
|
| 268 |
+
Args:
|
| 269 |
+
key: JAX rng key.
|
| 270 |
+
power_spectrum: An array with shape (total_wavenumber,) giving the power
|
| 271 |
+
which is desired at each total wavenumber (corresponding to a wavelength
|
| 272 |
+
EARTH_CIRCUMFERENCE/total_wavenumber) for total wavenumbers 0 up to some
|
| 273 |
+
maximum. This is in squared units of the quantity being sampled.
|
| 274 |
+
template: An array with the shape that you want the samples in, containing
|
| 275 |
+
'lat' and 'lon' dimensions. If other dimensions are present, we draw
|
| 276 |
+
multiple independent samples along these other dimensions.
|
| 277 |
+
grid: spherical_harmonic.Grid on which to sample the noise. If not specified
|
| 278 |
+
a grid will be created based on `template`, however note you may save some
|
| 279 |
+
RAM and compute by re-using a single Grid instance across multiple calls.
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
DataArray with the same shape as template.
|
| 283 |
+
"""
|
| 284 |
+
if grid is None:
|
| 285 |
+
grid = Grid.for_nodal_data(template)
|
| 286 |
+
dims = [d for d in template.dims if d not in ('lat', 'lon')]
|
| 287 |
+
shape = [template.sizes[d] for d in dims]
|
| 288 |
+
coords = {name: coord for name, coord in template.coords.items()
|
| 289 |
+
if name not in ('lat', 'lon')}
|
| 290 |
+
dims.extend(('total_wavenumber', 'longitude_wavenumber'))
|
| 291 |
+
shape.extend((len(grid.total_wavenumber_coords),
|
| 292 |
+
len(grid.longitude_wavenumber_coords)))
|
| 293 |
+
coords.update({'total_wavenumber': grid.total_wavenumber_coords,
|
| 294 |
+
'longitude_wavenumber': grid.longitude_wavenumber_coords})
|
| 295 |
+
coeffs = xarray_jax.DataArray(
|
| 296 |
+
data=jax.random.normal(key, shape), dims=dims, coords=coords)
|
| 297 |
+
# Mask out coefficients which are out of range. This broadcasts to a
|
| 298 |
+
# triangular mask with shape (total_wavenumber, longitude_wavenumber):
|
| 299 |
+
mask = (
|
| 300 |
+
abs(coeffs.longitude_wavenumber) <= coeffs.total_wavenumber
|
| 301 |
+
).astype(np.float32)
|
| 302 |
+
# For total_wavenumber t, there will be 2t+1 non-zero coefficients at
|
| 303 |
+
# different longitude_wavenumbers. We must normalize the coefficients so that
|
| 304 |
+
# summing their squares at each total_wavenumber, sums to the corresponding
|
| 305 |
+
# value in the power spectrum:
|
| 306 |
+
multiplier = mask * np.sqrt(power_spectrum / mask.sum(
|
| 307 |
+
'longitude_wavenumber', skipna=False))
|
| 308 |
+
# And a standard normalization factor used in this implementation of the
|
| 309 |
+
# spherical harmonic transform:
|
| 310 |
+
multiplier *= np.sqrt(4 * np.pi)
|
| 311 |
+
# Only finally multiply by coeffs to avoid too many broadcasting
|
| 312 |
+
# multiplications:
|
| 313 |
+
coeffs *= multiplier
|
| 314 |
+
result = cast(xarray.DataArray, grid.to_nodal(coeffs))
|
| 315 |
+
result = result.astype(template.dtype)
|
| 316 |
+
return result.transpose(*template.dims)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def spherical_white_noise_like(template: xarray.Dataset) -> xarray.Dataset:
|
| 320 |
+
"""Samples isotropic mean 0 variance 1 white noise on the sphere."""
|
| 321 |
+
def spherical_white_noise_like_dataarray(data_array: xarray.DataArray
|
| 322 |
+
) -> xarray.DataArray:
|
| 323 |
+
num_wavenumbers = data_array.lon.shape[0] // 2
|
| 324 |
+
key = hk.next_rng_key()
|
| 325 |
+
return sample(
|
| 326 |
+
key=key,
|
| 327 |
+
power_spectrum=xarray_jax.DataArray(
|
| 328 |
+
data=np.array([1/num_wavenumbers for _ in range(num_wavenumbers)]),
|
| 329 |
+
dims=['total_wavenumber']),
|
| 330 |
+
template=data_array)
|
| 331 |
+
return template.map(spherical_white_noise_like_dataarray)
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def rho_inverse_cdf(
|
| 335 |
+
min_value: float,
|
| 336 |
+
max_value: float,
|
| 337 |
+
rho: float,
|
| 338 |
+
cdf: Any) -> Any:
|
| 339 |
+
"""Quantiles of rho distribution used for noise levels at sampling time.
|
| 340 |
+
|
| 341 |
+
This is parameterised by rho as in Eqn 5 from the Elucidating paper
|
| 342 |
+
(but with max/min flipped so that quantiles are given in ascending not
|
| 343 |
+
descending order). It's equivalent to a Beta[rho, 1] distribution rescaled to
|
| 344 |
+
[min_value, max_value].
|
| 345 |
+
|
| 346 |
+
At sampling time we use noise levels at fixed quantiles of this distribution.
|
| 347 |
+
Unlike in the paper, we also use the same distribution for noise levels at
|
| 348 |
+
training time (albeit potentially with different parameters, and sampling from
|
| 349 |
+
it at random).
|
| 350 |
+
|
| 351 |
+
Args:
|
| 352 |
+
min_value:
|
| 353 |
+
max_value:
|
| 354 |
+
Define the support of the distribution.
|
| 355 |
+
rho:
|
| 356 |
+
Shape parameter.
|
| 357 |
+
cdf:
|
| 358 |
+
Value or values between 0 and 1 indicating which quantile you want. Can
|
| 359 |
+
be a numpy or jax array.
|
| 360 |
+
|
| 361 |
+
Returns:
|
| 362 |
+
Quantiles of the distribution, with same shape/type as `cdf`.
|
| 363 |
+
"""
|
| 364 |
+
return (
|
| 365 |
+
min_value**(1 / rho) + cdf *
|
| 366 |
+
(max_value**(1 / rho) - min_value**(1 / rho))
|
| 367 |
+
)**rho
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def tree_where(
|
| 371 |
+
cond: jnp.ndarray,
|
| 372 |
+
xs: Any,
|
| 373 |
+
ys: Any
|
| 374 |
+
) -> Any:
|
| 375 |
+
"""Like jnp.where but works with trees for xs and ys (but not for cond)."""
|
| 376 |
+
return jax.tree_util.tree_map(lambda x, y: jnp.where(cond, x, y), xs, ys)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def noise_schedule(
|
| 380 |
+
max_noise_level: float = 80.,
|
| 381 |
+
min_noise_level: float = 0.002,
|
| 382 |
+
num_noise_levels: int = 30,
|
| 383 |
+
rho: float = 7.,
|
| 384 |
+
) -> np.ndarray:
|
| 385 |
+
"""Computes a descending noise schedule for sampling, ending with zero."""
|
| 386 |
+
noise_levels = rho_inverse_cdf(
|
| 387 |
+
min_value=min_noise_level,
|
| 388 |
+
max_value=max_noise_level,
|
| 389 |
+
rho=rho,
|
| 390 |
+
# We want the noise levels in descending order, so ask for quantiles
|
| 391 |
+
# 1 down to 0:
|
| 392 |
+
cdf=np.linspace(1, 0, num_noise_levels))
|
| 393 |
+
# The final zero noise level is somewhat special-cased. We don't actually
|
| 394 |
+
# denoise from this noise level but appending it here is convenient for
|
| 395 |
+
# sampling loop implementations.
|
| 396 |
+
return np.append(noise_levels, 0.)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def stochastic_churn_rate_schedule(
|
| 400 |
+
noise_levels: np.ndarray,
|
| 401 |
+
stochastic_churn_rate: float = 0.,
|
| 402 |
+
churn_min_noise_level: float = 0.05,
|
| 403 |
+
churn_max_noise_level: float = 50.0,
|
| 404 |
+
) -> np.ndarray:
|
| 405 |
+
"""Computes a stochastic churn rate for each noise level."""
|
| 406 |
+
num_noise_levels = len(noise_levels)-1 # Exclude final zero noise level.
|
| 407 |
+
# As in the Elucidated Diffusion paper, clamp this so it doesn't increase the
|
| 408 |
+
# variance by a factor of more than 2, no matter how few noise levels are
|
| 409 |
+
# used:
|
| 410 |
+
per_step_churn_rate = min(stochastic_churn_rate / num_noise_levels,
|
| 411 |
+
np.sqrt(2) - 1)
|
| 412 |
+
return (
|
| 413 |
+
(churn_min_noise_level <= noise_levels[:-1]) &
|
| 414 |
+
(noise_levels[:-1] <= churn_max_noise_level)
|
| 415 |
+
) * per_step_churn_rate
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def apply_stochastic_churn(
|
| 419 |
+
x: Any,
|
| 420 |
+
noise_level: jax.typing.ArrayLike,
|
| 421 |
+
stochastic_churn_rate: jax.typing.ArrayLike,
|
| 422 |
+
noise_level_inflation_factor: jax.typing.ArrayLike,
|
| 423 |
+
) -> tuple[Any, jax.typing.ArrayLike]:
|
| 424 |
+
"""Returns x at higher noise level, and the higher noise level itself."""
|
| 425 |
+
# We increase the noise level of x a bit before taking it down again:
|
| 426 |
+
new_noise_level = noise_level * (1.0 + stochastic_churn_rate)
|
| 427 |
+
noise_diff = new_noise_level**2 - noise_level**2
|
| 428 |
+
# stochastic_churn_rate == 0 => new_noise_level == noise_level
|
| 429 |
+
# => noise_diff == 0. This can resolve to a negative value because of
|
| 430 |
+
# floating point rounding errors. To avoid this we clamp noise_diff to zero if
|
| 431 |
+
# it's negative.
|
| 432 |
+
noise_diff = jnp.maximum(noise_diff, 0)
|
| 433 |
+
extra_noise_stddev = jnp.sqrt(noise_diff)* noise_level_inflation_factor
|
| 434 |
+
updated_x = x + spherical_white_noise_like(x) * extra_noise_stddev
|
| 435 |
+
return updated_x, new_noise_level
|
| 436 |
+
|
model/graphcast/solar_radiation.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Computes TOA incident solar radiation compatible with ERA5.
|
| 15 |
+
|
| 16 |
+
The Top-Of-the-Atmosphere (TOA) incident solar radiation is available in the
|
| 17 |
+
ERA5 dataset as the parameter `toa_incident_solar_radiation` (or `tisr`). This
|
| 18 |
+
represents the TOA solar radiation flux integrated over a period of one hour
|
| 19 |
+
ending at the timestamp given by the `datetime` coordinate. See
|
| 20 |
+
https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation and
|
| 21 |
+
https://codes.ecmwf.int/grib/param-db/?id=212.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from collections.abc import Callable, Sequence
|
| 25 |
+
import dataclasses
|
| 26 |
+
import functools
|
| 27 |
+
|
| 28 |
+
import chex
|
| 29 |
+
import jax
|
| 30 |
+
import jax.numpy as jnp
|
| 31 |
+
import numpy as np
|
| 32 |
+
import pandas as pd
|
| 33 |
+
import xarray as xa
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Default value of the `integration_period` argument to be compatible with ERA5.
|
| 37 |
+
_DEFAULT_INTEGRATION_PERIOD = pd.Timedelta(hours=1)
|
| 38 |
+
|
| 39 |
+
# Default value for the `num_integration_bins` argument. This provides a good
|
| 40 |
+
# approximation of the solar radiation in ERA5.
|
| 41 |
+
_DEFAULT_NUM_INTEGRATION_BINS = 360
|
| 42 |
+
|
| 43 |
+
# The length of a Julian year in days.
|
| 44 |
+
# https://en.wikipedia.org/wiki/Julian_year_(astronomy)
|
| 45 |
+
_JULIAN_YEAR_LENGTH_IN_DAYS = 365.25
|
| 46 |
+
|
| 47 |
+
# Julian Date for the J2000 epoch, a standard reference used in astronomy.
|
| 48 |
+
# https://en.wikipedia.org/wiki/Epoch_(astronomy)#Julian_years_and_J2000
|
| 49 |
+
_J2000_EPOCH = 2451545.0
|
| 50 |
+
|
| 51 |
+
# Number of seconds in a day.
|
| 52 |
+
_SECONDS_PER_DAY = 60 * 60 * 24
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
_TimestampLike = str | pd.Timestamp | np.datetime64
|
| 56 |
+
_TimedeltaLike = str | pd.Timedelta | np.timedelta64
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Interface for loading Total Solar Irradiance (TSI) data.
|
| 60 |
+
# Returns a xa.DataArray containing yearly average TSI values with a `time`
|
| 61 |
+
# coordinate in units of years since 0000-1-1. E.g. 2023.5 corresponds to
|
| 62 |
+
# the middle of the year 2023.
|
| 63 |
+
TsiDataLoader = Callable[[], xa.DataArray]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# Total Solar Irradiance (TSI): Energy input to the top of the Earth's
|
| 67 |
+
# atmosphere in W⋅m⁻². TSI varies with time. This is the reference TSI value
|
| 68 |
+
# that can be used when more accurate data is not available.
|
| 69 |
+
# https://www.ncei.noaa.gov/products/climate-data-records/total-solar-irradiance
|
| 70 |
+
# https://github.com/ecmwf-ifs/ecrad/blob/6db82f929fb75028cc20606a04da87c0abe9b642/radiation/radiation_ecckd.F90#L296
|
| 71 |
+
_REFERENCE_TSI = 1361.0
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def reference_tsi_data() -> xa.DataArray:
|
| 75 |
+
"""A TsiDataProvider that returns a single reference TSI value."""
|
| 76 |
+
return xa.DataArray(
|
| 77 |
+
np.array([_REFERENCE_TSI]),
|
| 78 |
+
dims=["time"],
|
| 79 |
+
coords={"time": np.array([0.0])},
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def era5_tsi_data() -> xa.DataArray:
|
| 84 |
+
"""A TsiDataProvider that returns ERA5 compatible TSI data."""
|
| 85 |
+
# ECMWF provided the data used for ERA5, which was hardcoded in the IFS (cycle
|
| 86 |
+
# 41r2). The values were scaled down to agree better with more recent
|
| 87 |
+
# observations of the sun.
|
| 88 |
+
time = np.arange(1951.5, 2035.5, 1.0)
|
| 89 |
+
tsi = 0.9965 * np.array([
|
| 90 |
+
# fmt: off
|
| 91 |
+
# 1951-1995 (non-repeating sequence)
|
| 92 |
+
1365.7765, 1365.7676, 1365.6284, 1365.6564, 1365.7773,
|
| 93 |
+
1366.3109, 1366.6681, 1366.6328, 1366.3828, 1366.2767,
|
| 94 |
+
1365.9199, 1365.7484, 1365.6963, 1365.6976, 1365.7341,
|
| 95 |
+
1365.9178, 1366.1143, 1366.1644, 1366.2476, 1366.2426,
|
| 96 |
+
1365.9580, 1366.0525, 1365.7991, 1365.7271, 1365.5345,
|
| 97 |
+
1365.6453, 1365.8331, 1366.2747, 1366.6348, 1366.6482,
|
| 98 |
+
1366.6951, 1366.2859, 1366.1992, 1365.8103, 1365.6416,
|
| 99 |
+
1365.6379, 1365.7899, 1366.0826, 1366.6479, 1366.5533,
|
| 100 |
+
1366.4457, 1366.3021, 1366.0286, 1365.7971, 1365.6996,
|
| 101 |
+
# 1996-2008 (13 year cycle, repeated below)
|
| 102 |
+
1365.6121, 1365.7399, 1366.1021, 1366.3851, 1366.6836,
|
| 103 |
+
1366.6022, 1366.6807, 1366.2300, 1366.0480, 1365.8545,
|
| 104 |
+
1365.8107, 1365.7240, 1365.6918,
|
| 105 |
+
# 2009-2021
|
| 106 |
+
1365.6121, 1365.7399, 1366.1021, 1366.3851, 1366.6836,
|
| 107 |
+
1366.6022, 1366.6807, 1366.2300, 1366.0480, 1365.8545,
|
| 108 |
+
1365.8107, 1365.7240, 1365.6918,
|
| 109 |
+
# 2022-2034
|
| 110 |
+
1365.6121, 1365.7399, 1366.1021, 1366.3851, 1366.6836,
|
| 111 |
+
1366.6022, 1366.6807, 1366.2300, 1366.0480, 1365.8545,
|
| 112 |
+
1365.8107, 1365.7240, 1365.6918,
|
| 113 |
+
# fmt: on
|
| 114 |
+
])
|
| 115 |
+
return xa.DataArray(tsi, dims=["time"], coords={"time": time})
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# HRES compatible TSI data is from IFS cycle 47r1. The dataset can be obtained
|
| 119 |
+
# from the ECRAD package: https://confluence.ecmwf.int/display/ECRAD.
|
| 120 |
+
# The example code below can load this dataset from a local file.
|
| 121 |
+
|
| 122 |
+
# def hres_tsi_data() -> xa.DataArray:
|
| 123 |
+
# with open("total_solar_irradiance_CMIP6_47r1.nc", "rb") as f:
|
| 124 |
+
# with xa.load_dataset(f, decode_times=False) as ds:
|
| 125 |
+
# return ds["tsi"]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
_DEFAULT_TSI_DATA_LOADER: TsiDataLoader = era5_tsi_data
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def get_tsi(
|
| 132 |
+
timestamps: Sequence[_TimestampLike], tsi_data: xa.DataArray
|
| 133 |
+
) -> chex.Array:
|
| 134 |
+
"""Returns TSI values for the given timestamps.
|
| 135 |
+
|
| 136 |
+
TSI values are interpolated from the provided yearly TSI data.
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
timestamps: Timestamps for which to compute TSI values.
|
| 140 |
+
tsi_data: A DataArray with a single dimension `time` that has coordinates in
|
| 141 |
+
units of years since 0000-1-1. E.g. 2023.5 corresponds to the middle of
|
| 142 |
+
the year 2023.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
An Array containing interpolated TSI data.
|
| 146 |
+
"""
|
| 147 |
+
timestamps = pd.DatetimeIndex(timestamps)
|
| 148 |
+
timestamps_date = pd.DatetimeIndex(timestamps.date)
|
| 149 |
+
day_fraction = (timestamps - timestamps_date) / pd.Timedelta(days=1)
|
| 150 |
+
year_length = 365 + timestamps.is_leap_year
|
| 151 |
+
year_fraction = (timestamps.dayofyear - 1 + day_fraction) / year_length
|
| 152 |
+
fractional_year = timestamps.year + year_fraction
|
| 153 |
+
return np.interp(fractional_year, tsi_data.coords["time"].data, tsi_data.data)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@dataclasses.dataclass(frozen=True)
|
| 157 |
+
class _OrbitalParameters:
|
| 158 |
+
"""Parameters characterising Earth's position relative to the Sun.
|
| 159 |
+
|
| 160 |
+
The parameters characterize the position of the Earth in its orbit around the
|
| 161 |
+
Sun for specific points in time. Each attribute is an N-dimensional array
|
| 162 |
+
to represent orbital parameters for multiple points in time.
|
| 163 |
+
|
| 164 |
+
Attributes:
|
| 165 |
+
theta: The number of Julian years since the Julian epoch J2000.0.
|
| 166 |
+
rotational_phase: The phase of the Earth's rotation along its axis as a
|
| 167 |
+
ratio with 0 representing the phase at Julian epoch J2000.0 at exactly
|
| 168 |
+
12:00 Terrestrial Time (TT). Multiplying this value by `2*pi` yields the
|
| 169 |
+
phase in radians.
|
| 170 |
+
sin_declination: Sine of the declination of the Sun as seen from the Earth.
|
| 171 |
+
cos_declination: Cosine of the declination of the Sun as seen from the
|
| 172 |
+
Earth.
|
| 173 |
+
eq_of_time_seconds: The value of the equation of time, in seconds.
|
| 174 |
+
solar_distance_au: Earth-Sun distance in astronomical units.
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
theta: chex.Array
|
| 178 |
+
rotational_phase: chex.Array
|
| 179 |
+
sin_declination: chex.Array
|
| 180 |
+
cos_declination: chex.Array
|
| 181 |
+
eq_of_time_seconds: chex.Array
|
| 182 |
+
solar_distance_au: chex.Array
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _get_j2000_days(timestamp: pd.Timestamp) -> float:
|
| 186 |
+
"""Returns the number of days since the J2000 epoch.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
timestamp: A timestamp for which to compute the J2000 days.
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
The J2000 days corresponding to the input timestamp.
|
| 193 |
+
"""
|
| 194 |
+
return timestamp.to_julian_date() - _J2000_EPOCH
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _get_orbital_parameters(j2000_days: chex.Array) -> _OrbitalParameters:
|
| 198 |
+
"""Computes the orbital parameters for the given J2000 days.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
j2000_days: Timestamps represented as the number of days since the J2000
|
| 202 |
+
epoch.
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
Orbital parameters for the given timestamps. Each attribute of the return
|
| 206 |
+
value is an array containing the same dimensions as the input.
|
| 207 |
+
"""
|
| 208 |
+
# Orbital parameters are computed based on the formulas in this code, which
|
| 209 |
+
# were determined empirically to produce radiation values similar to ERA5:
|
| 210 |
+
# https://github.com/ECCC-ASTD-MRD/gem/blob/1d711f7b89971cd7b1e10afc7508d1135b51397d/src/rpnphy/src/base/sucst.F90
|
| 211 |
+
# https://github.com/ECCC-ASTD-MRD/gem/blob/1d711f7b89971cd7b1e10afc7508d1135b51397d/src/rpnphy/src/base/fctast.cdk
|
| 212 |
+
# https://github.com/ECCC-ASTD-MRD/gem/blob/1d711f7b89971cd7b1e10afc7508d1135b51397d/src/rpnphy/src/base/fcttim.cdk
|
| 213 |
+
# There are many variations to these formulas, but since the goal is to match
|
| 214 |
+
# the values in ERA5, the formulas were implemented as is. Comments reference
|
| 215 |
+
# the notation used in those sources. Here are some additional references
|
| 216 |
+
# related to the quantities being computed here:
|
| 217 |
+
# https://aa.usno.navy.mil/faq/sun_approx
|
| 218 |
+
# https://en.wikipedia.org/wiki/Position_of_the_Sun
|
| 219 |
+
# https://en.wikipedia.org/wiki/Equation_of_time
|
| 220 |
+
|
| 221 |
+
# Number of Julian years since the J2000 epoch (including fractional years).
|
| 222 |
+
theta = j2000_days / _JULIAN_YEAR_LENGTH_IN_DAYS
|
| 223 |
+
# The phase of the Earth's rotation along its axis as a ratio. 0 represents
|
| 224 |
+
# Julian epoch J2000.0 at exactly 12:00 Terrestrial Time (TT).
|
| 225 |
+
rotational_phase = j2000_days % 1.0
|
| 226 |
+
|
| 227 |
+
# REL(PTETA).
|
| 228 |
+
rel = 1.7535 + 6.283076 * theta
|
| 229 |
+
# REM(PTETA).
|
| 230 |
+
rem = 6.240041 + 6.283020 * theta
|
| 231 |
+
# RLLS(PTETA).
|
| 232 |
+
rlls = 4.8951 + 6.283076 * theta
|
| 233 |
+
|
| 234 |
+
# Variables used in the three polynomials below.
|
| 235 |
+
one = jnp.ones_like(theta)
|
| 236 |
+
sin_rel = jnp.sin(rel)
|
| 237 |
+
cos_rel = jnp.cos(rel)
|
| 238 |
+
sin_two_rel = jnp.sin(2.0 * rel)
|
| 239 |
+
cos_two_rel = jnp.cos(2.0 * rel)
|
| 240 |
+
sin_two_rlls = jnp.sin(2.0 * rlls)
|
| 241 |
+
cos_two_rlls = jnp.cos(2.0 * rlls)
|
| 242 |
+
sin_four_rlls = jnp.sin(4.0 * rlls)
|
| 243 |
+
sin_rem = jnp.sin(rem)
|
| 244 |
+
sin_two_rem = jnp.sin(2.0 * rem)
|
| 245 |
+
|
| 246 |
+
# Ecliptic longitude of the Sun - RLLLS(PTETA).
|
| 247 |
+
rllls = jnp.dot(
|
| 248 |
+
jnp.stack(
|
| 249 |
+
[one, theta, sin_rel, cos_rel, sin_two_rel, cos_two_rel], axis=-1
|
| 250 |
+
),
|
| 251 |
+
jnp.array([4.8952, 6.283320, -0.0075, -0.0326, -0.0003, 0.0002]),
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Angle in radians between the Earth's rotational axis and its orbital axis.
|
| 255 |
+
# Equivalent to 23.4393°.
|
| 256 |
+
repsm = 0.409093
|
| 257 |
+
|
| 258 |
+
# Declination of the Sun - RDS(teta).
|
| 259 |
+
sin_declination = jnp.sin(repsm) * jnp.sin(rllls)
|
| 260 |
+
cos_declination = jnp.sqrt(1.0 - sin_declination**2)
|
| 261 |
+
|
| 262 |
+
# Equation of time in seconds - RET(PTETA).
|
| 263 |
+
eq_of_time_seconds = jnp.dot(
|
| 264 |
+
jnp.stack(
|
| 265 |
+
[
|
| 266 |
+
sin_two_rlls,
|
| 267 |
+
sin_rem,
|
| 268 |
+
sin_rem * cos_two_rlls,
|
| 269 |
+
sin_four_rlls,
|
| 270 |
+
sin_two_rem,
|
| 271 |
+
],
|
| 272 |
+
axis=-1,
|
| 273 |
+
),
|
| 274 |
+
jnp.array([591.8, -459.4, 39.5, -12.7, -4.8]),
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
# Earth-Sun distance in astronomical units - RRS(PTETA).
|
| 278 |
+
solar_distance_au = jnp.dot(
|
| 279 |
+
jnp.stack([one, sin_rel, cos_rel], axis=-1),
|
| 280 |
+
jnp.array([1.0001, -0.0163, 0.0037]),
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
return _OrbitalParameters(
|
| 284 |
+
theta=theta,
|
| 285 |
+
rotational_phase=rotational_phase,
|
| 286 |
+
sin_declination=sin_declination,
|
| 287 |
+
cos_declination=cos_declination,
|
| 288 |
+
eq_of_time_seconds=eq_of_time_seconds,
|
| 289 |
+
solar_distance_au=solar_distance_au,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _get_solar_sin_altitude(
|
| 294 |
+
op: _OrbitalParameters,
|
| 295 |
+
sin_latitude: chex.Array,
|
| 296 |
+
cos_latitude: chex.Array,
|
| 297 |
+
longitude: chex.Array,
|
| 298 |
+
) -> chex.Array:
|
| 299 |
+
"""Returns the sine of the solar altitude angle.
|
| 300 |
+
|
| 301 |
+
All computations are vectorized. Dimensions of all the inputs should be
|
| 302 |
+
broadcastable using standard NumPy rules. For example, if `op` has shape
|
| 303 |
+
`(T, 1, 1)`, `latitude` has shape `(1, H, 1)`, and `longitude` has shape
|
| 304 |
+
`(1, H, W)`, the return value will have shape `(T, H, W)`.
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
op: Orbital parameters characterising Earth's position relative to the Sun.
|
| 308 |
+
sin_latitude: Sine of latitude coordinates.
|
| 309 |
+
cos_latitude: Cosine of latitude coordinates.
|
| 310 |
+
longitude: Longitude coordinates in radians.
|
| 311 |
+
|
| 312 |
+
Returns:
|
| 313 |
+
Sine of the solar altitude angle for each set of orbital parameters and each
|
| 314 |
+
geographical coordinates. The returned array has the shape resulting from
|
| 315 |
+
broadcasting all the inputs together.
|
| 316 |
+
"""
|
| 317 |
+
solar_time = op.rotational_phase + op.eq_of_time_seconds / _SECONDS_PER_DAY
|
| 318 |
+
# https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle
|
| 319 |
+
hour_angle = 2.0 * jnp.pi * solar_time + longitude
|
| 320 |
+
# https://en.wikipedia.org/wiki/Solar_zenith_angle
|
| 321 |
+
sin_altitude = (
|
| 322 |
+
cos_latitude * op.cos_declination * jnp.cos(hour_angle)
|
| 323 |
+
+ sin_latitude * op.sin_declination
|
| 324 |
+
)
|
| 325 |
+
return sin_altitude
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def _get_radiation_flux(
|
| 329 |
+
j2000_days: chex.Array,
|
| 330 |
+
sin_latitude: chex.Array,
|
| 331 |
+
cos_latitude: chex.Array,
|
| 332 |
+
longitude: chex.Array,
|
| 333 |
+
tsi: chex.Array,
|
| 334 |
+
) -> chex.Array:
|
| 335 |
+
"""Computes the instantaneous TOA incident solar radiation flux.
|
| 336 |
+
|
| 337 |
+
Computes the instantanous Top-Of-the-Atmosphere (TOA) incident radiation flux
|
| 338 |
+
in W⋅m⁻² for the given timestamps and locations on the surface of the Earth.
|
| 339 |
+
See https://en.wikipedia.org/wiki/Solar_irradiance.
|
| 340 |
+
|
| 341 |
+
All inputs are assumed to be broadcastable together using standard NumPy
|
| 342 |
+
rules.
|
| 343 |
+
|
| 344 |
+
Args:
|
| 345 |
+
j2000_days: Timestamps represented as the number of days since the J2000
|
| 346 |
+
epoch.
|
| 347 |
+
sin_latitude: Sine of latitude coordinates.
|
| 348 |
+
cos_latitude: Cosine of latitude coordinates.
|
| 349 |
+
longitude: Longitude coordinates in radians.
|
| 350 |
+
tsi: Total Solar Irradiance (TSI) in W⋅m⁻². This can be a scalar (default)
|
| 351 |
+
to use the same TSI value for all the inputs, or an array to allow TSI to
|
| 352 |
+
depend on the timestamps.
|
| 353 |
+
|
| 354 |
+
Returns:
|
| 355 |
+
The instataneous TOA incident solar radiation flux in W⋅m⁻² for the given
|
| 356 |
+
timestamps and geographical coordinates. The returned array has the shape
|
| 357 |
+
resulting from broadcasting all the inputs together.
|
| 358 |
+
"""
|
| 359 |
+
op = _get_orbital_parameters(j2000_days)
|
| 360 |
+
# Attenuation of the solar radiation based on the solar distance.
|
| 361 |
+
solar_factor = (1.0 / op.solar_distance_au) ** 2
|
| 362 |
+
sin_altitude = _get_solar_sin_altitude(
|
| 363 |
+
op, sin_latitude, cos_latitude, longitude
|
| 364 |
+
)
|
| 365 |
+
return tsi * solar_factor * jnp.maximum(sin_altitude, 0.0)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _get_integrated_radiation(
|
| 369 |
+
j2000_days: chex.Array,
|
| 370 |
+
sin_latitude: chex.Array,
|
| 371 |
+
cos_latitude: chex.Array,
|
| 372 |
+
longitude: chex.Array,
|
| 373 |
+
tsi: chex.Array,
|
| 374 |
+
integration_period: pd.Timedelta,
|
| 375 |
+
num_integration_bins: int,
|
| 376 |
+
) -> chex.Array:
|
| 377 |
+
"""Returns the TOA solar radiation flux integrated over a time period.
|
| 378 |
+
|
| 379 |
+
Integrates the instantaneous TOA solar radiation flux over a time period.
|
| 380 |
+
The input timestamps represent the end times of each integration period.
|
| 381 |
+
When the integration period is one hour this approximates the
|
| 382 |
+
`toa_incident_solar_radiation` (or `tisr`) parameter from the ERA5 dataset.
|
| 383 |
+
See https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation and
|
| 384 |
+
https://codes.ecmwf.int/grib/param-db/?id=212.
|
| 385 |
+
|
| 386 |
+
All inputs are assumed to be broadcastable together using standard NumPy
|
| 387 |
+
rules. To approximate the integral, the instantaneous radiation is computed
|
| 388 |
+
at `num_integration_bins+1` time steps using `_get_radiation_flux` and
|
| 389 |
+
integrated using the trapezoidal rule. A dimension is appended at the end
|
| 390 |
+
of all inputs to compute the instantaneous radiation, which is then integrated
|
| 391 |
+
over to compute the final result.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
j2000_days: Timestamps represented as the number of days since the J2000
|
| 395 |
+
epoch. These correspond to the end times of each integration period.
|
| 396 |
+
sin_latitude: Sine of latitude coordinates.
|
| 397 |
+
cos_latitude: Cosine of latitude coordinates.
|
| 398 |
+
longitude: Longitude in radians.
|
| 399 |
+
tsi: Total Solar Irradiance (TSI) in W⋅m⁻².
|
| 400 |
+
integration_period: Integration period.
|
| 401 |
+
num_integration_bins: Number of bins to divide the `integration_period` to
|
| 402 |
+
approximate the integral using the trapezoidal rule.
|
| 403 |
+
|
| 404 |
+
Returns:
|
| 405 |
+
The TOA solar radiation flux integrated over the requested time period for
|
| 406 |
+
the given timestamps and geographical coordinates. Unit is J⋅m⁻² .
|
| 407 |
+
"""
|
| 408 |
+
# Offsets for the integration time steps.
|
| 409 |
+
offsets = (
|
| 410 |
+
pd.timedelta_range(
|
| 411 |
+
start=-integration_period,
|
| 412 |
+
end=pd.Timedelta(0),
|
| 413 |
+
periods=num_integration_bins + 1,
|
| 414 |
+
)
|
| 415 |
+
/ pd.Timedelta(days=1)
|
| 416 |
+
).to_numpy()
|
| 417 |
+
|
| 418 |
+
# Integration happens over the time dimension. Compute the instantaneous
|
| 419 |
+
# radiation flux for all the integration time steps by appending a dimension
|
| 420 |
+
# to all the inputs and adding `offsets` to `j2000_days` (will be broadcast
|
| 421 |
+
# over all the other dimensions).
|
| 422 |
+
fluxes = _get_radiation_flux(
|
| 423 |
+
j2000_days=jnp.expand_dims(j2000_days, axis=-1) + offsets,
|
| 424 |
+
sin_latitude=jnp.expand_dims(sin_latitude, axis=-1),
|
| 425 |
+
cos_latitude=jnp.expand_dims(cos_latitude, axis=-1),
|
| 426 |
+
longitude=jnp.expand_dims(longitude, axis=-1),
|
| 427 |
+
tsi=jnp.expand_dims(tsi, axis=-1),
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# Size of each bin in seconds. The instantaneous solar radiation flux is
|
| 431 |
+
# returned in units of W⋅m⁻². Integrating over time expressed in seconds
|
| 432 |
+
# yields a result in units of J⋅m⁻².
|
| 433 |
+
dx = (integration_period / num_integration_bins) / pd.Timedelta(seconds=1)
|
| 434 |
+
return jax.scipy.integrate.trapezoid(fluxes, dx=dx)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
_get_integrated_radiation_jitted = jax.jit(
|
| 438 |
+
_get_integrated_radiation,
|
| 439 |
+
static_argnames=["integration_period", "num_integration_bins"],
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def get_toa_incident_solar_radiation(
|
| 444 |
+
timestamps: Sequence[_TimestampLike],
|
| 445 |
+
latitude: chex.Array,
|
| 446 |
+
longitude: chex.Array,
|
| 447 |
+
tsi_data: xa.DataArray | None = None,
|
| 448 |
+
integration_period: _TimedeltaLike = _DEFAULT_INTEGRATION_PERIOD,
|
| 449 |
+
num_integration_bins: int = _DEFAULT_NUM_INTEGRATION_BINS,
|
| 450 |
+
use_jit: bool = False,
|
| 451 |
+
) -> chex.Array:
|
| 452 |
+
"""Computes the solar radiation incident at the top of the atmosphere.
|
| 453 |
+
|
| 454 |
+
The solar radiation is computed for each element in `timestamps` for all the
|
| 455 |
+
locations on the grid determined by the `latitude` and `longitude` parameters.
|
| 456 |
+
|
| 457 |
+
To approximate the `toa_incident_solar_radiation` (or `tisr`) parameter from
|
| 458 |
+
the ERA5 dataset, set `integration_period` to one hour (default). See
|
| 459 |
+
https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation and
|
| 460 |
+
https://codes.ecmwf.int/grib/param-db/?id=212.
|
| 461 |
+
|
| 462 |
+
Args:
|
| 463 |
+
timestamps: Timestamps for which to compute the solar radiation.
|
| 464 |
+
latitude: The latitude coordinates in degrees of the grid for which to
|
| 465 |
+
compute the solar radiation.
|
| 466 |
+
longitude: The longitude coordinates in degrees of the grid for which to
|
| 467 |
+
compute the solar radiation.
|
| 468 |
+
tsi_data: A DataArray containing yearly TSI data as returned by a
|
| 469 |
+
`TsiDataLoader`. The default is to use ERA5 compatible TSI data.
|
| 470 |
+
integration_period: Timedelta to use to integrate the radiation, e.g. if
|
| 471 |
+
producing radiation for 1989-11-08 21:00:00, and `integration_period` is
|
| 472 |
+
"1h", radiation will be integrated from 1989-11-08 20:00:00 to 1989-11-08
|
| 473 |
+
21:00:00. The default value ("1h") matches ERA5.
|
| 474 |
+
num_integration_bins: Number of equally spaced bins to divide the
|
| 475 |
+
`integration_period` in when approximating the integral using the
|
| 476 |
+
trapezoidal rule. Performance and peak memory usage are affected by this
|
| 477 |
+
value. The default (360) provides a good approximation, but lower values
|
| 478 |
+
may work to improve performance and reduce memory usage.
|
| 479 |
+
use_jit: Set to True to use the jitted implementation, or False (default) to
|
| 480 |
+
use the non-jitted one.
|
| 481 |
+
|
| 482 |
+
Returns:
|
| 483 |
+
An 3D array with dimensions (time, lat, lon) containing the total
|
| 484 |
+
top of atmosphere solar radiation integrated for the `integration_period`
|
| 485 |
+
up to each timestamp.
|
| 486 |
+
"""
|
| 487 |
+
# Add a trailing dimension to latitude to get dimensions (lat, lon).
|
| 488 |
+
lat = jnp.radians(latitude).reshape((-1, 1))
|
| 489 |
+
lon = jnp.radians(longitude)
|
| 490 |
+
sin_lat = jnp.sin(lat)
|
| 491 |
+
cos_lat = jnp.cos(lat)
|
| 492 |
+
integration_period = pd.Timedelta(integration_period)
|
| 493 |
+
if tsi_data is None:
|
| 494 |
+
tsi_data = _DEFAULT_TSI_DATA_LOADER()
|
| 495 |
+
tsi = get_tsi(timestamps, tsi_data)
|
| 496 |
+
fn = (
|
| 497 |
+
_get_integrated_radiation_jitted if use_jit else _get_integrated_radiation
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
# Compute integral for each timestamp individually. Although this could be
|
| 501 |
+
# done in one step, peak memory usage would be proportional to
|
| 502 |
+
# `len(timestamps) * num_integration_bins`. Computing each timestamp
|
| 503 |
+
# individually reduces this to `max(len(timestamps), num_integration_bins)`.
|
| 504 |
+
# E.g. memory usage for a single timestamp, with a full 0.25° grid and 360
|
| 505 |
+
# integration bins is about 1.5 GB (1440 * 721 * 361 * 4 bytes); computing
|
| 506 |
+
# forcings for 40 prediction steps would require 60 GB.
|
| 507 |
+
results = []
|
| 508 |
+
for idx, timestamp in enumerate(timestamps):
|
| 509 |
+
results.append(
|
| 510 |
+
fn(
|
| 511 |
+
j2000_days=jnp.array(_get_j2000_days(pd.Timestamp(timestamp))),
|
| 512 |
+
sin_latitude=sin_lat,
|
| 513 |
+
cos_latitude=cos_lat,
|
| 514 |
+
longitude=lon,
|
| 515 |
+
tsi=tsi[idx],
|
| 516 |
+
integration_period=integration_period,
|
| 517 |
+
num_integration_bins=num_integration_bins,
|
| 518 |
+
)
|
| 519 |
+
)
|
| 520 |
+
return jnp.stack(results, axis=0)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def get_toa_incident_solar_radiation_for_xarray(
|
| 524 |
+
data_array_like: xa.DataArray | xa.Dataset,
|
| 525 |
+
tsi_data: xa.DataArray | None = None,
|
| 526 |
+
integration_period: _TimedeltaLike = _DEFAULT_INTEGRATION_PERIOD,
|
| 527 |
+
num_integration_bins: int = _DEFAULT_NUM_INTEGRATION_BINS,
|
| 528 |
+
use_jit: bool = False,
|
| 529 |
+
) -> xa.DataArray:
|
| 530 |
+
"""Computes the solar radiation incident at the top of the atmosphere.
|
| 531 |
+
|
| 532 |
+
This method is a wrapper for `get_toa_incident_solar_radiation` using
|
| 533 |
+
coordinates from an Xarray and returning an Xarray.
|
| 534 |
+
|
| 535 |
+
Args:
|
| 536 |
+
data_array_like: A xa.Dataset or xa.DataArray from which to take the time
|
| 537 |
+
and spatial coordinates for which to compute the solar radiation. It must
|
| 538 |
+
contain `lat` and `lon` spatial dimensions with corresponding coordinates.
|
| 539 |
+
If a `time` dimension is present, the `datetime` coordinate should be a
|
| 540 |
+
vector associated with that dimension containing timestamps for which to
|
| 541 |
+
compute the solar radiation. Otherwise, the `datetime` coordinate should
|
| 542 |
+
be a scalar representing the timestamp for which to compute the solar
|
| 543 |
+
radiation.
|
| 544 |
+
tsi_data: A DataArray containing yearly TSI data as returned by a
|
| 545 |
+
`TsiDataLoader`. The default is to use ERA5 compatible TSI data.
|
| 546 |
+
integration_period: Timedelta to use to integrate the radiation, e.g. if
|
| 547 |
+
producing radiation for 1989-11-08 21:00:00, and `integration_period` is
|
| 548 |
+
"1h", radiation will be integrated from 1989-11-08 20:00:00 to 1989-11-08
|
| 549 |
+
21:00:00. The default value ("1h") matches ERA5.
|
| 550 |
+
num_integration_bins: Number of equally spaced bins to divide the
|
| 551 |
+
`integration_period` in when approximating the integral using the
|
| 552 |
+
trapezoidal rule. Performance and peak memory usage are affected by this
|
| 553 |
+
value. The default (360) provides a good approximation, but lower values
|
| 554 |
+
may work to improve performance and reduce memory usage.
|
| 555 |
+
use_jit: Set to True to use the jitted implementation, or False to use the
|
| 556 |
+
non-jitted one.
|
| 557 |
+
|
| 558 |
+
Returns:
|
| 559 |
+
xa.DataArray with dimensions `(time, lat, lon)` if `data_array_like` had
|
| 560 |
+
a `time` dimension; or dimensions `(lat, lon)` otherwise. The `datetime`
|
| 561 |
+
coordinates and those for the dimensions are copied to the returned array.
|
| 562 |
+
The array contains the total top of atmosphere solar radiation integrated
|
| 563 |
+
for `integration_period` up to the corresponding `datetime`.
|
| 564 |
+
|
| 565 |
+
Raises:
|
| 566 |
+
ValueError: If there are missing coordinates or dimensions.
|
| 567 |
+
"""
|
| 568 |
+
missing_dims = set(["lat", "lon"]) - set(data_array_like.dims)
|
| 569 |
+
if missing_dims:
|
| 570 |
+
raise ValueError(
|
| 571 |
+
f"'{missing_dims}' dimensions are missing in `data_array_like`."
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
missing_coords = set(["datetime", "lat", "lon"]) - set(data_array_like.coords)
|
| 575 |
+
if missing_coords:
|
| 576 |
+
raise ValueError(
|
| 577 |
+
f"'{missing_coords}' coordinates are missing in `data_array_like`."
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
if "time" in data_array_like.dims:
|
| 581 |
+
timestamps = data_array_like.coords["datetime"].data
|
| 582 |
+
else:
|
| 583 |
+
timestamps = [data_array_like.coords["datetime"].data.item()]
|
| 584 |
+
|
| 585 |
+
radiation = get_toa_incident_solar_radiation(
|
| 586 |
+
timestamps=timestamps,
|
| 587 |
+
latitude=data_array_like.coords["lat"].data,
|
| 588 |
+
longitude=data_array_like.coords["lon"].data,
|
| 589 |
+
tsi_data=tsi_data,
|
| 590 |
+
integration_period=integration_period,
|
| 591 |
+
num_integration_bins=num_integration_bins,
|
| 592 |
+
use_jit=use_jit,
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
if "time" in data_array_like.dims:
|
| 596 |
+
output = xa.DataArray(radiation, dims=("time", "lat", "lon"))
|
| 597 |
+
else:
|
| 598 |
+
output = xa.DataArray(radiation[0], dims=("lat", "lon"))
|
| 599 |
+
|
| 600 |
+
# Preserve as many of the original coordinates as possible, so long as the
|
| 601 |
+
# dimension or the coordinate still exist in the output array.
|
| 602 |
+
for k, coord in data_array_like.coords.items():
|
| 603 |
+
if set(coord.dims).issubset(set(output.dims)):
|
| 604 |
+
output.coords[k] = coord
|
| 605 |
+
return output
|
model/graphcast/sparse_transformer.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Transformer with either dense or sparse attention.
|
| 15 |
+
|
| 16 |
+
The sparse attention implemented here is for nodes to attend only to themselves
|
| 17 |
+
and their neighbours on the graph). It assumes that the adjacency matrix has a
|
| 18 |
+
banded structure, and is implemented with dense operations computing with only
|
| 19 |
+
the diagonal, super diagonal, and subdiagonal blocks of the tri-block-diagonal
|
| 20 |
+
attention matrix.
|
| 21 |
+
|
| 22 |
+
The basic model structure of the transformer and some functions were adapted
|
| 23 |
+
from xlm's transformer_simple.py.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import dataclasses
|
| 27 |
+
import logging
|
| 28 |
+
from typing import Any, Callable, Literal, Optional, Tuple
|
| 29 |
+
|
| 30 |
+
from . import mlp as mlp_builder
|
| 31 |
+
from . import sparse_transformer_utils as utils
|
| 32 |
+
import haiku as hk
|
| 33 |
+
import jax
|
| 34 |
+
from jax.experimental.pallas.ops.tpu import splash_attention
|
| 35 |
+
import jax.numpy as jnp
|
| 36 |
+
import numpy as np
|
| 37 |
+
import scipy as sp
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclasses.dataclass
|
| 41 |
+
class _ModelConfig:
|
| 42 |
+
"""Transformer config."""
|
| 43 |
+
# Depth, or num transformer blocks. One 'layer' is attn + ffw.
|
| 44 |
+
num_layers: int
|
| 45 |
+
# Primary width, the number of channels on the carrier path.
|
| 46 |
+
d_model: int
|
| 47 |
+
# Number of heads for self-attention.
|
| 48 |
+
num_heads: int
|
| 49 |
+
# Mask block size.
|
| 50 |
+
mask_block_size: int
|
| 51 |
+
# Attention type - 'mha' or 'triblockdiag_mha'
|
| 52 |
+
attention_type: str = 'triblockdiag_mha'
|
| 53 |
+
block_q: Optional[int] = None
|
| 54 |
+
block_kv: Optional[int] = None
|
| 55 |
+
block_kv_compute: Optional[int] = None
|
| 56 |
+
block_q_dkv: Optional[int] = None
|
| 57 |
+
block_kv_dkv: Optional[int] = None
|
| 58 |
+
block_kv_dkv_compute: Optional[int] = None
|
| 59 |
+
# mask type if splash attention being used - 'full' or 'lazy'
|
| 60 |
+
mask_type: Optional[str] = 'full'
|
| 61 |
+
# Number of channels per-head for self-attn QK computation.
|
| 62 |
+
key_size: Optional[int] = None
|
| 63 |
+
# Number of channels per-head for self-attn V computation.
|
| 64 |
+
value_size: Optional[int] = None
|
| 65 |
+
# Activation to use, any in jax.nn.
|
| 66 |
+
activation: str = 'gelu'
|
| 67 |
+
# Init scale for ffw layers (divided by num_layers)
|
| 68 |
+
ffw_winit_mult: float = 2.0
|
| 69 |
+
# Init scale for final ffw layer (divided by depth)
|
| 70 |
+
ffw_winit_final_mult: float = 2.0
|
| 71 |
+
# Init scale for mha proj (divided by depth).
|
| 72 |
+
attn_winit_mult: float = 2.0
|
| 73 |
+
# Init scale for mha w (divided by depth).
|
| 74 |
+
attn_winit_final_mult: float = 2.0
|
| 75 |
+
# Number of hidden units in the MLP blocks. Defaults to 4 * d_model.
|
| 76 |
+
ffw_hidden: Optional[int] = None
|
| 77 |
+
|
| 78 |
+
def __post_init__(self):
|
| 79 |
+
if self.ffw_hidden is None:
|
| 80 |
+
self.ffw_hidden = 4 * self.d_model
|
| 81 |
+
# Compute key_size and value_size from d_model // num_heads.
|
| 82 |
+
if self.key_size is None:
|
| 83 |
+
if self.d_model % self.num_heads != 0:
|
| 84 |
+
raise ValueError('num_heads has to divide d_model exactly')
|
| 85 |
+
self.key_size = self.d_model // self.num_heads
|
| 86 |
+
if self.value_size is None:
|
| 87 |
+
if self.d_model % self.num_heads != 0:
|
| 88 |
+
raise ValueError('num_heads has to divide d_model exactly')
|
| 89 |
+
self.value_size = self.d_model // self.num_heads
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def get_mask_block_size(mask: sp.sparse.csr_matrix) -> int:
|
| 93 |
+
"""Get blocksize of the adjacency matrix (attn mask) for the permuted mesh."""
|
| 94 |
+
# sub-diagonal bandwidth
|
| 95 |
+
lbandwidth = (
|
| 96 |
+
np.arange(mask.shape[0]) - (mask != 0).argmax(axis=0) + 1).max()
|
| 97 |
+
# super-diagonal bandwidth
|
| 98 |
+
ubandwidth = (
|
| 99 |
+
(mask.shape[0]-1) - np.argmax(mask[::-1,:] != 0, axis=0
|
| 100 |
+
) - np.arange(mask.shape[0]) + 1).max()
|
| 101 |
+
block_size = np.maximum(lbandwidth, ubandwidth)
|
| 102 |
+
return block_size
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def ffw(x: jnp.ndarray, cfg: _ModelConfig) -> jnp.ndarray:
|
| 106 |
+
"""Feed-forward block."""
|
| 107 |
+
ffw_winit = hk.initializers.VarianceScaling(cfg.ffw_winit_mult /
|
| 108 |
+
cfg.num_layers)
|
| 109 |
+
ffw_winit_final = hk.initializers.VarianceScaling(cfg.ffw_winit_final_mult /
|
| 110 |
+
cfg.num_layers)
|
| 111 |
+
x = hk.Linear(cfg.ffw_hidden, name='ffw_up', w_init=ffw_winit)(x)
|
| 112 |
+
x = getattr(jax.nn, cfg.activation)(x)
|
| 113 |
+
return hk.Linear(cfg.d_model, name='ffw_down', w_init=ffw_winit_final)(x)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def triblockdiag_softmax(logits: Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]
|
| 117 |
+
) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray]:
|
| 118 |
+
"""Softmax given the diag, upper diag, and lower diag logit blocks."""
|
| 119 |
+
|
| 120 |
+
logits_d, logits_u, logits_l = logits
|
| 121 |
+
|
| 122 |
+
m = jnp.max(jnp.stack([
|
| 123 |
+
jax.lax.stop_gradient(logits_d.max(-1, keepdims=True)),
|
| 124 |
+
jax.lax.stop_gradient(logits_u.max(-1, keepdims=True)),
|
| 125 |
+
jax.lax.stop_gradient(logits_l.max(-1, keepdims=True))]), axis=0)
|
| 126 |
+
|
| 127 |
+
unnormalized_d = jnp.exp(logits_d - m)
|
| 128 |
+
unnormalized_u = jnp.exp(logits_u - m)
|
| 129 |
+
unnormalized_l = jnp.exp(logits_l - m)
|
| 130 |
+
|
| 131 |
+
denom = (
|
| 132 |
+
unnormalized_d.sum(-1, keepdims=True)
|
| 133 |
+
+ unnormalized_u.sum(-1, keepdims=True)
|
| 134 |
+
+ unnormalized_l.sum(-1, keepdims=True)
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
logits_d = unnormalized_d / denom
|
| 138 |
+
logits_u = unnormalized_u / denom
|
| 139 |
+
logits_l = unnormalized_l / denom
|
| 140 |
+
|
| 141 |
+
return (logits_d, logits_u, logits_l)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def triblockdiag_mha(q_input: jnp.ndarray, kv_input: jnp.ndarray,
|
| 145 |
+
mask: jnp.ndarray, cfg: _ModelConfig,
|
| 146 |
+
) -> jnp.ndarray:
|
| 147 |
+
"""Triblockdiag multihead attention."""
|
| 148 |
+
|
| 149 |
+
# q_inputs, kv_input: (batch, num_blocks, block_size, num_heads, d_model)
|
| 150 |
+
q = multihead_linear(q_input, 'q', cfg)
|
| 151 |
+
k = multihead_linear(kv_input, 'k', cfg)
|
| 152 |
+
v = multihead_linear(kv_input, 'v', cfg)
|
| 153 |
+
|
| 154 |
+
k = jnp.pad(k, ((0, 0), (1, 1), (0, 0), (0, 0), (0, 0)))
|
| 155 |
+
v = jnp.pad(v, ((0, 0), (1, 1), (0, 0), (0, 0), (0, 0)))
|
| 156 |
+
|
| 157 |
+
def qk_prod(queries, keys):
|
| 158 |
+
return jnp.einsum('bnqhd,bnkhd->bnhqk', queries, keys)
|
| 159 |
+
|
| 160 |
+
# q shape is (batch, num_blocks, block_size, num_heads, qk_dim)
|
| 161 |
+
# k shape is (batch, num_blocks + 2, block_size, num_heads, qk_dim)
|
| 162 |
+
logits_d = qk_prod(q, k[:, 1:-1, ...]) * cfg.key_size**-0.5
|
| 163 |
+
logits_u = qk_prod(q, k[:, 2:, ...]) * cfg.key_size**-0.5
|
| 164 |
+
logits_l = qk_prod(q, k[:, :-2, ...]) * cfg.key_size**-0.5
|
| 165 |
+
|
| 166 |
+
# apply mask
|
| 167 |
+
logits_d = jnp.where(mask[:, 0, ...], logits_d, -1e30)
|
| 168 |
+
logits_u = jnp.where(mask[:, 1, ...], logits_u, -1e30)
|
| 169 |
+
logits_l = jnp.where(mask[:, 2, ...], logits_l, -1e30)
|
| 170 |
+
|
| 171 |
+
logits_d, logits_u, logits_l = utils.wrap_fn_for_upcast_downcast(
|
| 172 |
+
(logits_d, logits_u, logits_l),
|
| 173 |
+
triblockdiag_softmax
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
def av_prod(attn_weights, values):
|
| 177 |
+
return jnp.einsum('bnhqk,bnkhd->bnqhd', attn_weights, values)
|
| 178 |
+
|
| 179 |
+
out_d = av_prod(logits_d, v[:, 1:-1, ...])
|
| 180 |
+
out_u = av_prod(logits_u, v[:, 2:, ...])
|
| 181 |
+
out_l = av_prod(logits_l, v[:, :-2, ...])
|
| 182 |
+
# x shape is (batch, num_blocks, block_size, num_heads, d_model)
|
| 183 |
+
x = out_d + out_u + out_l
|
| 184 |
+
|
| 185 |
+
x = jnp.reshape(x, x.shape[:-2] + (cfg.num_heads * cfg.value_size,))
|
| 186 |
+
attn_winit_final = hk.initializers.VarianceScaling(
|
| 187 |
+
cfg.attn_winit_final_mult / cfg.num_layers)
|
| 188 |
+
x = hk.Linear(cfg.d_model, name='mha_final', w_init=attn_winit_final)(x)
|
| 189 |
+
return x
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def multihead_linear(
|
| 193 |
+
x: jnp.ndarray, qkv: str, cfg: _ModelConfig
|
| 194 |
+
) -> jnp.ndarray:
|
| 195 |
+
"""Linearly project `x` to have `head_size` dimensions per head."""
|
| 196 |
+
head_size = cfg.value_size if qkv == 'v' else cfg.key_size
|
| 197 |
+
attn_winit = hk.initializers.VarianceScaling(cfg.attn_winit_mult /
|
| 198 |
+
cfg.num_layers)
|
| 199 |
+
out = hk.Linear(
|
| 200 |
+
cfg.num_heads * head_size,
|
| 201 |
+
w_init=attn_winit,
|
| 202 |
+
name='mha_proj_' + qkv,
|
| 203 |
+
with_bias=False,
|
| 204 |
+
)(x)
|
| 205 |
+
shape = out.shape[:-1] + (cfg.num_heads, head_size)
|
| 206 |
+
return jnp.reshape(out, shape)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def mha(q_input: jnp.ndarray, kv_input: jnp.ndarray,
|
| 210 |
+
mask: jnp.ndarray, cfg: _ModelConfig,
|
| 211 |
+
normalize_logits: bool = True,
|
| 212 |
+
) -> jnp.ndarray:
|
| 213 |
+
"""Multi head attention."""
|
| 214 |
+
|
| 215 |
+
q = multihead_linear(q_input, 'q', cfg)
|
| 216 |
+
k = multihead_linear(kv_input, 'k', cfg)
|
| 217 |
+
v = multihead_linear(kv_input, 'v', cfg)
|
| 218 |
+
|
| 219 |
+
logits = jnp.einsum('bthd, bThd->bhtT', q, k)
|
| 220 |
+
if normalize_logits:
|
| 221 |
+
logits *= cfg.key_size**-0.5
|
| 222 |
+
if mask is not None:
|
| 223 |
+
def apply_mask(m, l):
|
| 224 |
+
return jnp.where(m, l, -1e30)
|
| 225 |
+
logits = jax.vmap(jax.vmap(
|
| 226 |
+
apply_mask, in_axes=[None, 0]), in_axes=[None, 0])(mask, logits)
|
| 227 |
+
|
| 228 |
+
# Wrap softmax weights for upcasting & downcasting in case of BF16 activations
|
| 229 |
+
weights = utils.wrap_fn_for_upcast_downcast(logits, jax.nn.softmax)
|
| 230 |
+
|
| 231 |
+
# Note: our mask never has all 0 rows, since nodes always have self edges,
|
| 232 |
+
# so no need to account for that possibility explicitly.
|
| 233 |
+
|
| 234 |
+
x = jnp.einsum('bhtT,bThd->bthd', weights, v)
|
| 235 |
+
x = jnp.reshape(x, x.shape[:-2] + (cfg.num_heads * cfg.value_size,))
|
| 236 |
+
|
| 237 |
+
attn_winit_final = hk.initializers.VarianceScaling(
|
| 238 |
+
cfg.attn_winit_final_mult / cfg.num_layers)
|
| 239 |
+
|
| 240 |
+
x = hk.Linear(cfg.d_model, name='mha_final', w_init=attn_winit_final)(x)
|
| 241 |
+
return x
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _make_splash_mha(
|
| 245 |
+
mask,
|
| 246 |
+
mask_type: str,
|
| 247 |
+
num_heads: int,
|
| 248 |
+
block_q: Optional[int] = None,
|
| 249 |
+
block_kv: Optional[int] = None,
|
| 250 |
+
block_kv_compute: Optional[int] = None,
|
| 251 |
+
block_q_dkv: Optional[int] = None,
|
| 252 |
+
block_kv_dkv: Optional[int] = None,
|
| 253 |
+
block_kv_dkv_compute: Optional[int] = None,
|
| 254 |
+
tanh_soft_cap: Optional[float] = None,
|
| 255 |
+
) -> Callable[..., jnp.ndarray]:
|
| 256 |
+
"""Construct attention kernel."""
|
| 257 |
+
if mask_type == 'full':
|
| 258 |
+
mask = np.broadcast_to(mask[None],
|
| 259 |
+
(num_heads, *mask.shape)).astype(np.bool_)
|
| 260 |
+
|
| 261 |
+
block_sizes = splash_attention.BlockSizes(
|
| 262 |
+
block_q=block_q,
|
| 263 |
+
block_kv=block_kv,
|
| 264 |
+
block_kv_compute=block_kv_compute,
|
| 265 |
+
block_q_dkv=block_q_dkv,
|
| 266 |
+
block_kv_dkv=block_kv_dkv,
|
| 267 |
+
block_kv_dkv_compute=block_kv_dkv_compute,
|
| 268 |
+
use_fused_bwd_kernel=True,
|
| 269 |
+
)
|
| 270 |
+
attn = splash_attention.make_splash_mha(mask, block_sizes=block_sizes,
|
| 271 |
+
head_shards=1,
|
| 272 |
+
q_seq_shards=1,
|
| 273 |
+
attn_logits_soft_cap=tanh_soft_cap,
|
| 274 |
+
)
|
| 275 |
+
return attn
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def splash_mha(q_input: jnp.ndarray, kv_input: jnp.ndarray,
|
| 279 |
+
mask: jnp.ndarray | splash_attention.splash_attention_mask.Mask,
|
| 280 |
+
cfg: _ModelConfig,
|
| 281 |
+
tanh_soft_cap: Optional[float] = None,
|
| 282 |
+
normalize_q: bool = True) -> jnp.ndarray:
|
| 283 |
+
"""Splash attention."""
|
| 284 |
+
|
| 285 |
+
q = multihead_linear(q_input, 'q', cfg)
|
| 286 |
+
k = multihead_linear(kv_input, 'k', cfg)
|
| 287 |
+
v = multihead_linear(kv_input, 'v', cfg)
|
| 288 |
+
|
| 289 |
+
_, _, num_heads, head_dim = q.shape
|
| 290 |
+
|
| 291 |
+
assert head_dim % 128 == 0 # splash attention kernel requires this
|
| 292 |
+
|
| 293 |
+
attn = _make_splash_mha(
|
| 294 |
+
mask=mask,
|
| 295 |
+
mask_type=cfg.mask_type,
|
| 296 |
+
num_heads=num_heads,
|
| 297 |
+
block_q=cfg.block_q,
|
| 298 |
+
block_kv=cfg.block_kv,
|
| 299 |
+
block_kv_compute=cfg.block_kv_compute,
|
| 300 |
+
block_q_dkv=cfg.block_q_dkv,
|
| 301 |
+
block_kv_dkv=cfg.block_kv_dkv,
|
| 302 |
+
block_kv_dkv_compute=cfg.block_kv_dkv_compute,
|
| 303 |
+
tanh_soft_cap=tanh_soft_cap,
|
| 304 |
+
)
|
| 305 |
+
attn = jax.vmap(attn) # Add batch axis.
|
| 306 |
+
|
| 307 |
+
if normalize_q:
|
| 308 |
+
q *= cfg.key_size**-0.5
|
| 309 |
+
|
| 310 |
+
# (batch, nodes, num_heads, head_dim) -> (batch, num_heads, nodes, head_dim)
|
| 311 |
+
reformat = lambda y: y.transpose(0, 2, 1, 3)
|
| 312 |
+
x = attn(q=reformat(q), k=reformat(k), v=reformat(v))
|
| 313 |
+
x = x.transpose(0, 2, 1, 3)
|
| 314 |
+
|
| 315 |
+
x = jnp.reshape(x, x.shape[:-2] + (cfg.num_heads * cfg.value_size,))
|
| 316 |
+
|
| 317 |
+
attn_winit_final = hk.initializers.VarianceScaling(
|
| 318 |
+
cfg.attn_winit_final_mult / cfg.num_layers)
|
| 319 |
+
|
| 320 |
+
x = hk.Linear(cfg.d_model, name='mha_final', w_init=attn_winit_final)(x)
|
| 321 |
+
return x
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def layernorm(
|
| 325 |
+
x: jnp.ndarray, create_scale: bool, create_offset: bool
|
| 326 |
+
) -> jnp.ndarray:
|
| 327 |
+
return hk.LayerNorm(
|
| 328 |
+
axis=-1, create_scale=create_scale, create_offset=create_offset,
|
| 329 |
+
name='norm')(x)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def mask_block_diags(mask: sp.sparse.csr_matrix,
|
| 333 |
+
num_padding_nodes: int,
|
| 334 |
+
block_size: int) -> jnp.ndarray:
|
| 335 |
+
"""Pad and reshape mask diag, super-siag and sub-diag blocks."""
|
| 336 |
+
# add zero padding to mask
|
| 337 |
+
mask_padding_rows = sp.sparse.csr_matrix(
|
| 338 |
+
(num_padding_nodes, mask.shape[1]), dtype=jnp.int32)
|
| 339 |
+
mask = sp.sparse.vstack([mask, mask_padding_rows])
|
| 340 |
+
mask_padding_cols = sp.sparse.csr_matrix(
|
| 341 |
+
(mask.shape[0], num_padding_nodes), dtype=jnp.int32)
|
| 342 |
+
mask = sp.sparse.hstack([mask, mask_padding_cols])
|
| 343 |
+
|
| 344 |
+
assert (mask.shape[-1] % block_size) == 0
|
| 345 |
+
mask_daig_blocks = jnp.stack(
|
| 346 |
+
[jnp.array(mask[i * block_size : (i + 1) * block_size,
|
| 347 |
+
i * block_size : (i + 1) * block_size,
|
| 348 |
+
].toarray())
|
| 349 |
+
for i in range(mask.shape[0] // block_size)])
|
| 350 |
+
mask_upper_diag_blocks = jnp.stack(
|
| 351 |
+
[jnp.array(mask[i * block_size : (i + 1) * block_size,
|
| 352 |
+
(i + 1) * block_size : (i + 2) * block_size,
|
| 353 |
+
].toarray())
|
| 354 |
+
for i in range(mask.shape[0] // block_size - 1)]
|
| 355 |
+
+ [jnp.zeros((block_size, block_size), dtype=mask.dtype)])
|
| 356 |
+
mask_lower_diag_blocks = jnp.stack(
|
| 357 |
+
[jnp.zeros((block_size, block_size), dtype=mask.dtype)]
|
| 358 |
+
+ [jnp.array(mask[(i + 1) * block_size : (i + 2) * block_size,
|
| 359 |
+
i * block_size : (i + 1) * block_size,
|
| 360 |
+
].toarray())
|
| 361 |
+
for i in range(mask.shape[0] // block_size - 1)])
|
| 362 |
+
mask = jnp.stack(
|
| 363 |
+
[mask_daig_blocks, mask_upper_diag_blocks, mask_lower_diag_blocks]
|
| 364 |
+
)
|
| 365 |
+
mask = jnp.expand_dims(mask, (0, 3))
|
| 366 |
+
return mask
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _pad_mask(mask, num_padding_nodes: Tuple[int, int]) -> jnp.ndarray:
|
| 370 |
+
q_padding, kv_padding = num_padding_nodes
|
| 371 |
+
mask_padding_rows = sp.sparse.csr_matrix(
|
| 372 |
+
(q_padding, mask.shape[1]), dtype=np.bool_)
|
| 373 |
+
mask = sp.sparse.vstack([mask, mask_padding_rows])
|
| 374 |
+
mask_padding_cols = sp.sparse.csr_matrix(
|
| 375 |
+
(mask.shape[0], kv_padding), dtype=np.bool_)
|
| 376 |
+
mask = sp.sparse.hstack([mask, mask_padding_cols])
|
| 377 |
+
return mask
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
class WeatherMeshMask(splash_attention.splash_attention_mask.Mask):
|
| 381 |
+
"""Lazy local mask, prevent attention to embeddings outside window.
|
| 382 |
+
|
| 383 |
+
Attributes:
|
| 384 |
+
mask:
|
| 385 |
+
"""
|
| 386 |
+
|
| 387 |
+
_shape: Tuple[int, int]
|
| 388 |
+
mask: sp.sparse.spmatrix
|
| 389 |
+
|
| 390 |
+
def __init__(
|
| 391 |
+
self,
|
| 392 |
+
mask: Any
|
| 393 |
+
):
|
| 394 |
+
self._shape = mask.shape
|
| 395 |
+
self.mask = mask
|
| 396 |
+
|
| 397 |
+
@property
|
| 398 |
+
def shape(self) -> Tuple[int, int]:
|
| 399 |
+
return self._shape
|
| 400 |
+
|
| 401 |
+
def __getitem__(self, idx) -> np.ndarray:
|
| 402 |
+
if len(idx) != 2:
|
| 403 |
+
raise NotImplementedError(f'Unsupported slice: {idx}')
|
| 404 |
+
q_slice, kv_slice = idx
|
| 405 |
+
if not isinstance(q_slice, slice) or not isinstance(kv_slice, slice):
|
| 406 |
+
raise NotImplementedError(f'Unsupported slice: {idx}')
|
| 407 |
+
|
| 408 |
+
return self.mask[q_slice, kv_slice].toarray()
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class Block(hk.Module):
|
| 412 |
+
"""Transformer block (mha and ffw)."""
|
| 413 |
+
|
| 414 |
+
def __init__(self, cfg, mask, num_nodes, num_padding_nodes, name=None):
|
| 415 |
+
super().__init__(name=name)
|
| 416 |
+
self._cfg = cfg
|
| 417 |
+
self.mask = mask
|
| 418 |
+
self.num_nodes = num_nodes
|
| 419 |
+
self.num_padding_nodes = num_padding_nodes
|
| 420 |
+
|
| 421 |
+
def __call__(self, x, global_norm_conditioning=jax.Array):
|
| 422 |
+
# x shape is (batch, num_nodes, feature_dim)
|
| 423 |
+
def attn(x):
|
| 424 |
+
if self._cfg.attention_type == 'triblockdiag_mha':
|
| 425 |
+
# We pad -> reshape -> compute attn -> reshape -> select at each block
|
| 426 |
+
# so as to avoid complications involved in making the norm layers and
|
| 427 |
+
# ffw blocks account for the padding. However, this might be decreasing
|
| 428 |
+
# efficiency.
|
| 429 |
+
|
| 430 |
+
# Add padding so that number of nodes is divisible into blocks
|
| 431 |
+
x = jnp.pad(x, ((0, 0), (0, self.num_padding_nodes), (0, 0)))
|
| 432 |
+
x = x.reshape(x.shape[0],
|
| 433 |
+
x.shape[1]//self._cfg.mask_block_size,
|
| 434 |
+
self._cfg.mask_block_size,
|
| 435 |
+
x.shape[-1])
|
| 436 |
+
x = triblockdiag_mha(x, x, mask=self.mask, cfg=self._cfg)
|
| 437 |
+
x = x.reshape(x.shape[0],
|
| 438 |
+
self.num_nodes + self.num_padding_nodes,
|
| 439 |
+
x.shape[-1])
|
| 440 |
+
return x[:,:self.num_nodes, :]
|
| 441 |
+
|
| 442 |
+
elif self._cfg.attention_type == 'mha':
|
| 443 |
+
return mha(x, x, mask=self.mask, cfg=self._cfg)
|
| 444 |
+
|
| 445 |
+
elif self._cfg.attention_type == 'splash_mha':
|
| 446 |
+
# We pad -> reshape -> compute attn -> reshape -> select at each block
|
| 447 |
+
# so as to avoid complications involved in making the norm layers and
|
| 448 |
+
# ffw blocks account for the padding. However, this might be decreasing
|
| 449 |
+
# efficiency.
|
| 450 |
+
|
| 451 |
+
# Add padding so that number of nodes is divisible by block sizes.
|
| 452 |
+
x = jnp.pad(x, ((0, 0), (0, self.num_padding_nodes[0]), (0, 0)))
|
| 453 |
+
x = splash_mha(x, x, mask=self.mask, cfg=self._cfg)
|
| 454 |
+
return x[:,:self.num_nodes, :]
|
| 455 |
+
|
| 456 |
+
else:
|
| 457 |
+
raise NotImplementedError()
|
| 458 |
+
|
| 459 |
+
def norm_conditioning_layer(x):
|
| 460 |
+
return mlp_builder.LinearNormConditioning(
|
| 461 |
+
name=self.name+'_norm_conditioning')(
|
| 462 |
+
x,
|
| 463 |
+
norm_conditioning=jnp.expand_dims(global_norm_conditioning, 1)
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
x = x + attn(
|
| 467 |
+
norm_conditioning_layer(
|
| 468 |
+
layernorm(x, create_scale=False, create_offset=False)
|
| 469 |
+
)
|
| 470 |
+
)
|
| 471 |
+
x = x + ffw(
|
| 472 |
+
norm_conditioning_layer(
|
| 473 |
+
layernorm(x, create_scale=False, create_offset=False)
|
| 474 |
+
),
|
| 475 |
+
self._cfg,
|
| 476 |
+
)
|
| 477 |
+
return x
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
class Transformer(hk.Module):
|
| 481 |
+
"""Main transformer module that processes embeddings.
|
| 482 |
+
|
| 483 |
+
All but the very first and very last layer of a 'classic' Transformer:
|
| 484 |
+
Receives already embedded inputs instead of discrete tokens.
|
| 485 |
+
Outputs an embedding for each 'node'/'position' rather than logits.
|
| 486 |
+
"""
|
| 487 |
+
|
| 488 |
+
def __init__(self,
|
| 489 |
+
adj_mat: sp.sparse.csr_matrix,
|
| 490 |
+
attention_k_hop: int,
|
| 491 |
+
attention_type: Literal['splash_mha', 'triblockdiag_mha', 'mha'],
|
| 492 |
+
mask_type: Literal['full', 'lazy'],
|
| 493 |
+
num_heads=1,
|
| 494 |
+
name=None,
|
| 495 |
+
block_q: Optional[int] = None,
|
| 496 |
+
block_kv: Optional[int] = None,
|
| 497 |
+
block_kv_compute: Optional[int] = None,
|
| 498 |
+
block_q_dkv: Optional[int] = None,
|
| 499 |
+
block_kv_dkv: Optional[int] = None,
|
| 500 |
+
block_kv_dkv_compute: Optional[int] = None,
|
| 501 |
+
**kwargs):
|
| 502 |
+
super().__init__(name=name)
|
| 503 |
+
|
| 504 |
+
# Construct mask and deduce block size.
|
| 505 |
+
mask = adj_mat ** attention_k_hop
|
| 506 |
+
mask_block_size = get_mask_block_size(mask)
|
| 507 |
+
logging.info('mask_block_size: %s.', mask_block_size)
|
| 508 |
+
|
| 509 |
+
if attention_type == 'triblockdiag_mha':
|
| 510 |
+
# we will stack the nodes in blocks of 'block_size' nodes, so we need to
|
| 511 |
+
# pad the input such that (num_nodes + num_padding_nodes) % block_size = 0
|
| 512 |
+
self.num_padding_nodes = int(np.ceil(
|
| 513 |
+
mask.shape[0]/mask_block_size)*mask_block_size
|
| 514 |
+
- mask.shape[0])
|
| 515 |
+
self.mask = mask_block_diags(
|
| 516 |
+
mask, self.num_padding_nodes, mask_block_size)
|
| 517 |
+
elif attention_type == 'splash_mha':
|
| 518 |
+
max_q_block_size = np.maximum(block_q, block_q_dkv)
|
| 519 |
+
max_kv_block_size = np.maximum(block_kv, block_kv_dkv)
|
| 520 |
+
q_padding = int(np.ceil(
|
| 521 |
+
mask.shape[0]/max_q_block_size)*max_q_block_size - mask.shape[0])
|
| 522 |
+
kv_padding = int(np.ceil(
|
| 523 |
+
mask.shape[1]/max_kv_block_size)*max_kv_block_size - mask.shape[1])
|
| 524 |
+
self.num_padding_nodes = (q_padding, kv_padding)
|
| 525 |
+
mask = _pad_mask(mask, self.num_padding_nodes)
|
| 526 |
+
if mask_type == 'lazy':
|
| 527 |
+
splash_mask = [
|
| 528 |
+
WeatherMeshMask(mask)
|
| 529 |
+
for _ in range(num_heads)
|
| 530 |
+
]
|
| 531 |
+
self.mask = splash_attention.splash_attention_mask.MultiHeadMask(
|
| 532 |
+
splash_mask)
|
| 533 |
+
elif mask_type == 'full':
|
| 534 |
+
self.mask = mask.toarray() # pytype: disable=attribute-error
|
| 535 |
+
elif attention_type == 'mha':
|
| 536 |
+
self.mask = jnp.array(mask.toarray())
|
| 537 |
+
self.num_padding_nodes = 0
|
| 538 |
+
else:
|
| 539 |
+
raise ValueError(
|
| 540 |
+
'Unsupported attention type: %s' % attention_type
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
# Construct config for use within class.
|
| 544 |
+
self._cfg = _ModelConfig(
|
| 545 |
+
mask_block_size=mask_block_size,
|
| 546 |
+
attention_type=attention_type,
|
| 547 |
+
mask_type=mask_type,
|
| 548 |
+
num_heads=num_heads,
|
| 549 |
+
block_q=block_q,
|
| 550 |
+
block_kv=block_kv,
|
| 551 |
+
block_kv_compute=block_kv_compute,
|
| 552 |
+
block_q_dkv=block_q_dkv,
|
| 553 |
+
block_kv_dkv=block_kv_dkv,
|
| 554 |
+
block_kv_dkv_compute=block_kv_dkv_compute,
|
| 555 |
+
**kwargs)
|
| 556 |
+
|
| 557 |
+
def __call__(self, node_features, global_norm_conditioning: jax.Array):
|
| 558 |
+
# node_features expected to have shape (batch, num_nodes, d)
|
| 559 |
+
x = node_features
|
| 560 |
+
for i_layer in range(self._cfg.num_layers):
|
| 561 |
+
x = Block(cfg=self._cfg, mask=self.mask,
|
| 562 |
+
num_nodes=node_features.shape[1],
|
| 563 |
+
num_padding_nodes=self.num_padding_nodes,
|
| 564 |
+
name='block_%02d' % i_layer
|
| 565 |
+
)(x, global_norm_conditioning=global_norm_conditioning)
|
| 566 |
+
|
| 567 |
+
def norm_conditioning_layer(x):
|
| 568 |
+
return mlp_builder.LinearNormConditioning(
|
| 569 |
+
name=self.name+'_final_norm_conditioning')(
|
| 570 |
+
x,
|
| 571 |
+
norm_conditioning=jnp.expand_dims(global_norm_conditioning, 1)
|
| 572 |
+
)
|
| 573 |
+
x = norm_conditioning_layer(
|
| 574 |
+
layernorm(x, create_scale=False, create_offset=False)
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
return x
|
model/graphcast/sparse_transformer_utils.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utils for training models in low precision."""
|
| 15 |
+
|
| 16 |
+
import functools
|
| 17 |
+
from typing import Callable, Tuple, Union
|
| 18 |
+
|
| 19 |
+
import jax
|
| 20 |
+
import jax.numpy as jnp
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Wrappers for jax.lax.reduce_precision which is non-differentiable.
|
| 24 |
+
@functools.partial(jax.custom_vjp, nondiff_argnums=(1, 2))
|
| 25 |
+
def reduce_precision(x, exponent_bits, mantissa_bits):
|
| 26 |
+
return jax.tree_util.tree_map(
|
| 27 |
+
lambda y: jax.lax.reduce_precision(y, exponent_bits, mantissa_bits), x)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def reduce_precision_fwd(x, exponent_bits, mantissa_bits):
|
| 31 |
+
return reduce_precision(x, exponent_bits, mantissa_bits), None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def reduce_precision_bwd(exponent_bits, mantissa_bits, res, dout):
|
| 35 |
+
del res # Unused.
|
| 36 |
+
return reduce_precision(dout, exponent_bits, mantissa_bits),
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
reduce_precision.defvjp(reduce_precision_fwd, reduce_precision_bwd)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def wrap_fn_for_upcast_downcast(inputs: Union[jnp.ndarray,
|
| 43 |
+
Tuple[jnp.ndarray, ...]],
|
| 44 |
+
fn: Callable[[Union[jnp.ndarray,
|
| 45 |
+
Tuple[jnp.ndarray, ...]]],
|
| 46 |
+
Union[jnp.ndarray,
|
| 47 |
+
Tuple[jnp.ndarray, ...]]],
|
| 48 |
+
f32_upcast: bool = True,
|
| 49 |
+
guard_against_excess_precision: bool = True
|
| 50 |
+
) -> Union[jnp.ndarray,
|
| 51 |
+
Tuple[jnp.ndarray, ...]]:
|
| 52 |
+
"""Wraps `fn` to upcast to float32 and then downcast, for use with BF16."""
|
| 53 |
+
# Do not upcast if the inputs are already in float32.
|
| 54 |
+
# This removes a no-op `jax.lax.reduce_precision` which is unsupported
|
| 55 |
+
# in jax2tf at the moment.
|
| 56 |
+
if isinstance(inputs, Tuple):
|
| 57 |
+
f32_upcast = f32_upcast and inputs[0].dtype != jnp.float32
|
| 58 |
+
orig_dtype = inputs[0].dtype
|
| 59 |
+
else:
|
| 60 |
+
f32_upcast = f32_upcast and inputs.dtype != jnp.float32
|
| 61 |
+
orig_dtype = inputs.dtype
|
| 62 |
+
|
| 63 |
+
if f32_upcast:
|
| 64 |
+
inputs = jax.tree_util.tree_map(lambda x: x.astype(jnp.float32), inputs)
|
| 65 |
+
|
| 66 |
+
if guard_against_excess_precision:
|
| 67 |
+
# This is evil magic to guard against differences in precision in the QK
|
| 68 |
+
# calculation between the forward pass and backwards pass. This is like
|
| 69 |
+
# --xla_allow_excess_precision=false but scoped here.
|
| 70 |
+
finfo = jnp.finfo(orig_dtype) # jnp important!
|
| 71 |
+
inputs = reduce_precision(inputs, finfo.nexp, finfo.nmant)
|
| 72 |
+
|
| 73 |
+
output = fn(inputs)
|
| 74 |
+
if f32_upcast:
|
| 75 |
+
output = jax.tree_util.tree_map(lambda x: x.astype(orig_dtype), output)
|
| 76 |
+
return output
|
model/graphcast/transformer.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""A Transformer model for weather predictions.
|
| 15 |
+
|
| 16 |
+
This model wraps the a transformer model and swaps the leading two axes of the
|
| 17 |
+
nodes in the input graph prior to evaluating the model to make it compatible
|
| 18 |
+
with a [nodes, batch, ...] ordering of the inputs.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from typing import Any, Mapping, Optional
|
| 22 |
+
|
| 23 |
+
from . import typed_graph
|
| 24 |
+
import haiku as hk
|
| 25 |
+
import jax
|
| 26 |
+
import jax.numpy as jnp
|
| 27 |
+
import numpy as np
|
| 28 |
+
from scipy import sparse
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
Kwargs = Mapping[str, Any]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _get_adj_matrix_for_edge_set(
|
| 35 |
+
graph: typed_graph.TypedGraph,
|
| 36 |
+
edge_set_name: str,
|
| 37 |
+
add_self_edges: bool,
|
| 38 |
+
):
|
| 39 |
+
"""Returns the adjacency matrix for the given graph and edge set."""
|
| 40 |
+
# Get nodes and edges of the graph.
|
| 41 |
+
edge_set_key = graph.edge_key_by_name(edge_set_name)
|
| 42 |
+
sender_node_set, receiver_node_set = edge_set_key.node_sets
|
| 43 |
+
|
| 44 |
+
# Compute number of sender and receiver nodes.
|
| 45 |
+
sender_n_node = graph.nodes[sender_node_set].n_node[0]
|
| 46 |
+
receiver_n_node = graph.nodes[receiver_node_set].n_node[0]
|
| 47 |
+
|
| 48 |
+
# Build adjacency matrix.
|
| 49 |
+
adj_mat = sparse.csr_matrix((sender_n_node, receiver_n_node), dtype=np.bool_)
|
| 50 |
+
edge_set = graph.edges[edge_set_key]
|
| 51 |
+
s, r = edge_set.indices
|
| 52 |
+
adj_mat[s, r] = True
|
| 53 |
+
if add_self_edges:
|
| 54 |
+
# Should only do this if we are certain the adjacency matrix is square.
|
| 55 |
+
assert sender_node_set == receiver_node_set
|
| 56 |
+
adj_mat[np.arange(sender_n_node), np.arange(receiver_n_node)] = True
|
| 57 |
+
return adj_mat
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class MeshTransformer(hk.Module):
|
| 61 |
+
"""A Transformer for inputs with ordering [nodes, batch, ...]."""
|
| 62 |
+
|
| 63 |
+
def __init__(self,
|
| 64 |
+
transformer_ctor,
|
| 65 |
+
transformer_kwargs: Kwargs,
|
| 66 |
+
name: Optional[str] = None):
|
| 67 |
+
"""Initialises the Transformer model.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
transformer_ctor: Constructor for transformer.
|
| 71 |
+
transformer_kwargs: Kwargs to pass to the transformer module.
|
| 72 |
+
name: Optional name for haiku module.
|
| 73 |
+
"""
|
| 74 |
+
super().__init__(name=name)
|
| 75 |
+
# We defer the transformer initialisation to the first call to __call__,
|
| 76 |
+
# where we can build the mask senders and receivers of the TypedGraph
|
| 77 |
+
self._batch_first_transformer = None
|
| 78 |
+
self._transformer_ctor = transformer_ctor
|
| 79 |
+
self._transformer_kwargs = transformer_kwargs
|
| 80 |
+
|
| 81 |
+
@hk.name_like('__init__')
|
| 82 |
+
def _maybe_init_batch_first_transformer(self, x: typed_graph.TypedGraph):
|
| 83 |
+
if self._batch_first_transformer is not None:
|
| 84 |
+
return
|
| 85 |
+
self._batch_first_transformer = self._transformer_ctor(
|
| 86 |
+
adj_mat=_get_adj_matrix_for_edge_set(
|
| 87 |
+
graph=x,
|
| 88 |
+
edge_set_name='mesh',
|
| 89 |
+
add_self_edges=True,
|
| 90 |
+
),
|
| 91 |
+
**self._transformer_kwargs,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
def __call__(
|
| 95 |
+
self, x: typed_graph.TypedGraph,
|
| 96 |
+
global_norm_conditioning: jax.Array
|
| 97 |
+
) -> typed_graph.TypedGraph:
|
| 98 |
+
"""Applies the model to the input graph and returns graph of same shape."""
|
| 99 |
+
|
| 100 |
+
if set(x.nodes.keys()) != {'mesh_nodes'}:
|
| 101 |
+
raise ValueError(
|
| 102 |
+
f'Expected x.nodes to have key `mesh_nodes`, got {x.nodes.keys()}.'
|
| 103 |
+
)
|
| 104 |
+
features = x.nodes['mesh_nodes'].features
|
| 105 |
+
if features.ndim != 3: # pytype: disable=attribute-error # jax-ndarray
|
| 106 |
+
raise ValueError(
|
| 107 |
+
'Expected `x.nodes["mesh_nodes"].features` to be 3, got'
|
| 108 |
+
f' {features.ndim}.'
|
| 109 |
+
) # pytype: disable=attribute-error # jax-ndarray
|
| 110 |
+
|
| 111 |
+
# Initialise transformer and mask.
|
| 112 |
+
self._maybe_init_batch_first_transformer(x)
|
| 113 |
+
|
| 114 |
+
y = jnp.transpose(features, axes=[1, 0, 2])
|
| 115 |
+
y = self._batch_first_transformer(y, global_norm_conditioning)
|
| 116 |
+
y = jnp.transpose(y, axes=[1, 0, 2])
|
| 117 |
+
x = x._replace(
|
| 118 |
+
nodes={
|
| 119 |
+
'mesh_nodes': x.nodes['mesh_nodes']._replace(
|
| 120 |
+
features=y.astype(features.dtype)
|
| 121 |
+
)
|
| 122 |
+
}
|
| 123 |
+
)
|
| 124 |
+
return x
|
model/graphcast/typed_graph.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Data-structure for storing graphs with typed edges and nodes."""
|
| 15 |
+
|
| 16 |
+
from typing import NamedTuple, Any, Union, Tuple, Mapping, TypeVar
|
| 17 |
+
|
| 18 |
+
ArrayLike = Union[Any] # np.ndarray, jnp.ndarray, tf.tensor
|
| 19 |
+
ArrayLikeTree = Union[Any, ArrayLike] # Nest of ArrayLike
|
| 20 |
+
|
| 21 |
+
_T = TypeVar('_T')
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# All tensors have a "flat_batch_axis", which is similar to the leading
|
| 25 |
+
# axes of graph_tuples:
|
| 26 |
+
# * In the case of nodes this is simply a shared node and flat batch axis, with
|
| 27 |
+
# size corresponding to the total number of nodes in the flattened batch.
|
| 28 |
+
# * In the case of edges this is simply a shared edge and flat batch axis, with
|
| 29 |
+
# size corresponding to the total number of edges in the flattened batch.
|
| 30 |
+
# * In the case of globals this is simply the number of graphs in the flattened
|
| 31 |
+
# batch.
|
| 32 |
+
|
| 33 |
+
# All shapes may also have any additional leading shape "batch_shape".
|
| 34 |
+
# Options for building batches are:
|
| 35 |
+
# * Use a provided "flatten" method that takes a leading `batch_shape` and
|
| 36 |
+
# it into the flat_batch_axis (this will be useful when using `tf.Dataset`
|
| 37 |
+
# which supports batching into RaggedTensors, with leading batch shape even
|
| 38 |
+
# if graphs have different numbers of nodes and edges), so the RaggedBatches
|
| 39 |
+
# can then be converted into something without ragged dimensions that jax can
|
| 40 |
+
# use.
|
| 41 |
+
# * Directly build a "flat batch" using a provided function for batching a list
|
| 42 |
+
# of graphs (how it is done in `jraph`).
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class NodeSet(NamedTuple):
|
| 46 |
+
"""Represents a set of nodes."""
|
| 47 |
+
n_node: ArrayLike # [num_flat_graphs]
|
| 48 |
+
features: ArrayLikeTree # Prev. `nodes`: [num_flat_nodes] + feature_shape
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class EdgesIndices(NamedTuple):
|
| 52 |
+
"""Represents indices to nodes adjacent to the edges."""
|
| 53 |
+
senders: ArrayLike # [num_flat_edges]
|
| 54 |
+
receivers: ArrayLike # [num_flat_edges]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class EdgeSet(NamedTuple):
|
| 58 |
+
"""Represents a set of edges."""
|
| 59 |
+
n_edge: ArrayLike # [num_flat_graphs]
|
| 60 |
+
indices: EdgesIndices
|
| 61 |
+
features: ArrayLikeTree # Prev. `edges`: [num_flat_edges] + feature_shape
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class Context(NamedTuple):
|
| 65 |
+
# `n_graph` always contains ones but it is useful to query the leading shape
|
| 66 |
+
# in case of graphs without any nodes or edges sets.
|
| 67 |
+
n_graph: ArrayLike # [num_flat_graphs]
|
| 68 |
+
features: ArrayLikeTree # Prev. `globals`: [num_flat_graphs] + feature_shape
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class EdgeSetKey(NamedTuple):
|
| 72 |
+
name: str # Name of the EdgeSet.
|
| 73 |
+
|
| 74 |
+
# Sender node set name and receiver node set name connected by the edge set.
|
| 75 |
+
node_sets: Tuple[str, str]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class TypedGraph(NamedTuple):
|
| 79 |
+
"""A graph with typed nodes and edges.
|
| 80 |
+
|
| 81 |
+
A typed graph is made of a context, multiple sets of nodes and multiple
|
| 82 |
+
sets of edges connecting those nodes (as indicated by the EdgeSetKey).
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
context: Context
|
| 86 |
+
nodes: Mapping[str, NodeSet]
|
| 87 |
+
edges: Mapping[EdgeSetKey, EdgeSet]
|
| 88 |
+
|
| 89 |
+
def edge_key_by_name(self, name: str) -> EdgeSetKey:
|
| 90 |
+
found_key = [k for k in self.edges.keys() if k.name == name]
|
| 91 |
+
if len(found_key) != 1:
|
| 92 |
+
raise KeyError("invalid edge key '{}'. Available edges: [{}]".format(
|
| 93 |
+
name, ', '.join(x.name for x in self.edges.keys())))
|
| 94 |
+
return found_key[0]
|
| 95 |
+
|
| 96 |
+
def edge_by_name(self, name: str) -> EdgeSet:
|
| 97 |
+
return self.edges[self.edge_key_by_name(name)]
|
model/graphcast/typed_graph_net.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""A library of typed Graph Neural Networks."""
|
| 15 |
+
|
| 16 |
+
from typing import Callable, Mapping, Optional, Union
|
| 17 |
+
|
| 18 |
+
from . import typed_graph
|
| 19 |
+
import jax.numpy as jnp
|
| 20 |
+
import jax.tree_util as tree
|
| 21 |
+
import jraph
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# All features will be an ArrayTree.
|
| 25 |
+
NodeFeatures = EdgeFeatures = SenderFeatures = ReceiverFeatures = Globals = (
|
| 26 |
+
jraph.ArrayTree)
|
| 27 |
+
|
| 28 |
+
# Signature:
|
| 29 |
+
# (node features, outgoing edge features, incoming edge features,
|
| 30 |
+
# globals) -> updated node features
|
| 31 |
+
GNUpdateNodeFn = Callable[
|
| 32 |
+
[NodeFeatures, Mapping[str, SenderFeatures], Mapping[str, ReceiverFeatures],
|
| 33 |
+
Globals],
|
| 34 |
+
NodeFeatures]
|
| 35 |
+
|
| 36 |
+
GNUpdateGlobalFn = Callable[
|
| 37 |
+
[Mapping[str, NodeFeatures], Mapping[str, EdgeFeatures], Globals],
|
| 38 |
+
Globals]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def GraphNetwork( # pylint: disable=invalid-name
|
| 42 |
+
update_edge_fn: Mapping[str, jraph.GNUpdateEdgeFn],
|
| 43 |
+
update_node_fn: Mapping[str, GNUpdateNodeFn],
|
| 44 |
+
update_global_fn: Optional[GNUpdateGlobalFn] = None,
|
| 45 |
+
aggregate_edges_for_nodes_fn: jraph.AggregateEdgesToNodesFn = jraph
|
| 46 |
+
.segment_sum,
|
| 47 |
+
aggregate_nodes_for_globals_fn: jraph.AggregateNodesToGlobalsFn = jraph
|
| 48 |
+
.segment_sum,
|
| 49 |
+
aggregate_edges_for_globals_fn: jraph.AggregateEdgesToGlobalsFn = jraph
|
| 50 |
+
.segment_sum,
|
| 51 |
+
):
|
| 52 |
+
"""Returns a method that applies a configured GraphNetwork.
|
| 53 |
+
|
| 54 |
+
This implementation follows Algorithm 1 in https://arxiv.org/abs/1806.01261
|
| 55 |
+
extended to Typed Graphs with multiple edge sets and node sets and extended to
|
| 56 |
+
allow aggregating not only edges received by the nodes, but also edges sent by
|
| 57 |
+
the nodes.
|
| 58 |
+
|
| 59 |
+
Example usage::
|
| 60 |
+
|
| 61 |
+
gn = GraphNetwork(update_edge_function,
|
| 62 |
+
update_node_function, **kwargs)
|
| 63 |
+
# Conduct multiple rounds of message passing with the same parameters:
|
| 64 |
+
for _ in range(num_message_passing_steps):
|
| 65 |
+
graph = gn(graph)
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
update_edge_fn: mapping of functions used to update a subset of the edge
|
| 69 |
+
types, indexed by edge type name.
|
| 70 |
+
update_node_fn: mapping of functions used to update a subset of the node
|
| 71 |
+
types, indexed by node type name.
|
| 72 |
+
update_global_fn: function used to update the globals or None to deactivate
|
| 73 |
+
globals updates.
|
| 74 |
+
aggregate_edges_for_nodes_fn: function used to aggregate messages to each
|
| 75 |
+
node.
|
| 76 |
+
aggregate_nodes_for_globals_fn: function used to aggregate the nodes for the
|
| 77 |
+
globals.
|
| 78 |
+
aggregate_edges_for_globals_fn: function used to aggregate the edges for the
|
| 79 |
+
globals.
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
A method that applies the configured GraphNetwork.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def _apply_graph_net(graph: typed_graph.TypedGraph) -> typed_graph.TypedGraph:
|
| 86 |
+
"""Applies a configured GraphNetwork to a graph.
|
| 87 |
+
|
| 88 |
+
This implementation follows Algorithm 1 in https://arxiv.org/abs/1806.01261
|
| 89 |
+
extended to Typed Graphs with multiple edge sets and node sets and extended
|
| 90 |
+
to allow aggregating not only edges received by the nodes, but also edges
|
| 91 |
+
sent by the nodes.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
graph: a `TypedGraph` containing the graph.
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Updated `TypedGraph`.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
updated_graph = graph
|
| 101 |
+
|
| 102 |
+
# Edge update.
|
| 103 |
+
updated_edges = dict(updated_graph.edges)
|
| 104 |
+
for edge_set_name, edge_fn in update_edge_fn.items():
|
| 105 |
+
edge_set_key = graph.edge_key_by_name(edge_set_name)
|
| 106 |
+
updated_edges[edge_set_key] = _edge_update(
|
| 107 |
+
updated_graph, edge_fn, edge_set_key)
|
| 108 |
+
updated_graph = updated_graph._replace(edges=updated_edges)
|
| 109 |
+
|
| 110 |
+
# Node update.
|
| 111 |
+
updated_nodes = dict(updated_graph.nodes)
|
| 112 |
+
for node_set_key, node_fn in update_node_fn.items():
|
| 113 |
+
updated_nodes[node_set_key] = _node_update(
|
| 114 |
+
updated_graph, node_fn, node_set_key, aggregate_edges_for_nodes_fn)
|
| 115 |
+
updated_graph = updated_graph._replace(nodes=updated_nodes)
|
| 116 |
+
|
| 117 |
+
# Global update.
|
| 118 |
+
if update_global_fn:
|
| 119 |
+
updated_context = _global_update(
|
| 120 |
+
updated_graph, update_global_fn,
|
| 121 |
+
aggregate_edges_for_globals_fn,
|
| 122 |
+
aggregate_nodes_for_globals_fn)
|
| 123 |
+
updated_graph = updated_graph._replace(context=updated_context)
|
| 124 |
+
|
| 125 |
+
return updated_graph
|
| 126 |
+
|
| 127 |
+
return _apply_graph_net
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _edge_update(graph, edge_fn, edge_set_key): # pylint: disable=invalid-name
|
| 131 |
+
"""Updates an edge set of a given key."""
|
| 132 |
+
|
| 133 |
+
sender_nodes = graph.nodes[edge_set_key.node_sets[0]]
|
| 134 |
+
receiver_nodes = graph.nodes[edge_set_key.node_sets[1]]
|
| 135 |
+
edge_set = graph.edges[edge_set_key]
|
| 136 |
+
senders = edge_set.indices.senders # pytype: disable=attribute-error
|
| 137 |
+
receivers = edge_set.indices.receivers # pytype: disable=attribute-error
|
| 138 |
+
|
| 139 |
+
sent_attributes = tree.tree_map(
|
| 140 |
+
lambda n: n[senders], sender_nodes.features)
|
| 141 |
+
received_attributes = tree.tree_map(
|
| 142 |
+
lambda n: n[receivers], receiver_nodes.features)
|
| 143 |
+
|
| 144 |
+
n_edge = edge_set.n_edge
|
| 145 |
+
sum_n_edge = senders.shape[0]
|
| 146 |
+
global_features = tree.tree_map(
|
| 147 |
+
lambda g: jnp.repeat(g, n_edge, axis=0, total_repeat_length=sum_n_edge),
|
| 148 |
+
graph.context.features)
|
| 149 |
+
new_features = edge_fn(
|
| 150 |
+
edge_set.features, sent_attributes, received_attributes,
|
| 151 |
+
global_features)
|
| 152 |
+
return edge_set._replace(features=new_features)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _node_update(graph, node_fn, node_set_key, aggregation_fn): # pylint: disable=invalid-name
|
| 156 |
+
"""Updates an edge set of a given key."""
|
| 157 |
+
node_set = graph.nodes[node_set_key]
|
| 158 |
+
sum_n_node = tree.tree_leaves(node_set.features)[0].shape[0]
|
| 159 |
+
|
| 160 |
+
sent_features = {}
|
| 161 |
+
for edge_set_key, edge_set in graph.edges.items():
|
| 162 |
+
sender_node_set_key = edge_set_key.node_sets[0]
|
| 163 |
+
if sender_node_set_key == node_set_key:
|
| 164 |
+
assert isinstance(edge_set.indices, typed_graph.EdgesIndices)
|
| 165 |
+
senders = edge_set.indices.senders
|
| 166 |
+
sent_features[edge_set_key.name] = tree.tree_map(
|
| 167 |
+
lambda e: aggregation_fn(e, senders, sum_n_node), edge_set.features) # pylint: disable=cell-var-from-loop
|
| 168 |
+
|
| 169 |
+
received_features = {}
|
| 170 |
+
for edge_set_key, edge_set in graph.edges.items():
|
| 171 |
+
receiver_node_set_key = edge_set_key.node_sets[1]
|
| 172 |
+
if receiver_node_set_key == node_set_key:
|
| 173 |
+
assert isinstance(edge_set.indices, typed_graph.EdgesIndices)
|
| 174 |
+
receivers = edge_set.indices.receivers
|
| 175 |
+
received_features[edge_set_key.name] = tree.tree_map(
|
| 176 |
+
lambda e: aggregation_fn(e, receivers, sum_n_node), edge_set.features) # pylint: disable=cell-var-from-loop
|
| 177 |
+
|
| 178 |
+
n_node = node_set.n_node
|
| 179 |
+
global_features = tree.tree_map(
|
| 180 |
+
lambda g: jnp.repeat(g, n_node, axis=0, total_repeat_length=sum_n_node),
|
| 181 |
+
graph.context.features)
|
| 182 |
+
new_features = node_fn(
|
| 183 |
+
node_set.features, sent_features, received_features, global_features)
|
| 184 |
+
return node_set._replace(features=new_features)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _global_update(graph, global_fn, edge_aggregation_fn, node_aggregation_fn): # pylint: disable=invalid-name
|
| 188 |
+
"""Updates an edge set of a given key."""
|
| 189 |
+
n_graph = graph.context.n_graph.shape[0]
|
| 190 |
+
graph_idx = jnp.arange(n_graph)
|
| 191 |
+
|
| 192 |
+
edge_features = {}
|
| 193 |
+
for edge_set_key, edge_set in graph.edges.items():
|
| 194 |
+
assert isinstance(edge_set.indices, typed_graph.EdgesIndices)
|
| 195 |
+
sum_n_edge = edge_set.indices.senders.shape[0]
|
| 196 |
+
edge_gr_idx = jnp.repeat(
|
| 197 |
+
graph_idx, edge_set.n_edge, axis=0, total_repeat_length=sum_n_edge)
|
| 198 |
+
edge_features[edge_set_key.name] = tree.tree_map(
|
| 199 |
+
lambda e: edge_aggregation_fn(e, edge_gr_idx, n_graph), # pylint: disable=cell-var-from-loop
|
| 200 |
+
edge_set.features)
|
| 201 |
+
|
| 202 |
+
node_features = {}
|
| 203 |
+
for node_set_key, node_set in graph.nodes.items():
|
| 204 |
+
sum_n_node = tree.tree_leaves(node_set.features)[0].shape[0]
|
| 205 |
+
node_gr_idx = jnp.repeat(
|
| 206 |
+
graph_idx, node_set.n_node, axis=0, total_repeat_length=sum_n_node)
|
| 207 |
+
node_features[node_set_key] = tree.tree_map(
|
| 208 |
+
lambda n: node_aggregation_fn(n, node_gr_idx, n_graph), # pylint: disable=cell-var-from-loop
|
| 209 |
+
node_set.features)
|
| 210 |
+
|
| 211 |
+
new_features = global_fn(node_features, edge_features, graph.context.features)
|
| 212 |
+
return graph.context._replace(features=new_features)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
InteractionUpdateNodeFn = Callable[
|
| 216 |
+
[jraph.NodeFeatures,
|
| 217 |
+
Mapping[str, SenderFeatures],
|
| 218 |
+
Mapping[str, ReceiverFeatures]],
|
| 219 |
+
jraph.NodeFeatures]
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
InteractionUpdateNodeFnNoSentEdges = Callable[
|
| 223 |
+
[jraph.NodeFeatures,
|
| 224 |
+
Mapping[str, ReceiverFeatures]],
|
| 225 |
+
jraph.NodeFeatures]
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def InteractionNetwork( # pylint: disable=invalid-name
|
| 229 |
+
update_edge_fn: Mapping[str, jraph.InteractionUpdateEdgeFn],
|
| 230 |
+
update_node_fn: Mapping[str, Union[InteractionUpdateNodeFn,
|
| 231 |
+
InteractionUpdateNodeFnNoSentEdges]],
|
| 232 |
+
aggregate_edges_for_nodes_fn: jraph.AggregateEdgesToNodesFn = jraph
|
| 233 |
+
.segment_sum,
|
| 234 |
+
include_sent_messages_in_node_update: bool = False):
|
| 235 |
+
"""Returns a method that applies a configured InteractionNetwork.
|
| 236 |
+
|
| 237 |
+
An interaction network computes interactions on the edges based on the
|
| 238 |
+
previous edges features, and on the features of the nodes sending into those
|
| 239 |
+
edges. It then updates the nodes based on the incoming updated edges.
|
| 240 |
+
See https://arxiv.org/abs/1612.00222 for more details.
|
| 241 |
+
|
| 242 |
+
This implementation extends the behavior to `TypedGraphs` adding an option
|
| 243 |
+
to include edge features for which a node is a sender in the arguments to
|
| 244 |
+
the node update function.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
update_edge_fn: mapping of functions used to update a subset of the edge
|
| 248 |
+
types, indexed by edge type name.
|
| 249 |
+
update_node_fn: mapping of functions used to update a subset of the node
|
| 250 |
+
types, indexed by node type name.
|
| 251 |
+
aggregate_edges_for_nodes_fn: function used to aggregate messages to each
|
| 252 |
+
node.
|
| 253 |
+
include_sent_messages_in_node_update: pass edge features for which a node is
|
| 254 |
+
a sender to the node update function.
|
| 255 |
+
"""
|
| 256 |
+
# An InteractionNetwork is a GraphNetwork without globals features,
|
| 257 |
+
# so we implement the InteractionNetwork as a configured GraphNetwork.
|
| 258 |
+
|
| 259 |
+
# An InteractionNetwork edge function does not have global feature inputs,
|
| 260 |
+
# so we filter the passed global argument in the GraphNetwork.
|
| 261 |
+
wrapped_update_edge_fn = tree.tree_map(
|
| 262 |
+
lambda fn: lambda e, s, r, g: fn(e, s, r), update_edge_fn)
|
| 263 |
+
|
| 264 |
+
# Similarly, we wrap the update_node_fn to ensure only the expected
|
| 265 |
+
# arguments are passed to the Interaction net.
|
| 266 |
+
if include_sent_messages_in_node_update:
|
| 267 |
+
wrapped_update_node_fn = tree.tree_map(
|
| 268 |
+
lambda fn: lambda n, s, r, g: fn(n, s, r), update_node_fn)
|
| 269 |
+
else:
|
| 270 |
+
wrapped_update_node_fn = tree.tree_map(
|
| 271 |
+
lambda fn: lambda n, s, r, g: fn(n, r), update_node_fn)
|
| 272 |
+
return GraphNetwork(
|
| 273 |
+
update_edge_fn=wrapped_update_edge_fn,
|
| 274 |
+
update_node_fn=wrapped_update_node_fn,
|
| 275 |
+
aggregate_edges_for_nodes_fn=aggregate_edges_for_nodes_fn)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def GraphMapFeatures( # pylint: disable=invalid-name
|
| 279 |
+
embed_edge_fn: Optional[Mapping[str, jraph.EmbedEdgeFn]] = None,
|
| 280 |
+
embed_node_fn: Optional[Mapping[str, jraph.EmbedNodeFn]] = None,
|
| 281 |
+
embed_global_fn: Optional[jraph.EmbedGlobalFn] = None):
|
| 282 |
+
"""Returns function which embeds the components of a graph independently.
|
| 283 |
+
|
| 284 |
+
Args:
|
| 285 |
+
embed_edge_fn: mapping of functions used to embed each edge type,
|
| 286 |
+
indexed by edge type name.
|
| 287 |
+
embed_node_fn: mapping of functions used to embed each node type,
|
| 288 |
+
indexed by node type name.
|
| 289 |
+
embed_global_fn: function used to embed the globals.
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
+
def _embed(graph: typed_graph.TypedGraph) -> typed_graph.TypedGraph:
|
| 293 |
+
|
| 294 |
+
updated_edges = dict(graph.edges)
|
| 295 |
+
if embed_edge_fn:
|
| 296 |
+
for edge_set_name, embed_fn in embed_edge_fn.items():
|
| 297 |
+
edge_set_key = graph.edge_key_by_name(edge_set_name)
|
| 298 |
+
edge_set = graph.edges[edge_set_key]
|
| 299 |
+
updated_edges[edge_set_key] = edge_set._replace(
|
| 300 |
+
features=embed_fn(edge_set.features))
|
| 301 |
+
|
| 302 |
+
updated_nodes = dict(graph.nodes)
|
| 303 |
+
if embed_node_fn:
|
| 304 |
+
for node_set_key, embed_fn in embed_node_fn.items():
|
| 305 |
+
node_set = graph.nodes[node_set_key]
|
| 306 |
+
updated_nodes[node_set_key] = node_set._replace(
|
| 307 |
+
features=embed_fn(node_set.features))
|
| 308 |
+
|
| 309 |
+
updated_context = graph.context
|
| 310 |
+
if embed_global_fn:
|
| 311 |
+
updated_context = updated_context._replace(
|
| 312 |
+
features=embed_global_fn(updated_context.features))
|
| 313 |
+
|
| 314 |
+
return graph._replace(edges=updated_edges, nodes=updated_nodes,
|
| 315 |
+
context=updated_context)
|
| 316 |
+
|
| 317 |
+
return _embed
|
model/graphcast/xarray_jax.py
ADDED
|
@@ -0,0 +1,1080 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Helpers to use xarray.{Variable,DataArray,Dataset} with JAX.
|
| 15 |
+
|
| 16 |
+
Allows them to be based on JAX arrays without converting to numpy arrays under
|
| 17 |
+
the hood, so you can start with a JAX array, do some computation with it in
|
| 18 |
+
xarray-land, get a JAX array out the other end and (for example) jax.jit
|
| 19 |
+
through the whole thing. You can even jax.jit a function which accepts and
|
| 20 |
+
returns xarray.Dataset, DataArray and Variable.
|
| 21 |
+
|
| 22 |
+
## Creating xarray datatypes from jax arrays, and vice-versa.
|
| 23 |
+
|
| 24 |
+
You can use the xarray_jax.{Variable,DataArray,Dataset} constructors, which have
|
| 25 |
+
the same API as the standard xarray constructors but will accept JAX arrays
|
| 26 |
+
without converting them to numpy.
|
| 27 |
+
|
| 28 |
+
It does this by wrapping the JAX array in a wrapper before passing it to
|
| 29 |
+
xarray; you can also do this manually by calling xarray_jax.wrap on your JAX
|
| 30 |
+
arrays before passing them to the standard xarray constructors.
|
| 31 |
+
|
| 32 |
+
To get non-wrapped JAX arrays out the other end, you can use e.g.:
|
| 33 |
+
|
| 34 |
+
xarray_jax.jax_vars(dataset)
|
| 35 |
+
xarray_jax.jax_data(dataset.some_var)
|
| 36 |
+
|
| 37 |
+
which will complain if the data isn't actually a JAX array. Use this if you need
|
| 38 |
+
to make sure the computation has gone via JAX, e.g. if it's the output of code
|
| 39 |
+
that you want to JIT or compute gradients through. If this is not the case and
|
| 40 |
+
you want to support passing plain numpy arrays through as well as potentially
|
| 41 |
+
JAX arrays, you can use:
|
| 42 |
+
|
| 43 |
+
xarray_jax.unwrap_vars(dataset)
|
| 44 |
+
xarray_jax.unwrap_data(dataset.some_var)
|
| 45 |
+
|
| 46 |
+
which will unwrap the data if it is a wrapped JAX array, but otherwise pass
|
| 47 |
+
it through to you without complaint.
|
| 48 |
+
|
| 49 |
+
The wrapped JAX arrays aim to support all the core operations from the numpy
|
| 50 |
+
array API that xarray expects, however there may still be some gaps; if you run
|
| 51 |
+
into any problems around this, you may need to add a few more proxy methods onto
|
| 52 |
+
the wrapper class below.
|
| 53 |
+
|
| 54 |
+
In future once JAX and xarray support the new Python array API standard
|
| 55 |
+
(https://data-apis.org/array-api/latest/index.html), we hope to avoid the need
|
| 56 |
+
for wrapping the JAX arrays like this.
|
| 57 |
+
|
| 58 |
+
## jax.jit and pmap of functions taking and returning xarray datatypes
|
| 59 |
+
|
| 60 |
+
We register xarray datatypes with jax.tree_util, which allows them to be treated
|
| 61 |
+
as generic containers of jax arrays by various parts of jax including jax.jit.
|
| 62 |
+
|
| 63 |
+
This allows for, e.g.:
|
| 64 |
+
|
| 65 |
+
@jax.jit
|
| 66 |
+
def foo(input: xarray.Dataset) -> xarray.Dataset:
|
| 67 |
+
...
|
| 68 |
+
|
| 69 |
+
It will not work out-of-the-box with shape-modifying transformations like
|
| 70 |
+
jax.pmap, or e.g. a jax.tree_util.tree_map with some transform that alters array
|
| 71 |
+
shapes or dimension order. That's because we won't know what dimension names
|
| 72 |
+
and/or coordinates to use when unflattening, if the results have a different
|
| 73 |
+
shape to the data that was originally flattened.
|
| 74 |
+
|
| 75 |
+
You can work around this using xarray_jax.dims_change_on_unflatten, however,
|
| 76 |
+
and in the case of jax.pmap we provide a wrapper xarray_jax.pmap which allows
|
| 77 |
+
it to be used with functions taking and returning xarrays.
|
| 78 |
+
|
| 79 |
+
## Treatment of coordinates
|
| 80 |
+
|
| 81 |
+
We don't support passing jax arrays as coordinates when constructing a
|
| 82 |
+
DataArray/Dataset. This is because xarray's advanced indexing and slicing is
|
| 83 |
+
unlikely to work with jax arrays (at least when a Tracer is used during
|
| 84 |
+
jax.jit), and also because some important datatypes used for coordinates, like
|
| 85 |
+
timedelta64 and datetime64, are not supported by jax.
|
| 86 |
+
|
| 87 |
+
For the purposes of tree_util and jax.jit, coordinates are not treated as leaves
|
| 88 |
+
of the tree (array data 'contained' by a Dataset/DataArray), they are just a
|
| 89 |
+
static part of the structure. That means that if a jit'ed function is called
|
| 90 |
+
twice with Dataset inputs that use different coordinates, it will compile a
|
| 91 |
+
separate version of the function for each. The coordinates are treated like
|
| 92 |
+
static_argnums by jax.jit.
|
| 93 |
+
|
| 94 |
+
If you want to use dynamic data for coordinates, we recommend making it a
|
| 95 |
+
data_var instead of a coord. You won't be able to do indexing and slicing using
|
| 96 |
+
the coordinate, but that wasn't going to work with a jax array anyway.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
import collections
|
| 100 |
+
import contextlib
|
| 101 |
+
import contextvars
|
| 102 |
+
from typing import Any, Callable, Iterator, Mapping, Optional, Union, Tuple, TypeVar, cast
|
| 103 |
+
from typing import Hashable # pylint: disable=deprecated-class
|
| 104 |
+
|
| 105 |
+
import jax
|
| 106 |
+
import jax.numpy as jnp
|
| 107 |
+
import numpy as np
|
| 108 |
+
import tree
|
| 109 |
+
import xarray
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# Types which we wrap with JaxArrayWrapper to allow creating xarray datatypes
|
| 113 |
+
# from them.
|
| 114 |
+
# Note this includes some non-Array types which jax sometimes needs to use as
|
| 115 |
+
# leaves of pytrees, in order to ensure we can still use xarray datatypes as
|
| 116 |
+
# internal pytree nodes in these cases.
|
| 117 |
+
_WRAPPED_TYPES = (
|
| 118 |
+
jax.Array, jax.ShapeDtypeStruct, jax.stages.ArgInfo)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def Variable(dims, data, **kwargs) -> xarray.Variable: # pylint:disable=invalid-name
|
| 122 |
+
"""Like xarray.Variable, but can wrap JAX arrays."""
|
| 123 |
+
return xarray.Variable(dims, wrap(data), **kwargs)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
_JAX_COORD_ATTR_NAME = '_jax_coord'
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def DataArray( # pylint:disable=invalid-name
|
| 130 |
+
data,
|
| 131 |
+
coords=None,
|
| 132 |
+
dims=None,
|
| 133 |
+
name=None,
|
| 134 |
+
attrs=None,
|
| 135 |
+
jax_coords=None,
|
| 136 |
+
) -> xarray.DataArray:
|
| 137 |
+
"""Like xarray.DataArray, but supports using JAX arrays.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
data: As for xarray.DataArray, except jax arrays are also supported.
|
| 141 |
+
coords: Coordinates for the array, see xarray.DataArray. These coordinates
|
| 142 |
+
must be based on plain numpy arrays or something convertible to plain
|
| 143 |
+
numpy arrays. Their values will form a static part of the data structure
|
| 144 |
+
from the point of view of jax.tree_util. In particular this means these
|
| 145 |
+
coordinates will be passed as plain numpy arrays even inside a JIT'd
|
| 146 |
+
function, and the JIT'd function will be recompiled under the hood if the
|
| 147 |
+
coordinates of DataArrays passed into it change.
|
| 148 |
+
If this is not convenient for you, see also jax_coords below.
|
| 149 |
+
dims: See xarray.DataArray.
|
| 150 |
+
name: See xarray.DataArray.
|
| 151 |
+
attrs: See xarray.DataArray.
|
| 152 |
+
jax_coords: Additional coordinates, which *can* use JAX arrays. These
|
| 153 |
+
coordinates will be treated as JAX data from the point of view of
|
| 154 |
+
jax.tree_util, that means when JIT'ing they will be passed as tracers and
|
| 155 |
+
computation involving them will be JIT'd.
|
| 156 |
+
Unfortunately a side-effect of this is that they can't be used as index
|
| 157 |
+
coordinates (because xarray's indexing logic is not JIT-able). If you
|
| 158 |
+
specify a coordinate with the same name as a dimension here, it will not
|
| 159 |
+
be set as an index coordinate; this behaviour is different to the default
|
| 160 |
+
for `coords`, and it means that things like `.sel` based on the jax
|
| 161 |
+
coordinate will not work.
|
| 162 |
+
Note we require `jax_coords` to be explicitly specified via a different
|
| 163 |
+
constructor argument to `coords`, rather than just looking for jax arrays
|
| 164 |
+
within the `coords` and treating them differently. This is because it
|
| 165 |
+
affects the way jax.tree_util treats them, which is somewhat orthogonal to
|
| 166 |
+
whether the value is passed in as numpy or not, and generally needs to be
|
| 167 |
+
handled consistently so is something we encourage explicit control over.
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
An instance of xarray.DataArray. Where JAX arrays are used as data or
|
| 171 |
+
coords, they will be wrapped with JaxArrayWrapper and can be unwrapped via
|
| 172 |
+
`unwrap` and `unwrap_data`.
|
| 173 |
+
"""
|
| 174 |
+
result = xarray.DataArray(
|
| 175 |
+
wrap(data), dims=dims, name=name, attrs=attrs or {})
|
| 176 |
+
return assign_coords(result, coords=coords, jax_coords=jax_coords)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def Dataset( # pylint:disable=invalid-name
|
| 180 |
+
data_vars=None,
|
| 181 |
+
coords=None,
|
| 182 |
+
attrs=None,
|
| 183 |
+
jax_coords=None,
|
| 184 |
+
) -> xarray.Dataset:
|
| 185 |
+
"""Like xarray.Dataset, but can wrap JAX arrays.
|
| 186 |
+
|
| 187 |
+
Args:
|
| 188 |
+
data_vars: As for xarray.Dataset, except jax arrays are also supported.
|
| 189 |
+
coords: Coordinates for the dataset, see xarray.Dataset. These coordinates
|
| 190 |
+
must be based on plain numpy arrays or something convertible to plain
|
| 191 |
+
numpy arrays. Their values will form a static part of the data structure
|
| 192 |
+
from the point of view of jax.tree_util. In particular this means these
|
| 193 |
+
coordinates will be passed as plain numpy arrays even inside a JIT'd
|
| 194 |
+
function, and the JIT'd function will be recompiled under the hood if the
|
| 195 |
+
coordinates of DataArrays passed into it change.
|
| 196 |
+
If this is not convenient for you, see also jax_coords below.
|
| 197 |
+
attrs: See xarray.Dataset.
|
| 198 |
+
jax_coords: Additional coordinates, which *can* use JAX arrays. These
|
| 199 |
+
coordinates will be treated as JAX data from the point of view of
|
| 200 |
+
jax.tree_util, that means when JIT'ing they will be passed as tracers and
|
| 201 |
+
computation involving them will be JIT'd.
|
| 202 |
+
Unfortunately a side-effect of this is that they can't be used as index
|
| 203 |
+
coordinates (because xarray's indexing logic is not JIT-able). If you
|
| 204 |
+
specify a coordinate with the same name as a dimension here, it will not
|
| 205 |
+
be set as an index coordinate; this behaviour is different to the default
|
| 206 |
+
for `coords`, and it means that things like `.sel` based on the jax
|
| 207 |
+
coordinate will not work.
|
| 208 |
+
Note we require `jax_coords` to be explicitly specified via a different
|
| 209 |
+
constructor argument to `coords`, rather than just looking for jax arrays
|
| 210 |
+
within the `coords` and treating them differently. This is because it
|
| 211 |
+
affects the way jax.tree_util treats them, which is somewhat orthogonal to
|
| 212 |
+
whether the value is passed in as numpy or not, and generally needs to be
|
| 213 |
+
handled consistently so is something we encourage explicit control over.
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
An instance of xarray.Dataset. Where JAX arrays are used as data, they
|
| 217 |
+
will be wrapped with JaxArrayWrapper.
|
| 218 |
+
"""
|
| 219 |
+
wrapped_data_vars = {}
|
| 220 |
+
for name, var_like in (data_vars or {}).items():
|
| 221 |
+
# xarray.Dataset accepts a few different formats for data_vars:
|
| 222 |
+
if isinstance(var_like, _WRAPPED_TYPES):
|
| 223 |
+
wrapped_data_vars[name] = wrap(var_like)
|
| 224 |
+
elif isinstance(var_like, tuple):
|
| 225 |
+
# Layout is (dims, data, ...). We wrap data.
|
| 226 |
+
wrapped_data_vars[name] = (var_like[0], wrap(var_like[1])) + var_like[2:]
|
| 227 |
+
else:
|
| 228 |
+
# Could be a plain numpy array or scalar (we don't wrap), or an
|
| 229 |
+
# xarray.Variable, DataArray etc, which we must assume is already wrapped
|
| 230 |
+
# if necessary (e.g. if creating using xarray_jax.{Variable,DataArray}).
|
| 231 |
+
wrapped_data_vars[name] = var_like
|
| 232 |
+
|
| 233 |
+
result = xarray.Dataset(
|
| 234 |
+
data_vars=wrapped_data_vars,
|
| 235 |
+
attrs=attrs)
|
| 236 |
+
|
| 237 |
+
return assign_coords(result, coords=coords, jax_coords=jax_coords)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
DatasetOrDataArray = TypeVar(
|
| 241 |
+
'DatasetOrDataArray', xarray.Dataset, xarray.DataArray)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def assign_coords(
|
| 245 |
+
x: DatasetOrDataArray,
|
| 246 |
+
*,
|
| 247 |
+
coords: Optional[Mapping[Hashable, Any]] = None,
|
| 248 |
+
jax_coords: Optional[Mapping[Hashable, Any]] = None,
|
| 249 |
+
) -> DatasetOrDataArray:
|
| 250 |
+
"""Replacement for assign_coords which works in presence of jax_coords.
|
| 251 |
+
|
| 252 |
+
`jax_coords` allow certain specified coordinates to have their data passed as
|
| 253 |
+
JAX arrays (including through jax.jit boundaries). The compromise in return is
|
| 254 |
+
that they are not created as index coordinates and cannot be used for .sel
|
| 255 |
+
and other coordinate-based indexing operations. See docs for `jax_coords` on
|
| 256 |
+
xarray_jax.Dataset and xarray_jax.DataArray for more information.
|
| 257 |
+
|
| 258 |
+
This function can be used to set jax_coords on an existing DataArray or
|
| 259 |
+
Dataset, and also to set a mix of jax and non-jax coordinates. It implements
|
| 260 |
+
some workarounds to prevent xarray trying and failing to create IndexVariables
|
| 261 |
+
from jax arrays under the hood.
|
| 262 |
+
|
| 263 |
+
If you have any jax_coords with the same name as a dimension, you'll need to
|
| 264 |
+
use this function instead of data_array.assign_coords or dataset.assign_coords
|
| 265 |
+
in general, to avoid an xarray bug where it tries (and in our case fails) to
|
| 266 |
+
create indexes for existing jax coords. See
|
| 267 |
+
https://github.com/pydata/xarray/issues/7885.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
x: An xarray Dataset or DataArray.
|
| 271 |
+
coords: Dict of (non-JAX) coords, or None if not assigning any.
|
| 272 |
+
jax_coords: Dict of JAX coords, or None if not assigning any. See docs for
|
| 273 |
+
xarray_jax.Dataset / DataArray for more information on jax_coords.
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
The Dataset or DataArray with coordinates assigned, similarly to
|
| 277 |
+
Dataset.assign_coords / DataArray.assign_coords.
|
| 278 |
+
"""
|
| 279 |
+
coords = {} if coords is None else dict(coords) # Copy before mutating.
|
| 280 |
+
jax_coords = {} if jax_coords is None else dict(jax_coords)
|
| 281 |
+
|
| 282 |
+
# Any existing JAX coords must be dropped and re-added via the workaround
|
| 283 |
+
# below, since otherwise .assign_coords will trigger an xarray bug where
|
| 284 |
+
# it tries to recreate the indexes again for the existing coordinates.
|
| 285 |
+
# Can remove if/when https://github.com/pydata/xarray/issues/7885 fixed.
|
| 286 |
+
existing_jax_coords = get_jax_coords(x)
|
| 287 |
+
jax_coords = existing_jax_coords | jax_coords
|
| 288 |
+
x = x.drop_vars(existing_jax_coords.keys())
|
| 289 |
+
|
| 290 |
+
# We need to ensure that xarray doesn't try to create an index for
|
| 291 |
+
# coordinates with the same name as a dimension, since this will fail if
|
| 292 |
+
# given a wrapped JAX tracer.
|
| 293 |
+
# It appears the only way to avoid this is to name them differently to any
|
| 294 |
+
# dimension name, then rename them back afterwards.
|
| 295 |
+
renamed_jax_coords = {}
|
| 296 |
+
for name, coord in jax_coords.items():
|
| 297 |
+
if isinstance(coord, xarray.DataArray):
|
| 298 |
+
coord = coord.variable
|
| 299 |
+
|
| 300 |
+
if isinstance(coord, list):
|
| 301 |
+
coord = np.array(coord)
|
| 302 |
+
|
| 303 |
+
if isinstance(coord, xarray.Variable):
|
| 304 |
+
coord = coord.copy(deep=False) # Copy before mutating attrs.
|
| 305 |
+
elif isinstance(coord, tuple):
|
| 306 |
+
# A tuple represents a pair of (dims, data).
|
| 307 |
+
dims, data = coord
|
| 308 |
+
coord = Variable(dims, data)
|
| 309 |
+
elif jnp.isscalar(coord):
|
| 310 |
+
# A scalar coord maps to a scalar Variable:
|
| 311 |
+
coord = Variable(dims=(), data=coord)
|
| 312 |
+
elif isinstance(coord, jax.typing.ArrayLike) and jnp.ndim(coord) == 1:
|
| 313 |
+
# A 1D array maps to a 1D Variable whose dimension is the same as the
|
| 314 |
+
# coordinate name:
|
| 315 |
+
coord = Variable((name,), coord)
|
| 316 |
+
else:
|
| 317 |
+
raise ValueError(f'Unsupported value for coordinate {name}')
|
| 318 |
+
|
| 319 |
+
# We set an attr on each jax_coord identifying it as such. These attrs on
|
| 320 |
+
# the coord Variable gets reflected on the coord DataArray exposed too, and
|
| 321 |
+
# when set on coordinates they generally get preserved under the default
|
| 322 |
+
# keep_attrs setting.
|
| 323 |
+
# These attrs are used by jax.tree_util registered flatten/unflatten to
|
| 324 |
+
# determine which coords need to be treated as leaves of the flattened
|
| 325 |
+
# structure vs static data.
|
| 326 |
+
coord.attrs[_JAX_COORD_ATTR_NAME] = True
|
| 327 |
+
renamed_jax_coords[f'__NONINDEX_{name}'] = coord
|
| 328 |
+
|
| 329 |
+
x = x.assign_coords(coords=coords | renamed_jax_coords)
|
| 330 |
+
|
| 331 |
+
rename_back_mapping = {f'__NONINDEX_{name}': name for name in jax_coords}
|
| 332 |
+
if isinstance(x, xarray.Dataset):
|
| 333 |
+
# Using 'rename' doesn't work if renaming to the same name as a dimension.
|
| 334 |
+
return x.rename_vars(rename_back_mapping)
|
| 335 |
+
else: # DataArray
|
| 336 |
+
return x.rename(rename_back_mapping)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def get_jax_coords(x: DatasetOrDataArray) -> Mapping[Hashable, Any]:
|
| 340 |
+
return {
|
| 341 |
+
name: coord_var
|
| 342 |
+
for name, coord_var in x.coords.variables.items()
|
| 343 |
+
if coord_var.attrs.get(_JAX_COORD_ATTR_NAME, False)}
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def assign_jax_coords(
|
| 347 |
+
x: DatasetOrDataArray,
|
| 348 |
+
jax_coords: Optional[Mapping[Hashable, Any]] = None,
|
| 349 |
+
**jax_coords_kwargs
|
| 350 |
+
) -> DatasetOrDataArray:
|
| 351 |
+
"""Assigns only jax_coords, with same API as xarray's assign_coords."""
|
| 352 |
+
return assign_coords(x, jax_coords=jax_coords or jax_coords_kwargs)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def wrap(value):
|
| 356 |
+
"""Wraps JAX arrays for use in xarray, passing through other values."""
|
| 357 |
+
if isinstance(value, _WRAPPED_TYPES):
|
| 358 |
+
return JaxArrayWrapper(value)
|
| 359 |
+
else:
|
| 360 |
+
return value
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def unwrap(value, require_jax=False):
|
| 364 |
+
"""Unwraps wrapped JAX arrays used in xarray, passing through other values."""
|
| 365 |
+
if isinstance(value, JaxArrayWrapper):
|
| 366 |
+
return value.jax_array
|
| 367 |
+
elif isinstance(value, jax.Array):
|
| 368 |
+
return value
|
| 369 |
+
elif require_jax:
|
| 370 |
+
raise TypeError(f'Expected JAX array, found {type(value)}.')
|
| 371 |
+
else:
|
| 372 |
+
return value
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _wrapped(func):
|
| 376 |
+
"""Surrounds a function with JAX array unwrapping/wrapping."""
|
| 377 |
+
def wrapped_func(*args, **kwargs):
|
| 378 |
+
args, kwargs = tree.map_structure(unwrap, (args, kwargs))
|
| 379 |
+
result = func(*args, **kwargs)
|
| 380 |
+
return tree.map_structure(wrap, result)
|
| 381 |
+
return wrapped_func
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def unwrap_data(
|
| 385 |
+
value: Union[xarray.Variable, xarray.DataArray],
|
| 386 |
+
require_jax: bool = False
|
| 387 |
+
) -> Union[jax.Array, np.ndarray]:
|
| 388 |
+
"""The unwrapped (see unwrap) data of a an xarray.Variable or DataArray."""
|
| 389 |
+
return unwrap(value.data, require_jax=require_jax)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def unwrap_vars(
|
| 393 |
+
dataset: Mapping[Hashable, xarray.DataArray],
|
| 394 |
+
require_jax: bool = False
|
| 395 |
+
) -> Mapping[str, Union[jax.Array, np.ndarray]]:
|
| 396 |
+
"""The unwrapped data (see unwrap) of the variables in a dataset."""
|
| 397 |
+
# xarray types variable names as Hashable, but in practice they're invariably
|
| 398 |
+
# strings and we convert to str to allow for a more useful return type.
|
| 399 |
+
return {str(name): unwrap_data(var, require_jax=require_jax)
|
| 400 |
+
for name, var in dataset.items()}
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def unwrap_coords(
|
| 404 |
+
dataset: Union[xarray.Dataset, xarray.DataArray],
|
| 405 |
+
require_jax: bool = False
|
| 406 |
+
) -> Mapping[str, Union[jax.Array, np.ndarray]]:
|
| 407 |
+
"""The unwrapped data (see unwrap) of the coords in a Dataset or DataArray."""
|
| 408 |
+
return {str(name): unwrap_data(var, require_jax=require_jax)
|
| 409 |
+
for name, var in dataset.coords.items()}
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def jax_data(value: Union[xarray.Variable, xarray.DataArray]) -> jax.Array:
|
| 413 |
+
"""Like unwrap_data, but will complain if not a jax array."""
|
| 414 |
+
# Implementing this separately so we can give a more specific return type
|
| 415 |
+
# for it.
|
| 416 |
+
return cast(jax.Array, unwrap_data(value, require_jax=True))
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def jax_vars(
|
| 420 |
+
dataset: Mapping[Hashable, xarray.DataArray]) -> Mapping[str, jax.Array]:
|
| 421 |
+
"""Like unwrap_vars, but will complain if vars are not all jax arrays."""
|
| 422 |
+
return cast(Mapping[str, jax.Array], unwrap_vars(dataset, require_jax=True))
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
class JaxArrayWrapper(np.lib.mixins.NDArrayOperatorsMixin):
|
| 426 |
+
"""Wraps a JAX array into a duck-typed array suitable for use with xarray.
|
| 427 |
+
|
| 428 |
+
This uses an older duck-typed array protocol based on __array_ufunc__ and
|
| 429 |
+
__array_function__ which works with numpy and xarray. (In newer versions
|
| 430 |
+
of xarray it implements xarray.namedarray._typing._array_function.)
|
| 431 |
+
|
| 432 |
+
This is in the process of being superseded by the Python array API standard
|
| 433 |
+
(https://data-apis.org/array-api/latest/index.html), but JAX hasn't
|
| 434 |
+
implemented it yet. Once they have, we should be able to get rid of
|
| 435 |
+
this wrapper and use JAX arrays directly with xarray.
|
| 436 |
+
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
def __init__(self, jax_array):
|
| 440 |
+
self.jax_array = jax_array
|
| 441 |
+
|
| 442 |
+
def __array_ufunc__(self, ufunc, method, *args, **kwargs):
|
| 443 |
+
for x in args:
|
| 444 |
+
if not isinstance(x, (jax.typing.ArrayLike, type(self))):
|
| 445 |
+
return NotImplemented
|
| 446 |
+
if method != '__call__':
|
| 447 |
+
return NotImplemented
|
| 448 |
+
try:
|
| 449 |
+
# Get the corresponding jax.numpy function to the NumPy ufunc:
|
| 450 |
+
func = getattr(jnp, ufunc.__name__)
|
| 451 |
+
except AttributeError:
|
| 452 |
+
return NotImplemented
|
| 453 |
+
# There may be an 'out' kwarg requesting an in-place operation, e.g. when
|
| 454 |
+
# this is called via __iadd__ (+=), __imul__ (*=) etc. JAX doesn't support
|
| 455 |
+
# in-place operations so we just remove this argument and have the ufunc
|
| 456 |
+
# return a fresh JAX array instead.
|
| 457 |
+
kwargs.pop('out', None)
|
| 458 |
+
return _wrapped(func)(*args, **kwargs)
|
| 459 |
+
|
| 460 |
+
def __array_function__(self, func, types, args, kwargs):
|
| 461 |
+
try:
|
| 462 |
+
# Get the corresponding jax.np function to the NumPy function:
|
| 463 |
+
func = getattr(jnp, func.__name__)
|
| 464 |
+
except AttributeError:
|
| 465 |
+
return NotImplemented
|
| 466 |
+
return _wrapped(func)(*args, **kwargs)
|
| 467 |
+
|
| 468 |
+
def __repr__(self):
|
| 469 |
+
return f'xarray_jax.JaxArrayWrapper({repr(self.jax_array)})'
|
| 470 |
+
|
| 471 |
+
# NDArrayOperatorsMixin already proxies most __dunder__ operator methods.
|
| 472 |
+
# We need to proxy through a few more methods in a similar way:
|
| 473 |
+
|
| 474 |
+
# Essential array properties:
|
| 475 |
+
|
| 476 |
+
@property
|
| 477 |
+
def shape(self):
|
| 478 |
+
return self.jax_array.shape
|
| 479 |
+
|
| 480 |
+
@property
|
| 481 |
+
def dtype(self):
|
| 482 |
+
return self.jax_array.dtype
|
| 483 |
+
|
| 484 |
+
@property
|
| 485 |
+
def ndim(self):
|
| 486 |
+
return self.jax_array.ndim
|
| 487 |
+
|
| 488 |
+
@property
|
| 489 |
+
def size(self):
|
| 490 |
+
return self.jax_array.size
|
| 491 |
+
|
| 492 |
+
@property
|
| 493 |
+
def real(self):
|
| 494 |
+
return self.jax_array.real
|
| 495 |
+
|
| 496 |
+
@property
|
| 497 |
+
def imag(self):
|
| 498 |
+
return self.jax_array.imag
|
| 499 |
+
|
| 500 |
+
# Array methods not covered by NDArrayOperatorsMixin:
|
| 501 |
+
|
| 502 |
+
# Allows conversion to numpy array using np.asarray etc. Warning: doing this
|
| 503 |
+
# will fail in a jax.jit-ed function.
|
| 504 |
+
def __array__(self, dtype=None, context=None):
|
| 505 |
+
return np.asarray(self.jax_array, dtype=dtype)
|
| 506 |
+
|
| 507 |
+
__getitem__ = _wrapped(lambda array, *args: array.__getitem__(*args))
|
| 508 |
+
# We drop the kwargs on this as they are not supported by JAX, but xarray
|
| 509 |
+
# uses at least one of them (the copy arg).
|
| 510 |
+
astype = _wrapped(lambda array, *args, **kwargs: array.astype(*args))
|
| 511 |
+
|
| 512 |
+
# There are many more methods which are more canonically available via (j)np
|
| 513 |
+
# functions, e.g. .sum() available via jnp.sum, and also mean, max, min,
|
| 514 |
+
# argmax, argmin etc. We don't attempt to proxy through all of these as
|
| 515 |
+
# methods, since this doesn't appear to be expected from a duck-typed array
|
| 516 |
+
# implementation. But there are a few which xarray calls as methods, so we
|
| 517 |
+
# proxy those:
|
| 518 |
+
transpose = _wrapped(jnp.transpose)
|
| 519 |
+
reshape = _wrapped(jnp.reshape)
|
| 520 |
+
all = _wrapped(jnp.all)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def apply_ufunc(func, *args, require_jax=False, **apply_ufunc_kwargs):
|
| 524 |
+
"""Like xarray.apply_ufunc but for jax-specific ufuncs.
|
| 525 |
+
|
| 526 |
+
Many numpy ufuncs will work fine out of the box with xarray_jax and
|
| 527 |
+
JaxArrayWrapper, since JaxArrayWrapper quacks (mostly) like a numpy array and
|
| 528 |
+
will convert many numpy operations to jax ops under the hood. For these
|
| 529 |
+
situations, xarray.apply_ufunc should work fine.
|
| 530 |
+
|
| 531 |
+
But sometimes you need a jax-specific ufunc which needs to be given a
|
| 532 |
+
jax array as input or return a jax array as output. In that case you should
|
| 533 |
+
use this helper as it will remove any JaxArrayWrapper before calling the func,
|
| 534 |
+
and wrap the result afterwards before handing it back to xarray.
|
| 535 |
+
|
| 536 |
+
Args:
|
| 537 |
+
func: A function that works with jax arrays (e.g. using functions from
|
| 538 |
+
jax.numpy) but otherwise meets the spec for the func argument to
|
| 539 |
+
xarray.apply_ufunc.
|
| 540 |
+
*args: xarray arguments to be mapped to arguments for func
|
| 541 |
+
(see xarray.apply_ufunc).
|
| 542 |
+
require_jax: Whether to require that inputs are based on jax arrays or allow
|
| 543 |
+
those based on plain numpy arrays too.
|
| 544 |
+
**apply_ufunc_kwargs: See xarray.apply_ufunc.
|
| 545 |
+
|
| 546 |
+
Returns:
|
| 547 |
+
Corresponding xarray results (see xarray.apply_ufunc).
|
| 548 |
+
"""
|
| 549 |
+
def wrapped_func(*maybe_wrapped_args):
|
| 550 |
+
unwrapped_args = [unwrap(a, require_jax) for a in maybe_wrapped_args]
|
| 551 |
+
result = func(*unwrapped_args)
|
| 552 |
+
# Result can be an array or a tuple of arrays, this handles both:
|
| 553 |
+
return jax.tree_util.tree_map(wrap, result)
|
| 554 |
+
return xarray.apply_ufunc(wrapped_func, *args, **apply_ufunc_kwargs)
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
def pmap(
|
| 558 |
+
fn: Callable[..., Any],
|
| 559 |
+
dim: str,
|
| 560 |
+
axis_name: Optional[str] = None,
|
| 561 |
+
devices=None,
|
| 562 |
+
backend=None,
|
| 563 |
+
) -> Callable[..., Any]:
|
| 564 |
+
"""Wraps a subset of jax.pmap functionality to handle xarray input/output.
|
| 565 |
+
|
| 566 |
+
Constraints:
|
| 567 |
+
* Any Dataset or DataArray passed to the function must have `dim` as the
|
| 568 |
+
first dimension. This will be checked. You can ensure this if necessary
|
| 569 |
+
by calling `.transpose(dim, ...)` beforehand.
|
| 570 |
+
* All args and return values will be mapped over the first dimension,
|
| 571 |
+
it will use in_axes=0, out_axes=0.
|
| 572 |
+
* No support for static_broadcasted_argnums, donate_argnums etc.
|
| 573 |
+
|
| 574 |
+
Args:
|
| 575 |
+
fn: Function to be pmap'd which takes and returns trees which may contain
|
| 576 |
+
xarray Dataset/DataArray. Any Dataset/DataArrays passed as input must use
|
| 577 |
+
`dim` as the first dimension on all arrays.
|
| 578 |
+
dim: The xarray dimension name corresponding to the first dimension that is
|
| 579 |
+
pmapped over (pmap is called with in_axes=0, out_axes=0).
|
| 580 |
+
axis_name: Used by jax to identify the mapped axis so that parallel
|
| 581 |
+
collectives can be applied. Defaults to same as `dim`.
|
| 582 |
+
devices:
|
| 583 |
+
backend:
|
| 584 |
+
See jax.pmap.
|
| 585 |
+
|
| 586 |
+
Returns:
|
| 587 |
+
A pmap'd version of `fn`, which takes and returns Dataset/DataArray with an
|
| 588 |
+
extra leading dimension `dim` relative to what the original `fn` sees.
|
| 589 |
+
"""
|
| 590 |
+
return _vmap_or_pmap(
|
| 591 |
+
fn, dim, axis_name, devices, backend, is_vmap=False,
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def vmap(
|
| 596 |
+
fn: Callable[..., Any],
|
| 597 |
+
dim: str,
|
| 598 |
+
axis_name: Optional[str] = None,
|
| 599 |
+
) -> Callable[..., Any]:
|
| 600 |
+
"""Similar to pmap, but for vmap."""
|
| 601 |
+
return _vmap_or_pmap(
|
| 602 |
+
fn, dim, axis_name, None, None, is_vmap=True,
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
def _vmap_or_pmap(
|
| 607 |
+
fn: Callable[..., Any],
|
| 608 |
+
dim: str,
|
| 609 |
+
axis_name: Optional[str] = None,
|
| 610 |
+
devices=None,
|
| 611 |
+
backend=None,
|
| 612 |
+
is_vmap: bool = False,
|
| 613 |
+
) -> Callable[..., Any]:
|
| 614 |
+
"""See pmap documentations."""
|
| 615 |
+
|
| 616 |
+
input_treedef = None
|
| 617 |
+
output_treedef = None
|
| 618 |
+
|
| 619 |
+
def fn_passed_to_pmap(*flat_args):
|
| 620 |
+
assert input_treedef is not None
|
| 621 |
+
# Inside the pmap the original first dimension will no longer be present:
|
| 622 |
+
def check_and_remove_leading_dim(dims):
|
| 623 |
+
try:
|
| 624 |
+
index = dims.index(dim)
|
| 625 |
+
except ValueError:
|
| 626 |
+
index = None
|
| 627 |
+
if index != 0:
|
| 628 |
+
raise ValueError(f'Expected dim {dim} at index 0, found at {index}.')
|
| 629 |
+
return dims[1:]
|
| 630 |
+
with dims_change_on_unflatten(check_and_remove_leading_dim):
|
| 631 |
+
args = jax.tree_util.tree_unflatten(input_treedef, flat_args)
|
| 632 |
+
result = fn(*args)
|
| 633 |
+
nonlocal output_treedef
|
| 634 |
+
flat_result, output_treedef = jax.tree_util.tree_flatten(result)
|
| 635 |
+
return flat_result
|
| 636 |
+
|
| 637 |
+
if is_vmap:
|
| 638 |
+
assert devices is None
|
| 639 |
+
assert backend is None
|
| 640 |
+
pmapped_fn = jax.vmap(
|
| 641 |
+
fn_passed_to_pmap,
|
| 642 |
+
axis_name=axis_name or dim,
|
| 643 |
+
in_axes=0,
|
| 644 |
+
out_axes=0)
|
| 645 |
+
else:
|
| 646 |
+
pmapped_fn = jax.pmap(
|
| 647 |
+
fn_passed_to_pmap,
|
| 648 |
+
axis_name=axis_name or dim,
|
| 649 |
+
in_axes=0,
|
| 650 |
+
out_axes=0,
|
| 651 |
+
devices=devices,
|
| 652 |
+
backend=backend)
|
| 653 |
+
|
| 654 |
+
def result_fn(*args):
|
| 655 |
+
nonlocal input_treedef
|
| 656 |
+
flat_args, input_treedef = jax.tree_util.tree_flatten(args)
|
| 657 |
+
flat_result = pmapped_fn(*flat_args)
|
| 658 |
+
assert output_treedef is not None
|
| 659 |
+
# After the pmap an extra leading axis will be present, we need to add an
|
| 660 |
+
# xarray dimension for this when unflattening the result:
|
| 661 |
+
with dims_change_on_unflatten(lambda dims: (dim,) + dims):
|
| 662 |
+
return jax.tree_util.tree_unflatten(output_treedef, flat_result)
|
| 663 |
+
|
| 664 |
+
return result_fn
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
_PyTree = TypeVar('_PyTree')
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
def tree_map_variables(
|
| 671 |
+
func: Callable[[xarray.Variable], xarray.Variable],
|
| 672 |
+
tree_data: _PyTree) -> _PyTree:
|
| 673 |
+
"""Like jax.tree.map but operates with Variables as leaves.
|
| 674 |
+
|
| 675 |
+
This will work with any jax.tree_util-registered PyTree containing xarray
|
| 676 |
+
datatypes. All jax data in xarray datatypes is exposed via xarray.Variable
|
| 677 |
+
nodes by our registered flatten/unflatten functions and hence here too. Note
|
| 678 |
+
static coordinate data will not be mapped over however.
|
| 679 |
+
|
| 680 |
+
This allows you to see the associated dimensions for each leaf, and to change
|
| 681 |
+
them. If you change them, it's your responsibility to ensure that when
|
| 682 |
+
unflattened back into DataArray/Dataset/DataTree the result still makes sense.
|
| 683 |
+
In particular that any updated shapes are consistent with the shapes of any
|
| 684 |
+
static (non-jax_coord) coordinates, since these will not be mapped over.
|
| 685 |
+
|
| 686 |
+
Args:
|
| 687 |
+
func: Function from xarray.Variable to xarray.Variable.
|
| 688 |
+
tree_data: PyTree to be mapped over.
|
| 689 |
+
|
| 690 |
+
Returns:
|
| 691 |
+
PyTree with the same structure as `tree_data` but where xarray.Variables
|
| 692 |
+
within xarray datatypes have been mapped over by `func`. Any leaves outside
|
| 693 |
+
of xarray datatypes will be unchanged.
|
| 694 |
+
"""
|
| 695 |
+
return jax.tree.map(
|
| 696 |
+
lambda leaf: func(leaf) if isinstance(leaf, xarray.Variable) else leaf,
|
| 697 |
+
tree_data,
|
| 698 |
+
is_leaf=lambda x: isinstance(x, xarray.Variable))
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def tree_map_with_dims(
|
| 702 |
+
func: Callable[[jax.typing.ArrayLike, tuple[str, ...] | None],
|
| 703 |
+
jax.typing.ArrayLike],
|
| 704 |
+
data: _PyTree,
|
| 705 |
+
) -> _PyTree:
|
| 706 |
+
"""Like jax.tree.map but also passes in xarray dimensions where known.
|
| 707 |
+
|
| 708 |
+
This is convenient when applying logic to every jax array in some xarray data
|
| 709 |
+
structure, which wants to be sensitive to the xarray dimension names.
|
| 710 |
+
Typical examples of this would be jax operations relating to sharding where
|
| 711 |
+
you may want to map xarray dimension names to sharding axis names.
|
| 712 |
+
|
| 713 |
+
This only supports changing array shapes in limited situations (see below).
|
| 714 |
+
|
| 715 |
+
Unlike tree_map_variables above, this will also map over plain jax arrays
|
| 716 |
+
that don't occur within xarray.Variable nodes; these will be passed to func
|
| 717 |
+
with dims=None.
|
| 718 |
+
|
| 719 |
+
Args:
|
| 720 |
+
func: A function from (jax_array, dims) -> jax_array. dims will correspond
|
| 721 |
+
to the dimension names of the xarray.Variable containing the jax_array
|
| 722 |
+
where it occurs within an xarray.Variable, note this includes arrays
|
| 723 |
+
within xarray.Dataset and xarray.DataArray too. For plain jax arrays that
|
| 724 |
+
don't occur within an xarray.Variable, dims will be None.
|
| 725 |
+
The returned jax array should generally be of the same shape as the input.
|
| 726 |
+
However you can get away with changing the shape of a particular dimension
|
| 727 |
+
in limited circumstances: when there are no explicit coordinates involving
|
| 728 |
+
that dimension, or when the only coordinates involving that dimension are
|
| 729 |
+
jax_coords and you modify their shapes too in a consistent fashion.
|
| 730 |
+
You are not allowed to change the dimension order, add or remove
|
| 731 |
+
dimensions.
|
| 732 |
+
data: Any pytree with jax ArrayLike leaves suitable for use with
|
| 733 |
+
`jax.tree.map`. Thanks to xarray_jax such pytrees may include xarray
|
| 734 |
+
datatypes.
|
| 735 |
+
|
| 736 |
+
Returns:
|
| 737 |
+
A pytree of the same structure as data, with the result of applying func
|
| 738 |
+
to each jax array found.
|
| 739 |
+
"""
|
| 740 |
+
# All jax arrays within xarray.Dataset, xarray.DataArray (including
|
| 741 |
+
# jax_coord arrays) will be exposed via xarray.Variable internal nodes by
|
| 742 |
+
# xarray_jax's pytree registrations. So to find xarray dimension metadata
|
| 743 |
+
# it's sufficient to stop descending at xarray.Variable nodes:
|
| 744 |
+
def is_leaf(x):
|
| 745 |
+
return isinstance(x, xarray.Variable)
|
| 746 |
+
|
| 747 |
+
def wrapped_func(x):
|
| 748 |
+
if isinstance(x, xarray.Variable):
|
| 749 |
+
array = unwrap(x.data)
|
| 750 |
+
array = func(array, x.dims)
|
| 751 |
+
return Variable(dims=x.dims, data=array)
|
| 752 |
+
else:
|
| 753 |
+
return func(x, None)
|
| 754 |
+
|
| 755 |
+
return jax.tree_util.tree_map(wrapped_func, data, is_leaf=is_leaf)
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
_Carry = TypeVar('_Carry')
|
| 759 |
+
_X = TypeVar('_X')
|
| 760 |
+
_Y = TypeVar('_Y')
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def scan(f: Callable[[_Carry, _X], tuple[_Carry, _Y]],
|
| 764 |
+
init: _Carry,
|
| 765 |
+
dim: str,
|
| 766 |
+
xs: _X | None = None,
|
| 767 |
+
length: int | None = None,
|
| 768 |
+
reverse: bool = False,
|
| 769 |
+
unroll: int | bool = 1,
|
| 770 |
+
) -> tuple[_Carry, _Y]:
|
| 771 |
+
"""Like jax.lax.scan but supports xarray data.
|
| 772 |
+
|
| 773 |
+
This can handle a jax.tree containing any mix of xarray and plain jax data.
|
| 774 |
+
It scans along the dimension `dim` for xarray data, and the leading axis for
|
| 775 |
+
any non-xarray data. These scanned-along dimensions must all be consistent in
|
| 776 |
+
size.
|
| 777 |
+
|
| 778 |
+
Static coordinates along `dim` in the `xs` will not be present on the `x`
|
| 779 |
+
argument to `f`, since they would be different on each iteration and we can't
|
| 780 |
+
pass them through as static data. jax_coords will be passed through correctly
|
| 781 |
+
however.
|
| 782 |
+
|
| 783 |
+
Static coordinates along `dim` on the `xs` will also not be present on the
|
| 784 |
+
resulting `ys`, you will need to copy these across yourself if desired.
|
| 785 |
+
(This one we may be able to fix in future.)
|
| 786 |
+
|
| 787 |
+
Args:
|
| 788 |
+
f: Function to apply at each step of the scan. This should map
|
| 789 |
+
(carry, x) -> (carry, y), where `x` is a slice of the `xs` along the `dim`
|
| 790 |
+
axis (with the `dim` axis and any coordinates using it dropped), and `y`
|
| 791 |
+
is a slice of the desired output along the `dim` axis, with no `dim` axis
|
| 792 |
+
itself.
|
| 793 |
+
x, y and carry can in general be trees, in which the above applies to
|
| 794 |
+
each leaf of the tree.
|
| 795 |
+
init: Initial value of the carry.
|
| 796 |
+
dim: The xarray dimension name to scan along, for xarray data.
|
| 797 |
+
xs: The input to be scanned. If not provided, will scan over `length`
|
| 798 |
+
iterations with None passed as the `x` argument to `f`.
|
| 799 |
+
length: The length of the scan, if `xs` are not provided.
|
| 800 |
+
reverse: Whether to scan in reverse order.
|
| 801 |
+
unroll: How many steps to unroll the scan, see jax.lax.scan.
|
| 802 |
+
|
| 803 |
+
Returns:
|
| 804 |
+
final_carry: The carry returned from the final step of the scan.
|
| 805 |
+
ys: Data corresponding to the `y` returned from `f` on each step of the
|
| 806 |
+
scan, concatenated along an extra leading dimension (named `dim` for
|
| 807 |
+
xarray data).
|
| 808 |
+
"""
|
| 809 |
+
if xs is not None:
|
| 810 |
+
# Ensure `dim` is the leading axis on any xarray data in the `xs`. This is
|
| 811 |
+
# what jax.lax.scan will scan over. (Any non-array data it's on you to put
|
| 812 |
+
# the relevant axis first).
|
| 813 |
+
xs = tree_map_variables(lambda v: v.transpose(dim, ...), xs)
|
| 814 |
+
xs_leaves, xs_treedef = jax.tree.flatten(xs)
|
| 815 |
+
else:
|
| 816 |
+
xs_treedef = None
|
| 817 |
+
xs_leaves = None
|
| 818 |
+
|
| 819 |
+
y_treedef = None
|
| 820 |
+
|
| 821 |
+
def scan_fn(carry, x_leaves):
|
| 822 |
+
if x_leaves is None:
|
| 823 |
+
x = None
|
| 824 |
+
else:
|
| 825 |
+
with dims_change_on_unflatten(lambda dims: dims[1:]):
|
| 826 |
+
x = jax.tree.unflatten(xs_treedef, x_leaves)
|
| 827 |
+
carry, y = f(carry, x)
|
| 828 |
+
|
| 829 |
+
nonlocal y_treedef
|
| 830 |
+
y_leaves, y_treedef = jax.tree.flatten(y)
|
| 831 |
+
return carry, y_leaves
|
| 832 |
+
|
| 833 |
+
final_carry, ys_leaves = jax.lax.scan(
|
| 834 |
+
scan_fn,
|
| 835 |
+
init,
|
| 836 |
+
xs_leaves,
|
| 837 |
+
length=length,
|
| 838 |
+
reverse=reverse,
|
| 839 |
+
unroll=unroll)
|
| 840 |
+
|
| 841 |
+
assert isinstance(y_treedef, jax.tree_util.PyTreeDef)
|
| 842 |
+
|
| 843 |
+
with dims_change_on_unflatten(lambda dims: (dim,) + dims):
|
| 844 |
+
ys = jax.tree.unflatten(y_treedef, ys_leaves)
|
| 845 |
+
|
| 846 |
+
return final_carry, ys
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
# Register xarray datatypes with jax.tree_util.
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
DimsChangeFn = Callable[[Tuple[Hashable, ...]], Tuple[Hashable, ...]]
|
| 853 |
+
_DIMS_CHANGE_ON_UNFLATTEN_FN: contextvars.ContextVar[DimsChangeFn] = (
|
| 854 |
+
contextvars.ContextVar('dims_change_on_unflatten_fn'))
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
@contextlib.contextmanager
|
| 858 |
+
def dims_change_on_unflatten(dims_change_fn: DimsChangeFn):
|
| 859 |
+
"""Can be used to change the dims used when unflattening arrays into xarrays.
|
| 860 |
+
|
| 861 |
+
This is useful when some axes were added to / removed from the underlying jax
|
| 862 |
+
arrays after they were flattened using jax.tree_util.tree_flatten, and you
|
| 863 |
+
want to unflatten them again afterwards using the original treedef but
|
| 864 |
+
adjusted for the added/removed dimensions.
|
| 865 |
+
|
| 866 |
+
It can also be used with jax.tree_util.tree_map, when it's called with a
|
| 867 |
+
function that adds/removes axes or otherwise changes the axis order.
|
| 868 |
+
|
| 869 |
+
When dimensions are removed, any coordinates using those removed dimensions
|
| 870 |
+
will also be removed on unflatten.
|
| 871 |
+
|
| 872 |
+
This is implemented as a context manager that sets some thread-local state
|
| 873 |
+
affecting the behaviour of our unflatten functions, because it's not possible
|
| 874 |
+
to directly modify the treedef to change the dims/coords in it (and with
|
| 875 |
+
tree_map, the treedef isn't exposed to you anyway).
|
| 876 |
+
|
| 877 |
+
Args:
|
| 878 |
+
dims_change_fn: Maps a tuple of dimension names for the original
|
| 879 |
+
Variable/DataArray/Dataset that was flattened, to an updated tuple of
|
| 880 |
+
dimensions which should be used when unflattening.
|
| 881 |
+
|
| 882 |
+
Yields:
|
| 883 |
+
To a context manager in whose scope jax.tree_util.tree_unflatten and
|
| 884 |
+
jax.tree_util.tree_map will apply the dims_change_fn before reconstructing
|
| 885 |
+
xarrays from jax arrays.
|
| 886 |
+
"""
|
| 887 |
+
token = _DIMS_CHANGE_ON_UNFLATTEN_FN.set(dims_change_fn)
|
| 888 |
+
try:
|
| 889 |
+
yield
|
| 890 |
+
finally:
|
| 891 |
+
_DIMS_CHANGE_ON_UNFLATTEN_FN.reset(token)
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
def _flatten_variable(v: xarray.Variable) -> Tuple[
|
| 895 |
+
Tuple[jax.typing.ArrayLike], Tuple[Hashable, ...]]: # pylint: disable=g-one-element-tuple
|
| 896 |
+
"""Flattens a Variable for jax.tree_util."""
|
| 897 |
+
children = (unwrap_data(v),)
|
| 898 |
+
aux = v.dims
|
| 899 |
+
return children, aux
|
| 900 |
+
|
| 901 |
+
|
| 902 |
+
def _unflatten_variable(
|
| 903 |
+
aux: Tuple[Hashable, ...],
|
| 904 |
+
children: Tuple[jax.typing.ArrayLike]) -> xarray.Variable: # pylint: disable=g-one-element-tuple
|
| 905 |
+
"""Unflattens a Variable for jax.tree_util."""
|
| 906 |
+
dims = aux
|
| 907 |
+
dims_change_fn = _DIMS_CHANGE_ON_UNFLATTEN_FN.get(None)
|
| 908 |
+
if dims_change_fn: dims = dims_change_fn(dims)
|
| 909 |
+
return Variable(dims=dims, data=children[0])
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
def _split_static_and_jax_coords(
|
| 913 |
+
coords: xarray.core.coordinates.Coordinates) -> Tuple[
|
| 914 |
+
Mapping[Hashable, xarray.Variable], Mapping[Hashable, xarray.Variable]]:
|
| 915 |
+
static_coord_vars = {}
|
| 916 |
+
jax_coord_vars = {}
|
| 917 |
+
for name, coord in coords.items():
|
| 918 |
+
if coord.attrs.get(_JAX_COORD_ATTR_NAME, False):
|
| 919 |
+
jax_coord_vars[name] = coord.variable
|
| 920 |
+
else:
|
| 921 |
+
assert not isinstance(coord, (jax.Array, JaxArrayWrapper))
|
| 922 |
+
static_coord_vars[name] = coord.variable
|
| 923 |
+
return static_coord_vars, jax_coord_vars
|
| 924 |
+
|
| 925 |
+
|
| 926 |
+
def _drop_with_none_of_dims(
|
| 927 |
+
coord_vars: Mapping[Hashable, xarray.Variable],
|
| 928 |
+
dims: Tuple[Hashable, ...]) -> Mapping[Hashable, xarray.Variable]:
|
| 929 |
+
return {name: var for name, var in coord_vars.items()
|
| 930 |
+
if set(var.dims) <= set(dims)}
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
class _HashableCoords(collections.abc.Mapping):
|
| 934 |
+
"""Wraps a dict of xarray Variables as hashable, used for static coordinates.
|
| 935 |
+
|
| 936 |
+
This needs to be hashable so that when an xarray.Dataset is passed to a
|
| 937 |
+
jax.jit'ed function, jax can check whether it's seen an array with the
|
| 938 |
+
same static coordinates(*) before or whether it needs to recompile the
|
| 939 |
+
function for the new values of the static coordinates.
|
| 940 |
+
|
| 941 |
+
(*) note jax_coords are not included in this; their value can be different
|
| 942 |
+
on different calls without triggering a recompile.
|
| 943 |
+
"""
|
| 944 |
+
|
| 945 |
+
def __init__(self, coord_vars: Mapping[Hashable, xarray.Variable]):
|
| 946 |
+
self._variables = coord_vars
|
| 947 |
+
|
| 948 |
+
def __repr__(self) -> str:
|
| 949 |
+
return f'_HashableCoords({repr(self._variables)})'
|
| 950 |
+
|
| 951 |
+
def __getitem__(self, key: Hashable) -> xarray.Variable:
|
| 952 |
+
return self._variables[key]
|
| 953 |
+
|
| 954 |
+
def __len__(self) -> int:
|
| 955 |
+
return len(self._variables)
|
| 956 |
+
|
| 957 |
+
def __iter__(self) -> Iterator[Hashable]:
|
| 958 |
+
return iter(self._variables)
|
| 959 |
+
|
| 960 |
+
def __hash__(self):
|
| 961 |
+
if not hasattr(self, '_hash'):
|
| 962 |
+
self._hash = hash(frozenset((name, var.data.tobytes())
|
| 963 |
+
for name, var in self._variables.items()))
|
| 964 |
+
return self._hash
|
| 965 |
+
|
| 966 |
+
def __eq__(self, other):
|
| 967 |
+
if self is other:
|
| 968 |
+
return True
|
| 969 |
+
elif not isinstance(other, type(self)):
|
| 970 |
+
return NotImplemented
|
| 971 |
+
elif self._variables is other._variables:
|
| 972 |
+
return True
|
| 973 |
+
else:
|
| 974 |
+
return self._variables.keys() == other._variables.keys() and all(
|
| 975 |
+
variable.equals(other._variables[name])
|
| 976 |
+
for name, variable in self._variables.items())
|
| 977 |
+
|
| 978 |
+
|
| 979 |
+
def _flatten_data_array(v: xarray.DataArray) -> Tuple[
|
| 980 |
+
# Children (data variable, jax_coord_vars):
|
| 981 |
+
Tuple[xarray.Variable, Mapping[Hashable, xarray.Variable]],
|
| 982 |
+
# Static auxiliary data (name, static_coord_vars):
|
| 983 |
+
Tuple[Optional[Hashable], _HashableCoords]]:
|
| 984 |
+
"""Flattens a DataArray for jax.tree_util."""
|
| 985 |
+
static_coord_vars, jax_coord_vars = _split_static_and_jax_coords(v.coords)
|
| 986 |
+
children = (v.variable, jax_coord_vars)
|
| 987 |
+
aux = (v.name, _HashableCoords(static_coord_vars))
|
| 988 |
+
return children, aux
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
def _unflatten_data_array(
|
| 992 |
+
aux: Tuple[Optional[Hashable], _HashableCoords],
|
| 993 |
+
children: Tuple[xarray.Variable, Mapping[Hashable, xarray.Variable]],
|
| 994 |
+
) -> xarray.DataArray:
|
| 995 |
+
"""Unflattens a DataArray for jax.tree_util."""
|
| 996 |
+
variable, jax_coord_vars = children
|
| 997 |
+
name, static_coord_vars = aux
|
| 998 |
+
if _DIMS_CHANGE_ON_UNFLATTEN_FN.get(None):
|
| 999 |
+
# Drop static coords which have dims not present in any of the data_vars.
|
| 1000 |
+
# These would generally be dims that were dropped by a dims_change_fn, but
|
| 1001 |
+
# because static coordinates don't go through dims_change_fn on unflatten,
|
| 1002 |
+
# we just drop them where this causes a problem.
|
| 1003 |
+
# Since jax_coords go through the dims_change_fn on unflatten we don't need
|
| 1004 |
+
# to do this for jax_coords.
|
| 1005 |
+
static_coord_vars = _drop_with_none_of_dims(
|
| 1006 |
+
static_coord_vars, variable.dims)
|
| 1007 |
+
return DataArray(
|
| 1008 |
+
variable, name=name, coords=static_coord_vars, jax_coords=jax_coord_vars)
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def _flatten_dataset(dataset: xarray.Dataset) -> Tuple[
|
| 1012 |
+
# Children (data variables, jax_coord_vars):
|
| 1013 |
+
Tuple[Mapping[Hashable, xarray.Variable],
|
| 1014 |
+
Mapping[Hashable, xarray.Variable]],
|
| 1015 |
+
# Static auxiliary data (static_coord_vars):
|
| 1016 |
+
_HashableCoords]:
|
| 1017 |
+
"""Flattens a Dataset for jax.tree_util."""
|
| 1018 |
+
variables = {name: data_array.variable
|
| 1019 |
+
for name, data_array in dataset.data_vars.items()}
|
| 1020 |
+
static_coord_vars, jax_coord_vars = _split_static_and_jax_coords(
|
| 1021 |
+
dataset.coords)
|
| 1022 |
+
children = (variables, jax_coord_vars)
|
| 1023 |
+
aux = _HashableCoords(static_coord_vars)
|
| 1024 |
+
return children, aux
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
def _unflatten_dataset(
|
| 1028 |
+
aux: _HashableCoords,
|
| 1029 |
+
children: Tuple[Mapping[Hashable, xarray.Variable],
|
| 1030 |
+
Mapping[Hashable, xarray.Variable]],
|
| 1031 |
+
) -> xarray.Dataset:
|
| 1032 |
+
"""Unflattens a Dataset for jax.tree_util."""
|
| 1033 |
+
data_vars, jax_coord_vars = children
|
| 1034 |
+
static_coord_vars = aux
|
| 1035 |
+
dataset = xarray.Dataset(data_vars)
|
| 1036 |
+
if _DIMS_CHANGE_ON_UNFLATTEN_FN.get(None):
|
| 1037 |
+
# Drop static coords which have dims not present in any of the data_vars.
|
| 1038 |
+
# See corresponding comment in _unflatten_data_array.
|
| 1039 |
+
static_coord_vars = _drop_with_none_of_dims(
|
| 1040 |
+
static_coord_vars, dataset.dims) # pytype: disable=wrong-arg-types
|
| 1041 |
+
return assign_coords(
|
| 1042 |
+
dataset, coords=static_coord_vars, jax_coords=jax_coord_vars)
|
| 1043 |
+
|
| 1044 |
+
|
| 1045 |
+
def _flatten_datatree(datatree: xarray.DataTree) -> Tuple[
|
| 1046 |
+
Tuple[Mapping[str, xarray.DataTree], xarray.Dataset], str | None]:
|
| 1047 |
+
"""Flattens a DataTree for jax.tree_util."""
|
| 1048 |
+
# For simplicity we assume DataTrees will be flattened/unflattened from the
|
| 1049 |
+
# root. If you give it a non-root-node, it will still work but any parents
|
| 1050 |
+
# (and any coordinates inherited from them) will be lost.
|
| 1051 |
+
node_dataset = datatree.to_dataset(inherit=False)
|
| 1052 |
+
children = (dict(datatree.children), node_dataset)
|
| 1053 |
+
aux = datatree.name
|
| 1054 |
+
return children, aux
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
def _unflatten_datatree(
|
| 1058 |
+
aux: str | None,
|
| 1059 |
+
children: Tuple[Mapping[str, xarray.DataTree], xarray.Dataset],
|
| 1060 |
+
) -> xarray.DataTree:
|
| 1061 |
+
"""Unflattens a DataTree for jax.tree_util."""
|
| 1062 |
+
children_dict, node_dataset = children
|
| 1063 |
+
name = aux
|
| 1064 |
+
return xarray.DataTree(
|
| 1065 |
+
dataset=node_dataset, children=children_dict, name=name)
|
| 1066 |
+
|
| 1067 |
+
|
| 1068 |
+
jax.tree_util.register_pytree_node(
|
| 1069 |
+
xarray.Variable, _flatten_variable, _unflatten_variable)
|
| 1070 |
+
# This is a subclass of Variable but still needs registering separately.
|
| 1071 |
+
# Flatten/unflatten for IndexVariable is a bit of a corner case but we do
|
| 1072 |
+
# need to support it.
|
| 1073 |
+
jax.tree_util.register_pytree_node(
|
| 1074 |
+
xarray.IndexVariable, _flatten_variable, _unflatten_variable)
|
| 1075 |
+
jax.tree_util.register_pytree_node(
|
| 1076 |
+
xarray.DataArray, _flatten_data_array, _unflatten_data_array)
|
| 1077 |
+
jax.tree_util.register_pytree_node(
|
| 1078 |
+
xarray.Dataset, _flatten_dataset, _unflatten_dataset)
|
| 1079 |
+
jax.tree_util.register_pytree_node(
|
| 1080 |
+
xarray.DataTree, _flatten_datatree, _unflatten_datatree)
|
model/graphcast/xarray_tree.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 DeepMind Technologies Limited.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Utilities for working with trees of xarray.DataArray (including Datasets).
|
| 15 |
+
|
| 16 |
+
Note that xarray.Dataset doesn't work out-of-the-box with the `tree` library;
|
| 17 |
+
it won't work as a leaf node since it implements Mapping, but also won't work
|
| 18 |
+
as an internal node since tree doesn't know how to re-create it properly.
|
| 19 |
+
|
| 20 |
+
To fix this, we reimplement a subset of `map_structure`, exposing its
|
| 21 |
+
constituent DataArrays as leaf nodes. This means it can be mapped over as a
|
| 22 |
+
generic container of DataArrays, while still preserving the result as a Dataset
|
| 23 |
+
where possible.
|
| 24 |
+
|
| 25 |
+
This is useful because in a few places we need to handle a general
|
| 26 |
+
Mapping[str, DataArray] (where the coordinates might not be compatible across
|
| 27 |
+
the constituent DataArrays) but also the special case of a Dataset nicely.
|
| 28 |
+
|
| 29 |
+
For the result e.g. of a tree.map_structure(fn, dataset), if fn returns None for
|
| 30 |
+
some of the child DataArrays, they will be omitted from the returned dataset. If
|
| 31 |
+
any values other than DataArrays or None are returned, then we don't attempt to
|
| 32 |
+
return a Dataset and just return a plain dict of the results. Similarly if
|
| 33 |
+
DataArrays are returned but with non-matching coordinates, it will just return a
|
| 34 |
+
plain dict of DataArrays.
|
| 35 |
+
|
| 36 |
+
Note xarray datatypes are registered with `jax.tree_util` by xarray_jax.py,
|
| 37 |
+
but `jax.tree_util.tree_map` is distinct from the `xarray_tree.map_structure`.
|
| 38 |
+
as the former exposes the underlying JAX/numpy arrays as leaf nodes, while the
|
| 39 |
+
latter exposes DataArrays as leaf nodes.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
from typing import Any, Callable
|
| 43 |
+
|
| 44 |
+
import xarray
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def map_structure(func: Callable[..., Any], *structures: Any) -> Any:
|
| 48 |
+
"""Maps func through given structures with xarrays. See tree.map_structure."""
|
| 49 |
+
if not callable(func):
|
| 50 |
+
raise TypeError(f'func must be callable, got: {func}')
|
| 51 |
+
if not structures:
|
| 52 |
+
raise ValueError('Must provide at least one structure')
|
| 53 |
+
|
| 54 |
+
first = structures[0]
|
| 55 |
+
if isinstance(first, xarray.Dataset):
|
| 56 |
+
data = {k: func(*[s[k] for s in structures]) for k in first.keys()}
|
| 57 |
+
if all(isinstance(a, (type(None), xarray.DataArray))
|
| 58 |
+
for a in data.values()):
|
| 59 |
+
data_arrays = [v.rename(k) for k, v in data.items() if v is not None]
|
| 60 |
+
try:
|
| 61 |
+
return xarray.merge(data_arrays, join='exact')
|
| 62 |
+
except ValueError: # Exact join not possible.
|
| 63 |
+
pass
|
| 64 |
+
return data
|
| 65 |
+
if isinstance(first, dict):
|
| 66 |
+
return {k: map_structure(func, *[s[k] for s in structures])
|
| 67 |
+
for k in first.keys()}
|
| 68 |
+
if isinstance(first, (list, tuple, set)):
|
| 69 |
+
return type(first)(map_structure(func, *s) for s in zip(*structures))
|
| 70 |
+
return func(*structures)
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""生成确定性的 ERA5、GenCast 统计量和静态场。"""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import h5py
|
| 11 |
+
import numpy as np
|
| 12 |
+
import xarray
|
| 13 |
+
import yaml
|
| 14 |
+
|
| 15 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 16 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
PRESSURE_LEVELS = (50, 100, 150, 200, 250, 300, 400, 500, 600, 700, 850, 925, 1000)
|
| 20 |
+
SURFACE = (
|
| 21 |
+
"2m_temperature", "mean_sea_level_pressure", "10m_v_component_of_wind",
|
| 22 |
+
"10m_u_component_of_wind", "sea_surface_temperature", "total_precipitation",
|
| 23 |
+
)
|
| 24 |
+
ATMOSPHERIC = (
|
| 25 |
+
"temperature", "geopotential", "u_component_of_wind",
|
| 26 |
+
"v_component_of_wind", "vertical_velocity", "specific_humidity",
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_args() -> argparse.Namespace:
|
| 31 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 32 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 33 |
+
parser.add_argument("--height", type=int)
|
| 34 |
+
parser.add_argument("--width", type=int)
|
| 35 |
+
parser.add_argument("--timesteps", type=int)
|
| 36 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 37 |
+
return parser.parse_args()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _stat_dataset(value: float, *, minimum: bool = False) -> xarray.Dataset:
|
| 41 |
+
data_vars = {}
|
| 42 |
+
surface_targets = (
|
| 43 |
+
"2m_temperature", "mean_sea_level_pressure", "10m_v_component_of_wind",
|
| 44 |
+
"10m_u_component_of_wind", "sea_surface_temperature", "total_precipitation_12hr",
|
| 45 |
+
)
|
| 46 |
+
for name in surface_targets:
|
| 47 |
+
scalar = -3.0 if minimum and name == "sea_surface_temperature" else value
|
| 48 |
+
data_vars[name] = xarray.DataArray(np.float32(scalar))
|
| 49 |
+
for name in ATMOSPHERIC:
|
| 50 |
+
data_vars[name] = xarray.DataArray(
|
| 51 |
+
np.full(len(PRESSURE_LEVELS), value, dtype=np.float32),
|
| 52 |
+
dims=("level",),
|
| 53 |
+
coords={"level": np.asarray(PRESSURE_LEVELS, dtype=np.int32)},
|
| 54 |
+
)
|
| 55 |
+
# Generated forcings already in [-1, 1] — include so normalization is silent
|
| 56 |
+
for name in ("year_progress_sin", "year_progress_cos",
|
| 57 |
+
"day_progress_sin", "day_progress_cos"):
|
| 58 |
+
data_vars[name] = xarray.DataArray(np.float32(value))
|
| 59 |
+
for name in ("geopotential_at_surface", "land_sea_mask"):
|
| 60 |
+
data_vars[name] = xarray.DataArray(np.float32(value))
|
| 61 |
+
return xarray.Dataset(data_vars)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def main() -> None:
|
| 65 |
+
args = parse_args()
|
| 66 |
+
with Path(args.config).open(encoding="utf-8") as source:
|
| 67 |
+
config = yaml.safe_load(source)
|
| 68 |
+
root = PROJECT_ROOT / config["data"]["data_dir"]
|
| 69 |
+
height = int(args.height or config["fake_data"]["height"])
|
| 70 |
+
width = int(args.width or config["fake_data"]["width"])
|
| 71 |
+
timesteps = int(args.timesteps or config["fake_data"]["timesteps"])
|
| 72 |
+
if width != 2 * (height - 1):
|
| 73 |
+
raise ValueError("Synthetic GenCast grid must satisfy width=2*(height-1)")
|
| 74 |
+
variables = list(SURFACE) + [f"{name}_{level}" for name in ATMOSPHERIC for level in PRESSURE_LEVELS]
|
| 75 |
+
years = sorted(set(config["data"]["train_years"] + config["data"]["test_years"]))
|
| 76 |
+
rng = np.random.default_rng(args.seed)
|
| 77 |
+
(root / "data").mkdir(parents=True, exist_ok=True)
|
| 78 |
+
for year in years:
|
| 79 |
+
path = root / "data" / f"{year}.h5"
|
| 80 |
+
time = np.arange(timesteps, dtype=np.float32)[:, None, None, None]
|
| 81 |
+
channel = np.arange(len(variables), dtype=np.float32)[None, :, None, None]
|
| 82 |
+
lat = np.linspace(1.0, -1.0, height, dtype=np.float32)[None, None, :, None]
|
| 83 |
+
lon = np.linspace(0.0, 2.0 * np.pi, width, endpoint=False, dtype=np.float32)[None, None, None, :]
|
| 84 |
+
values = 0.01 * time + 0.001 * channel + 0.1 * lat + 0.05 * np.sin(lon)
|
| 85 |
+
values += rng.normal(0.0, 1e-4, values.shape).astype(np.float32)
|
| 86 |
+
precip_index = variables.index("total_precipitation")
|
| 87 |
+
values[:, precip_index] = np.maximum(values[:, precip_index], 0.0)
|
| 88 |
+
sst_index = variables.index("sea_surface_temperature")
|
| 89 |
+
values[:, sst_index, : height // 4] = np.nan
|
| 90 |
+
with h5py.File(path, "w") as output:
|
| 91 |
+
fields = output.create_dataset("fields", data=values, chunks=(1, len(variables), height, width))
|
| 92 |
+
fields.attrs["variables"] = variables
|
| 93 |
+
fields.attrs["time_step"] = 6
|
| 94 |
+
print(f"Generated {path} shape={values.shape}")
|
| 95 |
+
|
| 96 |
+
static_dir = root / "static"
|
| 97 |
+
static_dir.mkdir(parents=True, exist_ok=True)
|
| 98 |
+
np.save(
|
| 99 |
+
static_dir / "geopotential_at_surface.npy",
|
| 100 |
+
np.zeros((height, width), dtype=np.float32),
|
| 101 |
+
)
|
| 102 |
+
land = np.zeros((height, width), dtype=np.float32)
|
| 103 |
+
land[: height // 4] = 1.0
|
| 104 |
+
np.save(static_dir / "land_mask.npy", land)
|
| 105 |
+
stats_dir = root / "stats"
|
| 106 |
+
stats_dir.mkdir(parents=True, exist_ok=True)
|
| 107 |
+
_stat_dataset(0.0).to_netcdf(stats_dir / "mean_by_level.nc")
|
| 108 |
+
_stat_dataset(1.0).to_netcdf(stats_dir / "stddev_by_level.nc")
|
| 109 |
+
_stat_dataset(1.0).to_netcdf(stats_dir / "diffs_stddev_by_level.nc")
|
| 110 |
+
_stat_dataset(0.0, minimum=True).to_netcdf(stats_dir / "min_by_level.nc")
|
| 111 |
+
print(f"Generated GenCast statistics and static fields under {root}")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""使用官方 GenCast DPM-Solver++ 执行集合自回归推理。"""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import sys
|
| 8 |
+
import warnings
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
warnings.filterwarnings("ignore", message="Changing the sparsity structure")
|
| 12 |
+
|
| 13 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 15 |
+
|
| 16 |
+
from model.common import configure_jax, load_config, load_stats, resolve_path
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_args() -> argparse.Namespace:
|
| 20 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 21 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 22 |
+
parser.add_argument("--checkpoint")
|
| 23 |
+
parser.add_argument("--sample-index", type=int, default=0)
|
| 24 |
+
parser.add_argument("--num-members", type=int)
|
| 25 |
+
parser.add_argument("--prediction-steps", type=int)
|
| 26 |
+
parser.add_argument("--output")
|
| 27 |
+
return parser.parse_args()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main() -> None:
|
| 31 |
+
args = parse_args()
|
| 32 |
+
config = load_config(args.config)
|
| 33 |
+
configure_jax(config["runtime"].get("platform", "auto"))
|
| 34 |
+
|
| 35 |
+
import jax
|
| 36 |
+
import numpy as np
|
| 37 |
+
import xarray
|
| 38 |
+
|
| 39 |
+
from model.graphcast import rollout
|
| 40 |
+
from model.gencast import GenCastModel, load_model_checkpoint
|
| 41 |
+
from model.common import (
|
| 42 |
+
load_trainer_checkpoint, validate_checkpoint_config,
|
| 43 |
+
)
|
| 44 |
+
from model.data_loader import GenCastERA5Dataset
|
| 45 |
+
|
| 46 |
+
prediction_steps = int(args.prediction_steps or config["inference"]["prediction_steps"])
|
| 47 |
+
num_members = int(args.num_members or config["inference"]["num_members"])
|
| 48 |
+
stats = load_stats(config["data"]["stats_dir"])
|
| 49 |
+
checkpoint_path = args.checkpoint or config["inference"].get("official_checkpoint")
|
| 50 |
+
if checkpoint_path:
|
| 51 |
+
official = load_model_checkpoint(resolve_path(checkpoint_path))
|
| 52 |
+
model = GenCastModel.from_checkpoint_and_stats(
|
| 53 |
+
official,
|
| 54 |
+
stats,
|
| 55 |
+
attention_type=config["inference"].get("attention_type_override"),
|
| 56 |
+
)
|
| 57 |
+
params, state = official.params, {}
|
| 58 |
+
task_config = official.task_config
|
| 59 |
+
else:
|
| 60 |
+
model = GenCastModel.from_config_and_stats(config, stats)
|
| 61 |
+
params, state, _, _, saved_config = load_trainer_checkpoint(
|
| 62 |
+
config["checkpoint"]["trainer"]
|
| 63 |
+
)
|
| 64 |
+
validate_checkpoint_config(config, saved_config, scope="inference")
|
| 65 |
+
task_config = model.task_config
|
| 66 |
+
|
| 67 |
+
dataset = GenCastERA5Dataset(
|
| 68 |
+
resolve_path(config["data"]["data_dir"]),
|
| 69 |
+
list(config["data"]["test_years"]),
|
| 70 |
+
static_dir=resolve_path(config["data"]["static_dir"]),
|
| 71 |
+
prediction_steps=prediction_steps,
|
| 72 |
+
stride=int(config["data"].get("test_stride", 1)),
|
| 73 |
+
task_config=task_config,
|
| 74 |
+
precipitation_interval_hours=int(
|
| 75 |
+
config["data"]["precipitation_interval_hours"]
|
| 76 |
+
),
|
| 77 |
+
load_future_targets=False,
|
| 78 |
+
)
|
| 79 |
+
inputs, targets, forcings = dataset[args.sample_index]
|
| 80 |
+
|
| 81 |
+
def forward(rng, inputs, targets_template, forcings):
|
| 82 |
+
return model.predict(
|
| 83 |
+
params, state, rng, inputs, targets_template, forcings
|
| 84 |
+
)[0]
|
| 85 |
+
|
| 86 |
+
forward = jax.jit(forward)
|
| 87 |
+
seed = int(config["inference"]["seed"])
|
| 88 |
+
rngs = np.stack([jax.random.fold_in(jax.random.PRNGKey(seed), i) for i in range(num_members)])
|
| 89 |
+
chunks = rollout.chunked_prediction_generator_multiple_runs(
|
| 90 |
+
predictor_fn=forward,
|
| 91 |
+
rngs=rngs,
|
| 92 |
+
inputs=inputs,
|
| 93 |
+
targets_template=targets * np.nan,
|
| 94 |
+
forcings=forcings,
|
| 95 |
+
num_steps_per_chunk=1,
|
| 96 |
+
num_samples=num_members,
|
| 97 |
+
pmap_devices=None,
|
| 98 |
+
)
|
| 99 |
+
output = resolve_path(args.output or config["output"]["prediction"])
|
| 100 |
+
if bool(config["inference"].get("stream_chunks", True)):
|
| 101 |
+
output_dir = output.with_suffix("")
|
| 102 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
for chunk_index, chunk in enumerate(chunks):
|
| 104 |
+
host_chunk = jax.device_get(chunk)
|
| 105 |
+
member = int(host_chunk.coords["sample"])
|
| 106 |
+
lead = int(host_chunk.time.values[0] / np.timedelta64(1, "h"))
|
| 107 |
+
host_chunk = host_chunk.drop_vars("sample").assign_coords(time=[lead])
|
| 108 |
+
host_chunk.coords["time"].attrs = {"long_name": "forecast lead time hours"}
|
| 109 |
+
host_chunk.attrs.update(
|
| 110 |
+
model="GenCast", target_channel_count=84,
|
| 111 |
+
forecast_reference_time=inputs.attrs["forecast_reference_time"],
|
| 112 |
+
)
|
| 113 |
+
path = output_dir / f"member_{member:03d}_lead_{lead:04d}h.nc"
|
| 114 |
+
host_chunk.to_netcdf(path)
|
| 115 |
+
print(f"Saved prediction chunk to {path}")
|
| 116 |
+
return
|
| 117 |
+
|
| 118 |
+
chunks = list(chunks)
|
| 119 |
+
member_chunks: list[list[xarray.Dataset]] = [[] for _ in range(num_members)]
|
| 120 |
+
for chunk in chunks:
|
| 121 |
+
host_chunk = jax.device_get(chunk)
|
| 122 |
+
member = int(host_chunk.coords["sample"])
|
| 123 |
+
member_chunks[member].append(host_chunk.drop_vars("sample"))
|
| 124 |
+
members = [
|
| 125 |
+
xarray.concat(parts, dim="time").expand_dims(sample=[member])
|
| 126 |
+
for member, parts in enumerate(member_chunks)
|
| 127 |
+
]
|
| 128 |
+
predictions = xarray.concat(members, dim="sample")
|
| 129 |
+
predictions.attrs.update(
|
| 130 |
+
model="GenCast",
|
| 131 |
+
target_channel_count=84,
|
| 132 |
+
ensemble_members=num_members,
|
| 133 |
+
step_hours=12,
|
| 134 |
+
forecast_reference_time=inputs.attrs["forecast_reference_time"],
|
| 135 |
+
)
|
| 136 |
+
# Store lead time as plain hours; xarray_jax's internal dtype attribute is
|
| 137 |
+
# not valid CF metadata and conflicts with decoding after NetCDF round-trip.
|
| 138 |
+
lead_hours = (
|
| 139 |
+
predictions.coords["time"].values / np.timedelta64(1, "h")
|
| 140 |
+
).astype(np.int32)
|
| 141 |
+
predictions = predictions.assign_coords(time=("time", lead_hours))
|
| 142 |
+
predictions.coords["time"].attrs = {
|
| 143 |
+
"long_name": "forecast lead time",
|
| 144 |
+
"units": "hours",
|
| 145 |
+
}
|
| 146 |
+
reference_time = np.datetime64(inputs.attrs["forecast_reference_time"])
|
| 147 |
+
predictions = predictions.assign_coords(
|
| 148 |
+
valid_time=("time", reference_time + lead_hours.astype("timedelta64[h]"))
|
| 149 |
+
)
|
| 150 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 151 |
+
temporary = output.with_suffix(output.suffix + ".tmp")
|
| 152 |
+
predictions.to_netcdf(temporary)
|
| 153 |
+
temporary.replace(output)
|
| 154 |
+
print(f"Saved predictions to {output}")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
main()
|
scripts/result.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""可视化 GenCast 集合预报结果。"""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import sys
|
| 8 |
+
import warnings
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
import numpy as np
|
| 13 |
+
import xarray
|
| 14 |
+
|
| 15 |
+
warnings.filterwarnings("ignore", message="Changing the sparsity structure")
|
| 16 |
+
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 19 |
+
|
| 20 |
+
from model.common import load_config, resolve_path
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def load_predictions(path: Path) -> xarray.Dataset:
|
| 24 |
+
"""Load predictions from either a single NetCDF or a chunked directory."""
|
| 25 |
+
# Auto-detect: if path.nc doesn't exist but path/ directory does, use chunks
|
| 26 |
+
if not path.exists() and path.suffix == ".nc":
|
| 27 |
+
chunk_dir = path.with_suffix("")
|
| 28 |
+
if chunk_dir.is_dir():
|
| 29 |
+
path = chunk_dir
|
| 30 |
+
if path.is_dir():
|
| 31 |
+
files = sorted(path.glob("member_*_lead_*.nc"))
|
| 32 |
+
if not files:
|
| 33 |
+
raise FileNotFoundError(f"No prediction chunks found in {path}")
|
| 34 |
+
datasets = [xarray.load_dataset(f) for f in files]
|
| 35 |
+
members = sorted(set(int(f.stem.split("_")[1]) for f in files))
|
| 36 |
+
member_parts = []
|
| 37 |
+
for m in members:
|
| 38 |
+
member_ds = [ds for ds, f in zip(datasets, files)
|
| 39 |
+
if f"member_{m:03d}_lead_" in str(f)]
|
| 40 |
+
member_parts.append(xarray.concat(member_ds, dim="time"))
|
| 41 |
+
return xarray.concat(member_parts, dim="sample")
|
| 42 |
+
else:
|
| 43 |
+
return xarray.load_dataset(path)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def parse_args() -> argparse.Namespace:
|
| 47 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 48 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 49 |
+
parser.add_argument("--prediction")
|
| 50 |
+
parser.add_argument("--variable", default="2m_temperature")
|
| 51 |
+
parser.add_argument("--output")
|
| 52 |
+
parser.add_argument("--lead", type=int, default=0, help="lead time index (0-based)")
|
| 53 |
+
return parser.parse_args()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main() -> None:
|
| 57 |
+
args = parse_args()
|
| 58 |
+
config = load_config(args.config)
|
| 59 |
+
prediction_path = resolve_path(args.prediction or config["output"]["prediction"])
|
| 60 |
+
output_path = resolve_path(args.output or config["output"]["plot"])
|
| 61 |
+
prediction = load_predictions(prediction_path)[args.variable]
|
| 62 |
+
|
| 63 |
+
sample_dim = "sample" if "sample" in prediction.dims else None
|
| 64 |
+
mean = prediction.mean(sample_dim) if sample_dim else prediction
|
| 65 |
+
spread = prediction.std(sample_dim) if sample_dim else xarray.zeros_like(mean)
|
| 66 |
+
|
| 67 |
+
# Select lead time index (0=first, -1=last)
|
| 68 |
+
lead_idx = args.lead
|
| 69 |
+
field = mean.isel(batch=0, time=lead_idx).values
|
| 70 |
+
spread_field = spread.isel(batch=0, time=lead_idx).values
|
| 71 |
+
|
| 72 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 4), constrained_layout=True)
|
| 73 |
+
im0 = axes[0].imshow(field, origin="lower", cmap="viridis", aspect="auto")
|
| 74 |
+
axes[0].set_title(f"GenCast ensemble mean: {args.variable}")
|
| 75 |
+
fig.colorbar(im0, ax=axes[0], orientation="horizontal")
|
| 76 |
+
im1 = axes[1].imshow(spread_field, origin="lower", cmap="magma", aspect="auto")
|
| 77 |
+
axes[1].set_title("Ensemble spread")
|
| 78 |
+
fig.colorbar(im1, ax=axes[1], orientation="horizontal")
|
| 79 |
+
|
| 80 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
fig.savefig(output_path, dpi=180)
|
| 82 |
+
plt.close(fig)
|
| 83 |
+
print(f"Saved plot to {output_path}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
main()
|
scripts/train.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""使用官方单步 EDM 去噪目标训练 GenCast。"""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import itertools
|
| 8 |
+
import sys
|
| 9 |
+
import warnings
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import xarray
|
| 13 |
+
|
| 14 |
+
# Mesh adjacency construction triggers one-time scipy CSR restructure warning.
|
| 15 |
+
warnings.filterwarnings("ignore", message="Changing the sparsity structure")
|
| 16 |
+
|
| 17 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 19 |
+
|
| 20 |
+
from model.common import configure_jax, load_config, load_stats, resolve_path
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args() -> argparse.Namespace:
|
| 24 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 25 |
+
parser.add_argument("--config", default=str(PROJECT_ROOT / "conf/config.yaml"))
|
| 26 |
+
parser.add_argument("--max-steps", type=int)
|
| 27 |
+
parser.add_argument("--resume")
|
| 28 |
+
parser.add_argument("--parallel-mode", choices=("single", "pmap"))
|
| 29 |
+
parser.add_argument("--num-devices", type=int)
|
| 30 |
+
parser.add_argument("--global-batch-size", type=int)
|
| 31 |
+
parser.add_argument("--checkpoint")
|
| 32 |
+
parser.add_argument("--seed", type=int)
|
| 33 |
+
return parser.parse_args()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _adam_init(params):
|
| 37 |
+
import jax
|
| 38 |
+
import jax.numpy as jnp
|
| 39 |
+
|
| 40 |
+
zeros = jax.tree_util.tree_map(jnp.zeros_like, params)
|
| 41 |
+
return {"count": jnp.asarray(0, dtype=jnp.int32), "mu": zeros, "nu": zeros}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _adam_update(params, grads, state, learning_rate, beta1, beta2, eps):
|
| 45 |
+
import jax
|
| 46 |
+
import jax.numpy as jnp
|
| 47 |
+
|
| 48 |
+
count = state["count"] + 1
|
| 49 |
+
mu = jax.tree_util.tree_map(
|
| 50 |
+
lambda old, grad: beta1 * old + (1.0 - beta1) * grad,
|
| 51 |
+
state["mu"], grads,
|
| 52 |
+
)
|
| 53 |
+
nu = jax.tree_util.tree_map(
|
| 54 |
+
lambda old, grad: beta2 * old + (1.0 - beta2) * jnp.square(grad),
|
| 55 |
+
state["nu"], grads,
|
| 56 |
+
)
|
| 57 |
+
mu_hat = jax.tree_util.tree_map(lambda value: value / (1.0 - beta1**count), mu)
|
| 58 |
+
nu_hat = jax.tree_util.tree_map(lambda value: value / (1.0 - beta2**count), nu)
|
| 59 |
+
params = jax.tree_util.tree_map(
|
| 60 |
+
lambda value, first, second: value - learning_rate * first / (jnp.sqrt(second) + eps),
|
| 61 |
+
params, mu_hat, nu_hat,
|
| 62 |
+
)
|
| 63 |
+
return params, {"count": count, "mu": mu, "nu": nu}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _replicate(tree, devices):
|
| 67 |
+
import jax
|
| 68 |
+
|
| 69 |
+
return jax.device_put_replicated(tree, devices)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _unreplicate(tree):
|
| 73 |
+
import jax
|
| 74 |
+
|
| 75 |
+
return jax.tree_util.tree_map(lambda value: value[0], tree)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _device_batch(batch, device_count):
|
| 79 |
+
"""Add a leading device dimension to each GenCast xarray input."""
|
| 80 |
+
result = []
|
| 81 |
+
for value in batch:
|
| 82 |
+
if not isinstance(value, xarray.Dataset):
|
| 83 |
+
raise TypeError("GenCast batches must contain xarray.Dataset values")
|
| 84 |
+
value = value.transpose("batch", ...)
|
| 85 |
+
if "batch" not in value.dims:
|
| 86 |
+
value = value.expand_dims("batch")
|
| 87 |
+
if value.sizes["batch"] % device_count:
|
| 88 |
+
raise ValueError("Batch size must be divisible by the device count")
|
| 89 |
+
local_batch = value.sizes["batch"] // device_count
|
| 90 |
+
shards = [
|
| 91 |
+
value.isel(batch=slice(index * local_batch, (index + 1) * local_batch))
|
| 92 |
+
for index in range(device_count)
|
| 93 |
+
]
|
| 94 |
+
result.append(xarray.concat(shards, dim="device"))
|
| 95 |
+
return tuple(result)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def main() -> None:
|
| 99 |
+
args = parse_args()
|
| 100 |
+
config = load_config(args.config)
|
| 101 |
+
parallel = config.setdefault("parallel", {})
|
| 102 |
+
if args.parallel_mode is not None:
|
| 103 |
+
parallel["mode"] = args.parallel_mode
|
| 104 |
+
if args.num_devices is not None:
|
| 105 |
+
parallel["num_devices"] = args.num_devices
|
| 106 |
+
if args.global_batch_size is not None:
|
| 107 |
+
parallel["global_batch_size"] = args.global_batch_size
|
| 108 |
+
if args.checkpoint is not None:
|
| 109 |
+
config["checkpoint"]["trainer"] = args.checkpoint
|
| 110 |
+
if args.seed is not None:
|
| 111 |
+
config["training"]["seed"] = args.seed
|
| 112 |
+
configure_jax(config["runtime"].get("platform", "auto"))
|
| 113 |
+
|
| 114 |
+
import jax
|
| 115 |
+
import jax.numpy as jnp
|
| 116 |
+
|
| 117 |
+
from model.gencast import GenCastModel, parameter_count
|
| 118 |
+
from model.common import (
|
| 119 |
+
load_trainer_checkpoint, save_trainer_checkpoint,
|
| 120 |
+
validate_checkpoint_config,
|
| 121 |
+
)
|
| 122 |
+
from model.data_loader import GenCastERA5Dataset, batch_iterator
|
| 123 |
+
|
| 124 |
+
mode = str(parallel.get("mode", "single")).lower()
|
| 125 |
+
if mode not in ("single", "pmap"):
|
| 126 |
+
raise ValueError("parallel.mode must be 'single' or 'pmap'")
|
| 127 |
+
devices = list(jax.local_devices())
|
| 128 |
+
requested_devices = int(parallel.get("num_devices", 1))
|
| 129 |
+
if requested_devices < 1:
|
| 130 |
+
raise ValueError("parallel.num_devices must be positive")
|
| 131 |
+
if mode == "pmap":
|
| 132 |
+
if requested_devices > len(devices):
|
| 133 |
+
raise ValueError(
|
| 134 |
+
f"Requested {requested_devices} devices, only {len(devices)} available"
|
| 135 |
+
)
|
| 136 |
+
devices = devices[:requested_devices]
|
| 137 |
+
else:
|
| 138 |
+
requested_devices = 1
|
| 139 |
+
devices = devices[:1]
|
| 140 |
+
global_batch_size = int(parallel.get("global_batch_size", requested_devices))
|
| 141 |
+
if global_batch_size < 1 or global_batch_size % requested_devices:
|
| 142 |
+
raise ValueError("global_batch_size must be divisible by the device count")
|
| 143 |
+
stats = load_stats(config["data"]["stats_dir"])
|
| 144 |
+
model = GenCastModel.from_config_and_stats(config, stats)
|
| 145 |
+
dataset = GenCastERA5Dataset(
|
| 146 |
+
resolve_path(config["data"]["data_dir"]),
|
| 147 |
+
list(config["data"]["train_years"]),
|
| 148 |
+
static_dir=resolve_path(config["data"]["static_dir"]),
|
| 149 |
+
prediction_steps=1,
|
| 150 |
+
stride=int(config["data"].get("train_stride", 1)),
|
| 151 |
+
precipitation_interval_hours=int(
|
| 152 |
+
config["data"]["precipitation_interval_hours"]
|
| 153 |
+
),
|
| 154 |
+
)
|
| 155 |
+
first_batch = dataset[0]
|
| 156 |
+
seed = int(config["training"]["seed"])
|
| 157 |
+
start_step = 0
|
| 158 |
+
resume = args.resume or config["checkpoint"].get("resume")
|
| 159 |
+
if resume:
|
| 160 |
+
params, state, optimizer_state, start_step, saved_config = \
|
| 161 |
+
load_trainer_checkpoint(resume)
|
| 162 |
+
validate_checkpoint_config(config, saved_config)
|
| 163 |
+
else:
|
| 164 |
+
params, state = model.init(
|
| 165 |
+
jax.random.fold_in(jax.random.PRNGKey(seed), -1), *first_batch
|
| 166 |
+
)
|
| 167 |
+
optimizer_state = _adam_init(params)
|
| 168 |
+
|
| 169 |
+
learning_rate = float(config["training"]["learning_rate"])
|
| 170 |
+
beta1, beta2 = (float(value) for value in config["training"]["betas"])
|
| 171 |
+
epsilon = float(config["training"].get("epsilon", 1e-8))
|
| 172 |
+
|
| 173 |
+
def train_step(params, state, optimizer_state, rng, inputs, targets, forcings):
|
| 174 |
+
def objective(current_params, current_state):
|
| 175 |
+
(loss, diagnostics), next_state = model.loss(
|
| 176 |
+
current_params, current_state, rng, inputs, targets, forcings
|
| 177 |
+
)
|
| 178 |
+
return loss, (diagnostics, next_state)
|
| 179 |
+
|
| 180 |
+
(loss, (diagnostics, next_state)), grads = jax.value_and_grad(
|
| 181 |
+
objective, has_aux=True
|
| 182 |
+
)(params, state)
|
| 183 |
+
finite = jnp.logical_and(
|
| 184 |
+
jnp.isfinite(loss),
|
| 185 |
+
jnp.all(jnp.asarray([jnp.all(jnp.isfinite(x)) for x in jax.tree_util.tree_leaves(grads)])),
|
| 186 |
+
)
|
| 187 |
+
new_params, new_optimizer_state = _adam_update(
|
| 188 |
+
params, grads, optimizer_state, learning_rate, beta1, beta2, epsilon
|
| 189 |
+
)
|
| 190 |
+
params = jax.tree_util.tree_map(
|
| 191 |
+
lambda new, old: jnp.where(finite, new, old), new_params, params
|
| 192 |
+
)
|
| 193 |
+
next_state = jax.tree_util.tree_map(
|
| 194 |
+
lambda new, old: jnp.where(finite, new, old), next_state, state
|
| 195 |
+
)
|
| 196 |
+
new_optimizer_state = jax.tree_util.tree_map(
|
| 197 |
+
lambda new, old: jnp.where(finite, new, old),
|
| 198 |
+
new_optimizer_state,
|
| 199 |
+
optimizer_state,
|
| 200 |
+
)
|
| 201 |
+
return params, next_state, new_optimizer_state, loss, diagnostics, finite
|
| 202 |
+
|
| 203 |
+
if mode == "pmap":
|
| 204 |
+
axis_name = str(parallel.get("axis_name", "devices"))
|
| 205 |
+
|
| 206 |
+
def parallel_train_step(
|
| 207 |
+
params, state, optimizer_state, rng, inputs, targets, forcings
|
| 208 |
+
):
|
| 209 |
+
rng = jax.random.fold_in(rng, jax.lax.axis_index(axis_name))
|
| 210 |
+
|
| 211 |
+
def objective(current_params, current_state):
|
| 212 |
+
(loss, diagnostics), next_state = model.loss(
|
| 213 |
+
current_params, current_state, rng, inputs, targets, forcings
|
| 214 |
+
)
|
| 215 |
+
return loss, (diagnostics, next_state)
|
| 216 |
+
|
| 217 |
+
(loss, (diagnostics, next_state)), grads = jax.value_and_grad(
|
| 218 |
+
objective, has_aux=True
|
| 219 |
+
)(params, state)
|
| 220 |
+
grads = jax.lax.pmean(grads, axis_name)
|
| 221 |
+
loss = jax.lax.pmean(loss, axis_name)
|
| 222 |
+
diagnostics = jax.tree_util.tree_map(
|
| 223 |
+
lambda value: jax.lax.pmean(value, axis_name), diagnostics
|
| 224 |
+
)
|
| 225 |
+
next_state = jax.tree_util.tree_map(
|
| 226 |
+
lambda value: jax.lax.pmean(value, axis_name), next_state
|
| 227 |
+
)
|
| 228 |
+
finite = jnp.logical_and(
|
| 229 |
+
jnp.isfinite(loss),
|
| 230 |
+
jnp.all(jnp.asarray([
|
| 231 |
+
jnp.all(jnp.isfinite(x))
|
| 232 |
+
for x in jax.tree_util.tree_leaves(grads)
|
| 233 |
+
])),
|
| 234 |
+
)
|
| 235 |
+
finite = jax.lax.pmin(finite, axis_name)
|
| 236 |
+
new_params, new_optimizer_state = _adam_update(
|
| 237 |
+
params, grads, optimizer_state, learning_rate, beta1, beta2, epsilon
|
| 238 |
+
)
|
| 239 |
+
params = jax.tree_util.tree_map(
|
| 240 |
+
lambda new, old: jnp.where(finite, new, old), new_params, params
|
| 241 |
+
)
|
| 242 |
+
next_state = jax.tree_util.tree_map(
|
| 243 |
+
lambda new, old: jnp.where(finite, new, old), next_state, state
|
| 244 |
+
)
|
| 245 |
+
new_optimizer_state = jax.tree_util.tree_map(
|
| 246 |
+
lambda new, old: jnp.where(finite, new, old),
|
| 247 |
+
new_optimizer_state,
|
| 248 |
+
optimizer_state,
|
| 249 |
+
)
|
| 250 |
+
return params, next_state, new_optimizer_state, loss, diagnostics, finite
|
| 251 |
+
|
| 252 |
+
from model.graphcast import xarray_jax
|
| 253 |
+
|
| 254 |
+
train_step = xarray_jax.pmap(
|
| 255 |
+
parallel_train_step, dim="device", axis_name=axis_name, devices=devices
|
| 256 |
+
)
|
| 257 |
+
else:
|
| 258 |
+
train_step = jax.jit(train_step)
|
| 259 |
+
max_steps = int(args.max_steps or config["training"]["max_steps"])
|
| 260 |
+
save_interval = int(config["training"].get("save_interval", max_steps))
|
| 261 |
+
checkpoint_path = config["checkpoint"]["trainer"]
|
| 262 |
+
print(f"Training samples: {len(dataset)}; parameters: {parameter_count(params):,}")
|
| 263 |
+
if mode == "pmap":
|
| 264 |
+
params = _replicate(params, devices)
|
| 265 |
+
state = _replicate(state, devices)
|
| 266 |
+
optimizer_state = _replicate(optimizer_state, devices)
|
| 267 |
+
print(
|
| 268 |
+
f"Parallel mode: pmap; devices: {requested_devices}; "
|
| 269 |
+
f"global batch: {global_batch_size}"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
step = start_step
|
| 273 |
+
batches_per_epoch = len(dataset) // global_batch_size
|
| 274 |
+
if batches_per_epoch < 1:
|
| 275 |
+
raise ValueError(
|
| 276 |
+
f"Dataset has {len(dataset)} samples, fewer than global_batch_size "
|
| 277 |
+
f"{global_batch_size}"
|
| 278 |
+
)
|
| 279 |
+
while step < max_steps:
|
| 280 |
+
epoch = step // batches_per_epoch
|
| 281 |
+
offset = step % batches_per_epoch
|
| 282 |
+
epoch_batches = batch_iterator(
|
| 283 |
+
dataset,
|
| 284 |
+
shuffle=True,
|
| 285 |
+
seed=seed + epoch,
|
| 286 |
+
batch_size=global_batch_size,
|
| 287 |
+
)
|
| 288 |
+
for batch in itertools.islice(epoch_batches, offset, None):
|
| 289 |
+
if step >= max_steps:
|
| 290 |
+
break
|
| 291 |
+
step_rng = jax.random.fold_in(jax.random.PRNGKey(seed), step)
|
| 292 |
+
if mode == "pmap":
|
| 293 |
+
batch = _device_batch(batch, requested_devices)
|
| 294 |
+
step_rng = jax.numpy.broadcast_to(
|
| 295 |
+
step_rng, (requested_devices, *step_rng.shape)
|
| 296 |
+
)
|
| 297 |
+
params, state, optimizer_state, loss, _, finite = train_step(
|
| 298 |
+
params, state, optimizer_state, step_rng, *batch
|
| 299 |
+
)
|
| 300 |
+
loss, finite = loss[0], finite[0]
|
| 301 |
+
else:
|
| 302 |
+
params, state, optimizer_state, loss, _, finite = train_step(
|
| 303 |
+
params, state, optimizer_state, step_rng, *batch
|
| 304 |
+
)
|
| 305 |
+
step += 1
|
| 306 |
+
print(f"step={step} loss={float(loss):.8f} finite={bool(finite)}")
|
| 307 |
+
if not bool(finite):
|
| 308 |
+
raise FloatingPointError(f"Non-finite GenCast loss at step {step}")
|
| 309 |
+
if step % save_interval == 0 or step == max_steps:
|
| 310 |
+
checkpoint_trees = (params, state, optimizer_state)
|
| 311 |
+
if mode == "pmap":
|
| 312 |
+
checkpoint_trees = tuple(map(_unreplicate, checkpoint_trees))
|
| 313 |
+
save_trainer_checkpoint(
|
| 314 |
+
checkpoint_path,
|
| 315 |
+
params=checkpoint_trees[0],
|
| 316 |
+
state=checkpoint_trees[1],
|
| 317 |
+
optimizer_state=checkpoint_trees[2],
|
| 318 |
+
step=step,
|
| 319 |
+
config=config,
|
| 320 |
+
)
|
| 321 |
+
print(f"Saved checkpoint to {resolve_path(checkpoint_path)}")
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
if __name__ == "__main__":
|
| 325 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|