diff --git "a/examples/PrithviWxC_inference.ipynb" "b/examples/PrithviWxC_inference.ipynb" new file mode 100644--- /dev/null +++ "b/examples/PrithviWxC_inference.ipynb" @@ -0,0 +1,3186 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PrithviWxC\n", + "\n", + "This notebook will walk you through how to construct the model,\n", + "load the weights, build the dataset, and use the model for inference." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "from huggingface_hub import hf_hub_download, snapshot_download" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now configure the backends and torch states, including setting the seeds for the RNGs." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "torch.jit.enable_onednn_fusion(True)\n", + "if torch.cuda.is_available():\n", + " print(f\"Using device: {torch.cuda.get_device_name()}\")\n", + " torch.backends.cudnn.benchmark = True\n", + " torch.backends.cudnn.deterministic = True\n", + "\n", + "random.seed(42)\n", + "if torch.cuda.is_available():\n", + " torch.cuda.manual_seed(42)\n", + "torch.manual_seed(42)\n", + "np.random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model has approximately 2.3 billion parameters, so it\n", + "requires reasonable computational resources, but it is possible\n", + "to run it on a CPU." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "if torch.cuda.is_available():\n", + " device = torch.device(\"cuda\")\n", + "else:\n", + " device = torch.device(\"cpu\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dataloader\n", + "### Variables and times\n", + "\n", + "With the environment ready to go, we now need to set up the task.\n", + "The core model expects a fixed set of variables from the MERRA-2\n", + "dataset, which are prescribed below. The variables are comprised\n", + "of surface variables, surface static variables, and variables at\n", + "various vertical levels within the atmosphere. More details on the\n", + "MERRA-2 dataset can be found\n", + "[here](https://gmao.gsfc.nasa.gov/reanalysis/MERRA-2/).\n", + "\n", + "The MERRA-2 dataset includes data at longitudes of $-180^\\circ$\n", + "and $+180^\\circ$. This represents duplicate data, so we set a\n", + "padding variable to remove it.\n", + "\n", + "The input to the core model consists of these variables at two\n", + "different times. The time difference in hours between these samples\n", + "is passed to the model and set in the input_time variable.\n", + "\n", + "The model's task is to predict the fixed set of variables at a\n", + "target time, given the input data.\n", + "\n", + "For example, if the input times are 0900 and 1200, resulting in\n", + "an input_time of -3, then a lead_time of 6 would result in a\n", + "target time of 1800." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "surface_vars = [\n", + " \"EFLUX\",\n", + " \"GWETROOT\",\n", + " \"HFLUX\",\n", + " \"LAI\",\n", + " \"LWGAB\",\n", + " \"LWGEM\",\n", + " \"LWTUP\",\n", + " \"PS\",\n", + " \"QV2M\",\n", + " \"SLP\",\n", + " \"SWGNT\",\n", + " \"SWTNT\",\n", + " \"T2M\",\n", + " \"TQI\",\n", + " \"TQL\",\n", + " \"TQV\",\n", + " \"TS\",\n", + " \"U10M\",\n", + " \"V10M\",\n", + " \"Z0M\",\n", + "]\n", + "static_surface_vars = [\"FRACI\", \"FRLAND\", \"FROCEAN\", \"PHIS\"]\n", + "vertical_vars = [\"CLOUD\", \"H\", \"OMEGA\", \"PL\", \"QI\", \"QL\", \"QV\", \"T\", \"U\", \"V\"]\n", + "levels = [\n", + " 34.0,\n", + " 39.0,\n", + " 41.0,\n", + " 43.0,\n", + " 44.0,\n", + " 45.0,\n", + " 48.0,\n", + " 51.0,\n", + " 53.0,\n", + " 56.0,\n", + " 63.0,\n", + " 68.0,\n", + " 71.0,\n", + " 72.0,\n", + "]\n", + "padding = {\"level\": [0, 0], \"lat\": [0, -1], \"lon\": [0, 0]}\n", + "\n", + "lead_times = [12] # This varibale can be change to change the task\n", + "input_times = [-6] # This varibale can be change to change the task" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data file\n", + "MERRA-2 data is available from 1980 to the present day,\n", + "at 3-hour temporal resolution. The dataloader we have provided\n", + "expects the surface data and vertical data to be saved in\n", + "separate files, and when provided with the directories, will\n", + "search for the relevant data that falls within the provided time range.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "45d1a1486bdc4dff82597d5cf87095f0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Fetching 1 files: 0%| | 0/1 [00:00 dict[str, Tensor]:\n", + " \"\"\"Prepressing function for MERRA2 Dataset\n", + "\n", + " Args:\n", + " batch (dict): List of training samples, each sample should be a\n", + " dictionary with the following keys::\n", + "\n", + " 'sur_static': Numpy array of shape (3, lat, lon). For each pixel (lat, lon), the first dimension indexes sin(lat), cos(lon), sin(lon).\n", + " 'sur_vals': Torch tensor of shape (parameter, time, lat, lon).\n", + " 'sur_tars': Torch tensor of shape (parameter, time, lat, lon).\n", + " 'ulv_vals': Torch tensor of shape (parameter, level, time, lat, lon).\n", + " 'ulv_tars': Torch tensor of shape (parameter, level, time, lat, lon).\n", + " 'sur_climate': Torch tensor of shape (parameter, lat, lon)\n", + " 'ulv_climate': Torch tensor of shape (parameter, level, lat, lon)\n", + " 'lead_time': Integer.\n", + " 'input_time': Integer.\n", + "\n", + " padding: Dictionary with keys 'level', 'lat', 'lon', each of dim 2.\n", + "\n", + " Returns:\n", + " Dictionary with the following keys::\n", + "\n", + " 'x': [batch, time, parameter, lat, lon]\n", + " 'y': [batch, parameter, lat, lon]\n", + " 'static': [batch, parameter, lat, lon]\n", + " 'lead_time': [batch]\n", + " 'input_time': [batch]\n", + " 'climate (Optional)': [batch, parameter, lat, lon]\n", + "\n", + " Note:\n", + " Here, for x and y, 'parameter' is [surface parameter, upper level,\n", + " parameter x level]. Similarly for the static information we have\n", + " [sin(lat), cos(lon), sin(lon), cos(doy), sin(doy), cos(hod), sin(hod),\n", + " ...].\n", + " \"\"\" # noqa: E501\n", + " b0 = batch[0]\n", + " nbatch = len(batch)\n", + " data_keys = set(b0.keys())\n", + "\n", + " essential_keys = {\n", + " \"sur_static\",\n", + " \"sur_vals\",\n", + " \"sur_tars\",\n", + " \"ulv_vals\",\n", + " \"ulv_tars\",\n", + " \"input_time\",\n", + " \"lead_time\",\n", + " }\n", + "\n", + " climate_keys = {\n", + " \"sur_climate\",\n", + " \"ulv_climate\",\n", + " }\n", + "\n", + " all_keys = essential_keys | climate_keys\n", + "\n", + " if not essential_keys.issubset(data_keys):\n", + " raise ValueError(\"Missing essential keys.\")\n", + "\n", + " if not data_keys.issubset(all_keys):\n", + " raise ValueError(\"Unexpected keys in batch.\")\n", + "\n", + " # Bring all tensors from the batch into a single tensor\n", + " upl_x = torch.empty((nbatch, *b0[\"ulv_vals\"].shape))\n", + " upl_y = torch.empty((nbatch, *b0[\"ulv_tars\"].shape))\n", + "\n", + " sur_x = torch.empty((nbatch, *b0[\"sur_vals\"].shape))\n", + " sur_y = torch.empty((nbatch, *b0[\"sur_tars\"].shape))\n", + "\n", + " sur_sta = torch.empty((nbatch, *b0[\"sur_static\"].shape))\n", + "\n", + " lead_time = torch.empty((nbatch,), dtype=torch.float32)\n", + " input_time = torch.empty((nbatch,), dtype=torch.float32)\n", + "\n", + " for i, rec in enumerate(batch):\n", + " sur_x[i] = rec[\"sur_vals\"]\n", + " sur_y[i] = rec[\"sur_tars\"]\n", + "\n", + " upl_x[i] = rec[\"ulv_vals\"]\n", + " upl_y[i] = rec[\"ulv_tars\"]\n", + "\n", + " sur_sta[i] = rec[\"sur_static\"]\n", + "\n", + " lead_time[i] = rec[\"lead_time\"]\n", + " input_time[i] = rec[\"input_time\"]\n", + "\n", + " return_value = {\n", + " \"lead_time\": lead_time,\n", + " \"input_time\": input_time,\n", + " }\n", + "\n", + " # Reshape (batch, parameter, level, time, lat, lon) ->\n", + " # (batch, time, parameter, level, lat, lon)\n", + " upl_x = upl_x.permute((0, 3, 1, 2, 4, 5))\n", + " upl_y = upl_y.permute((0, 3, 1, 2, 4, 5))\n", + " # Reshape (batch, parameter, time, lat, lon) ->\n", + " # (batch, time, parameter, lat, lon)\n", + " sur_x = sur_x.permute((0, 2, 1, 3, 4))\n", + " sur_y = sur_y.permute((0, 2, 1, 3, 4))\n", + "\n", + " # Pad\n", + " padding_2d = (*padding[\"lon\"], *padding[\"lat\"])\n", + "\n", + " def pad2d(x):\n", + " return torch.nn.functional.pad(x, padding_2d, mode=\"constant\", value=0)\n", + "\n", + " padding_3d = (*padding[\"lon\"], *padding[\"lat\"], *padding[\"level\"])\n", + "\n", + " def pad3d(x):\n", + " return torch.nn.functional.pad(x, padding_3d, mode=\"constant\", value=0)\n", + "\n", + " sur_x = pad2d(sur_x).contiguous()\n", + " upl_x = pad3d(upl_x).contiguous()\n", + " sur_y = pad2d(sur_y).contiguous()\n", + " upl_y = pad3d(upl_y).contiguous()\n", + " return_value[\"static\"] = pad2d(sur_sta).contiguous()\n", + "\n", + " # Remove time for targets\n", + " upl_y = torch.squeeze(upl_y, 1)\n", + " sur_y = torch.squeeze(sur_y, 1)\n", + "\n", + " # We stack along the combined parameter x level dimension\n", + " return_value[\"x\"] = torch.cat(\n", + " (sur_x, upl_x.view(*upl_x.shape[:2], -1, *upl_x.shape[4:])), dim=2\n", + " )\n", + " return_value[\"y\"] = torch.cat(\n", + " (sur_y, upl_y.view(upl_y.shape[0], -1, *upl_y.shape[3:])), dim=1\n", + " )\n", + "\n", + " if climate_keys.issubset(data_keys):\n", + " sur_climate = torch.empty((nbatch, *b0[\"sur_climate\"].shape))\n", + " ulv_climate = torch.empty((nbatch, *b0[\"ulv_climate\"].shape))\n", + " for i, rec in enumerate(batch):\n", + " sur_climate[i] = rec[\"sur_climate\"]\n", + " ulv_climate[i] = rec[\"ulv_climate\"]\n", + " sur_climate = pad2d(sur_climate)\n", + " ulv_climate = pad3d(ulv_climate)\n", + "\n", + " return_value[\"climate\"] = torch.cat(\n", + " (\n", + " sur_climate,\n", + " ulv_climate.view(nbatch, -1, *ulv_climate.shape[3:]),\n", + " ),\n", + " dim=1,\n", + " )\n", + "\n", + " return return_value\n", + "\n", + "\n", + "def input_scalers(\n", + " surf_vars: list[str],\n", + " vert_vars: list[str],\n", + " levels: list[float],\n", + " surf_path: str | Path,\n", + " vert_path: str | Path,\n", + ") -> tuple[Tensor, Tensor]:\n", + " \"\"\"Reads the input scalers\n", + "\n", + " Args:\n", + " surf_vars: surface variables to be used.\n", + " vert_vars: vertical variables to be used.\n", + " levels: MERRA2 levels to use.\n", + " surf_path: path to surface scalers file.\n", + " vert_path: path to vertical level scalers file.\n", + "\n", + " Returns:\n", + " mu (Tensor): mean values\n", + " var (Tensor): varience values\n", + " \"\"\"\n", + " with h5py.File(Path(surf_path), \"r\", libver=\"latest\") as surf_file:\n", + " stats = [x.decode().lower() for x in surf_file[\"statistic\"][()]]\n", + " mu_idx = stats.index(\"mu\")\n", + " sig_idx = stats.index(\"sigma\")\n", + "\n", + " s_mu = torch.tensor([surf_file[k][()][mu_idx] for k in surf_vars])\n", + " s_sig = torch.tensor([surf_file[k][()][sig_idx] for k in surf_vars])\n", + "\n", + " with h5py.File(Path(vert_path), \"r\", libver=\"latest\") as vert_file:\n", + " stats = [x.decode().lower() for x in vert_file[\"statistic\"][()]]\n", + " mu_idx = stats.index(\"mu\")\n", + " sig_idx = stats.index(\"sigma\")\n", + "\n", + " lvl = vert_file[\"lev\"][()]\n", + " l_idx = [np.where(lvl == v)[0].item() for v in levels]\n", + "\n", + " v_mu = np.array([vert_file[k][()][mu_idx, l_idx] for k in vert_vars])\n", + " v_sig = np.array([vert_file[k][()][sig_idx, l_idx] for k in vert_vars])\n", + "\n", + " v_mu = torch.from_numpy(v_mu).view(-1)\n", + " v_sig = torch.from_numpy(v_sig).view(-1)\n", + "\n", + " mu = torch.cat((s_mu, v_mu), dim=0).to(torch.float32)\n", + " sig = torch.cat((s_sig, v_sig), dim=0).to(torch.float32).clamp(1e-4, 1e4)\n", + " return mu, sig\n", + "\n", + "\n", + "def static_input_scalers(\n", + " scalar_path: str | Path, stat_vars: list[str], unscaled_params: int = 7\n", + ") -> tuple[Tensor, Tensor]:\n", + " scalar_path = Path(scalar_path)\n", + "\n", + " with h5py.File(scalar_path, \"r\", libver=\"latest\") as scaler_file:\n", + " stats = [x.decode().lower() for x in scaler_file[\"statistic\"][()]]\n", + " mu_idx = stats.index(\"mu\")\n", + " sig_idx = stats.index(\"sigma\")\n", + "\n", + " mu = torch.tensor([scaler_file[k][()][mu_idx] for k in stat_vars])\n", + " sig = torch.tensor([scaler_file[k][()][sig_idx] for k in stat_vars])\n", + "\n", + " z = torch.zeros(unscaled_params, dtype=mu.dtype, device=mu.device)\n", + " o = torch.ones(unscaled_params, dtype=sig.dtype, device=sig.device)\n", + " mu = torch.cat((z, mu), dim=0).to(torch.float32)\n", + " sig = torch.cat((o, sig), dim=0).to(torch.float32)\n", + "\n", + " return mu, sig.clamp(1e-4, 1e4)\n", + "\n", + "\n", + "def output_scalers(\n", + " surf_vars: list[str],\n", + " vert_vars: list[str],\n", + " levels: list[float],\n", + " surf_path: str | Path,\n", + " vert_path: str | Path,\n", + ") -> Tensor:\n", + " surf_path = Path(surf_path)\n", + " vert_path = Path(vert_path)\n", + "\n", + " with h5py.File(surf_path, \"r\", libver=\"latest\") as surf_file:\n", + " svars = torch.tensor([surf_file[k][()] for k in surf_vars])\n", + "\n", + " with h5py.File(vert_path, \"r\", libver=\"latest\") as vert_file:\n", + " lvl = vert_file[\"lev\"][()]\n", + " l_idx = [np.where(lvl == v)[0].item() for v in levels]\n", + " vvars = np.array([vert_file[k][()][l_idx] for k in vert_vars])\n", + " vvars = torch.from_numpy(vvars).view(-1)\n", + "\n", + " var = torch.cat((svars, vvars), dim=0).to(torch.float32).clamp(1e-7, 1e7)\n", + "\n", + " return var\n", + "\n", + "\n", + "class SampleSpec:\n", + " \"\"\"\n", + " A data class to collect the information used to define a sample.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " inputs: tuple[pd.Timestamp, pd.Timestamp],\n", + " lead_time: int,\n", + " target: pd.Timestamp | list[pd.Timestamp],\n", + " ):\n", + " \"\"\"\n", + " Args:\n", + " inputs: Tuple of timestamps. In ascending order.\n", + " lead_time: Lead time. In hours.\n", + " target: Timestamp of the target. Can be before or after the inputs.\n", + " \"\"\"\n", + " if not inputs[0] < inputs[1]:\n", + " raise ValueError(\n", + " \"Timestamps in `inputs` should be in strictly ascending order.\"\n", + " )\n", + "\n", + " self.inputs = inputs\n", + " self.input_time = (inputs[1] - inputs[0]).total_seconds() / 3600\n", + " self.lead_time = lead_time\n", + " self.target = target\n", + "\n", + " self.times = [*inputs, target]\n", + " self.stat_times = [inputs[-1]]\n", + "\n", + " @property\n", + " def climatology_info(self) -> tuple[int, int]:\n", + " \"\"\"Get the required climatology info.\n", + "\n", + " :return: information required to obtain climatology data. Essentially\n", + " this is the day of the year and hour of the day of the target\n", + " timestamp, with the former restricted to the interval [1, 365].\n", + " :rtype: tuple\n", + " \"\"\"\n", + " return (min(self.target.dayofyear, 365), self.target.hour)\n", + "\n", + " @property\n", + " def year(self) -> int:\n", + " return self.inputs[1].year\n", + "\n", + " @property\n", + " def dayofyear(self) -> int:\n", + " return self.inputs[1].dayofyear\n", + "\n", + " @property\n", + " def hourofday(self) -> int:\n", + " return self.inputs[1].hour\n", + "\n", + " def _info_str(self) -> str:\n", + " iso_8601 = \"%Y-%m-%dT%H:%M:%S\"\n", + "\n", + " return (\n", + " f\"Issue time: {self.inputs[1].strftime(iso_8601)}\\n\"\n", + " f\"Lead time: {self.lead_time} hours ahead\\n\"\n", + " f\"Input delta: {self.input_time} hours\\n\"\n", + " f\"Target time: {self.target.strftime(iso_8601)}\"\n", + " )\n", + "\n", + " @classmethod\n", + " def get(cls, timestamp: pd.Timestamp, dt: int, lead_time: int):\n", + " \"\"\"Given a timestamp and lead time, generates a SampleSpec object\n", + " describing the sample further.\n", + "\n", + " Args:\n", + " timestamp: Timstamp of the sample, Ie this is the larger of the two\n", + " input timstamps.\n", + " dt: Time between input samples, in hours.\n", + " lead_time: Lead time. In hours.\n", + "\n", + " Returns:\n", + " SampleSpec\n", + " \"\"\" # noqa: E501\n", + " assert dt > 0, \"dt should be possitive\"\n", + " lt = pd.to_timedelta(lead_time, unit=\"h\")\n", + " dt = pd.to_timedelta(dt, unit=\"h\")\n", + "\n", + " if lead_time >= 0:\n", + " timestamp_target = timestamp + lt\n", + " else:\n", + " timestamp_target = timestamp - dt + lt\n", + "\n", + " spec = cls(\n", + " inputs=(timestamp - dt, timestamp),\n", + " lead_time=lead_time,\n", + " target=timestamp_target,\n", + " )\n", + "\n", + " return spec\n", + "\n", + " def __repr__(self) -> str:\n", + " return self._info_str()\n", + "\n", + " def __str__(self) -> str:\n", + " return self._info_str()\n", + "\n", + "\n", + "class Merra2Dataset(Dataset):\n", + " \"\"\"MERRA2 dataset. The dataset unifies surface and vertical data as well as\n", + " optional climatology.\n", + "\n", + " Samples come in the form of a dictionary. Not all keys support all\n", + " variables, yet the general ordering of dimensions is\n", + " parameter, level, time, lat, lon\n", + "\n", + " Note:\n", + " Data is assumed to be in NetCDF files containing daily data at 3-hourly\n", + " intervals. These follow the naming patterns\n", + " MERRA2_sfc_YYYYMMHH.nc and MERRA_pres_YYYYMMHH.nc and can be located in\n", + " two different locations. Optional climatology data comes from files\n", + " climate_surface_doyDOY_hourHOD.nc and\n", + " climate_vertical_doyDOY_hourHOD.nc.\n", + "\n", + "\n", + " Note:\n", + " `_get_valid_timestamps` assembles a set of all timestamps for which\n", + " there is data (with hourly resolutions). The result is stored in\n", + " `_valid_timestamps`. `_get_valid_climate_timestamps` does the same with\n", + " climatology data and stores it in `_valid_climate_timestamps`.\n", + "\n", + " Based on this information, `samples` generates a list of valid samples,\n", + " stored in `samples`. Here the format is::\n", + "\n", + " [\n", + " [\n", + " (timestamp 1, lead time A),\n", + " (timestamp 1, lead time B),\n", + " (timestamp 1, lead time C),\n", + " ],\n", + " [\n", + " (timestamp 2, lead time D),\n", + " (timestamp 2, lead time E),\n", + " ]\n", + " ]\n", + "\n", + " That is, the outer list iterates over timestamps (init times), the\n", + " inner over lead times. Only valid entries are stored.\n", + " \"\"\"\n", + "\n", + " valid_vertical_vars = [\n", + " \"CLOUD\",\n", + " \"H\",\n", + " \"OMEGA\",\n", + " \"PL\",\n", + " \"QI\",\n", + " \"QL\",\n", + " \"QV\",\n", + " \"T\",\n", + " \"U\",\n", + " \"V\",\n", + " ]\n", + " valid_surface_vars = [\n", + " \"EFLUX\",\n", + " \"GWETROOT\",\n", + " \"HFLUX\",\n", + " \"LAI\",\n", + " \"LWGAB\",\n", + " \"LWGEM\",\n", + " \"LWTUP\",\n", + " \"PRECTOT\",\n", + " \"PS\",\n", + " \"QV2M\",\n", + " \"SLP\",\n", + " \"SWGNT\",\n", + " \"SWTNT\",\n", + " \"T2M\",\n", + " \"TQI\",\n", + " \"TQL\",\n", + " \"TQV\",\n", + " \"TS\",\n", + " \"U10M\",\n", + " \"V10M\",\n", + " \"Z0M\",\n", + " ]\n", + " valid_static_surface_vars = [\"FRACI\", \"FRLAND\", \"FROCEAN\", \"PHIS\"]\n", + "\n", + " valid_levels = [\n", + " 34.0,\n", + " 39.0,\n", + " 41.0,\n", + " 43.0,\n", + " 44.0,\n", + " 45.0,\n", + " 48.0,\n", + " 51.0,\n", + " 53.0,\n", + " 56.0,\n", + " 63.0,\n", + " 68.0,\n", + " 71.0,\n", + " 72.0,\n", + " ]\n", + "\n", + " timedelta_input = pd.to_timedelta(3, unit=\"h\")\n", + "\n", + " def __init__(\n", + " self,\n", + " time_range: tuple[str | pd.Timestamp, str | pd.Timestamp],\n", + " lead_times: list[int],\n", + " input_times: list[int],\n", + " data_path_surface: str | Path,\n", + " data_path_vertical: str | Path,\n", + " climatology_path_surface: str | Path | None = None,\n", + " climatology_path_vertical: str | Path | None = None,\n", + " surface_vars: list[str] | None = None,\n", + " static_surface_vars: list[str] | None = None,\n", + " vertical_vars: list[str] | None = None,\n", + " levels: list[float] | None = None,\n", + " roll_longitudes: int = 0,\n", + " positional_encoding: str = \"absolute\",\n", + " rtype: type = np.float32,\n", + " dtype: torch.dtype = torch.float32,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " data_path_surface: Location of surface data.\n", + " data_path_vertical: Location of vertical data.\n", + " climatology_path_surface: Location of (optional) surface\n", + " climatology.\n", + " climatology_path_vertical: Location of (optional) vertical\n", + " climatology.\n", + " surface_vars: Surface variables.\n", + " static_surface_vars: Static surface variables.\n", + " vertical_vars: Vertical variables.\n", + " levels: Levels.\n", + " time_range: Used to subset data.\n", + " lead_times: Lead times for generalized forecasting.\n", + " roll_longitudes: Set to non-zero value to data by random amount\n", + " along longitude dimension.\n", + " position_encoding: possible values are\n", + " ['absolute' (default), 'fourier'].\n", + " 'absolute' returns lat lon encoded in 3 dimensions using sine\n", + " and cosine\n", + " 'fourier' returns lat/lon to be encoded by model\n", + " returns lat/lon to be encoded by model\n", + " rtype: numpy data type used during read\n", + " dtype: torch data type of data output\n", + " \"\"\"\n", + "\n", + " self.time_range = (\n", + " pd.to_datetime(time_range[0]),\n", + " pd.to_datetime(time_range[1]),\n", + " )\n", + " self.lead_times = lead_times\n", + " self.input_times = input_times\n", + " self._roll_longitudes = list(range(roll_longitudes + 1))\n", + "\n", + " self._uvars = vertical_vars or self.valid_vertical_vars\n", + " self._level = levels or self.valid_levels\n", + " self._svars = surface_vars or self.valid_surface_vars\n", + " self._sstat = static_surface_vars or self.valid_static_surface_vars\n", + " self._nuvars = len(self._uvars)\n", + " self._nlevel = len(self._level)\n", + " self._nsvars = len(self._svars)\n", + " self._nsstat = len(self._sstat)\n", + "\n", + " self.rtype = rtype\n", + " self.dtype = dtype\n", + "\n", + " self.positional_encoding = positional_encoding\n", + "\n", + " self._data_path_surface = Path(data_path_surface)\n", + " self._data_path_vertical = Path(data_path_vertical)\n", + "\n", + " self.dir_exists(self._data_path_surface)\n", + " self.dir_exists(self._data_path_vertical)\n", + "\n", + " self._get_coordinates()\n", + "\n", + " self._climatology_path_surface = Path(climatology_path_surface) or None\n", + " self._climatology_path_vertical = (\n", + " Path(climatology_path_vertical) or None\n", + " )\n", + " self._require_clim = (\n", + " self._climatology_path_surface is not None\n", + " and self._climatology_path_vertical is not None\n", + " )\n", + "\n", + " if self._require_clim:\n", + " self.dir_exists(self._climatology_path_surface)\n", + " self.dir_exists(self._climatology_path_vertical)\n", + " elif (\n", + " climatology_path_surface is None\n", + " and climatology_path_vertical is None\n", + " ):\n", + " self._climatology_path_surface = None\n", + " self._climatology_path_vertical = None\n", + " else:\n", + " raise ValueError(\n", + " \"Either both or neither of\"\n", + " \"`climatology_path_surface` and\"\n", + " \"`climatology_path_vertical` should be None.\"\n", + " )\n", + "\n", + " if not set(self._svars).issubset(set(self.valid_surface_vars)):\n", + " raise ValueError(\"Invalid surface variable.\")\n", + "\n", + " if not set(self._sstat).issubset(set(self.valid_static_surface_vars)):\n", + " raise ValueError(\"Invalid static surface variable.\")\n", + "\n", + " if not set(self._uvars).issubset(set(self.valid_vertical_vars)):\n", + " raise ValueError(\"Inalid vertical variable.\")\n", + "\n", + " if not set(self._level).issubset(set(self.valid_levels)):\n", + " raise ValueError(\"Invalid level.\")\n", + "\n", + " @staticmethod\n", + " def dir_exists(path: Path) -> None:\n", + " if not path.is_dir():\n", + " raise ValueError(f\"Directory {path} does not exist.\")\n", + "\n", + " @property\n", + " def upper_shape(self) -> tuple:\n", + " \"\"\"Returns the vertical variables shape\n", + " Returns:\n", + " tuple: vertical variable shape in the following order::\n", + "\n", + " [VAR, LEV, TIME, LAT, LON]\n", + " \"\"\"\n", + " return self._nuvars, self._nlevel, 2, 361, 576\n", + "\n", + " @property\n", + " def surface_shape(self) -> tuple:\n", + " \"\"\"Returns the surface variables shape\n", + "\n", + " Returns:\n", + " tuple: surafce shape in the following order::\n", + "\n", + " [VAR, LEV, TIME, LAT, LON]\n", + " \"\"\"\n", + " return self._nsvars, 2, 361, 576\n", + "\n", + " def data_file_surface(self, timestamp: pd.Timestamp) -> Path:\n", + " \"\"\"Build the surfcae data file name based on timestamp\n", + "\n", + " Args:\n", + " timestamp: a timestamp\n", + "\n", + " Returns:\n", + " Path: constructed path\n", + " \"\"\"\n", + " pattern = \"MERRA2_sfc_%Y%m%d.nc\"\n", + " data_file = self._data_path_surface / timestamp.strftime(pattern)\n", + " return data_file\n", + "\n", + " def data_file_vertical(self, timestamp: pd.Timestamp) -> Path:\n", + " \"\"\"Build the vertical data file name based on timestamp\n", + "\n", + " Args:\n", + " timestamp: a timestamp\n", + "\n", + " Returns:\n", + " Path: constructed path\n", + " \"\"\"\n", + " pattern = \"MERRA_pres_%Y%m%d.nc\"\n", + " data_file = self._data_path_vertical / timestamp.strftime(pattern)\n", + " return data_file\n", + "\n", + " def data_file_surface_climate(\n", + " self,\n", + " timestamp: pd.Timestamp | None = None,\n", + " dayofyear: int | None = None,\n", + " hourofday: int | None = None,\n", + " ) -> Path:\n", + " \"\"\"\n", + " Returns the path to a climatology file based either on a timestamp or\n", + " the dayofyear / hourofday combination.\n", + " Args:\n", + " timestamp: A timestamp.\n", + " dayofyear: Day of the year. 1 to 366.\n", + " hourofday: Hour of the day. 0 to 23.\n", + " Returns:\n", + " Path: Path to climatology file.\n", + " \"\"\"\n", + " if timestamp is not None and (\n", + " (dayofyear is not None) or (hourofday is not None)\n", + " ):\n", + " raise ValueError(\n", + " \"Provide either timestamp or both dayofyear and hourofday.\"\n", + " )\n", + "\n", + " if timestamp is not None:\n", + " dayofyear = min(timestamp.dayofyear, 365)\n", + " hourofday = timestamp.hour\n", + "\n", + " file_name = f\"climate_surface_doy{dayofyear:03}_hour{hourofday:02}.nc\"\n", + " data_file = self._climatology_path_surface / file_name\n", + " return data_file\n", + "\n", + " def data_file_vertical_climate(\n", + " self,\n", + " timestamp: pd.Timestamp | None = None,\n", + " dayofyear: int | None = None,\n", + " hourofday: int | None = None,\n", + " ) -> Path:\n", + " \"\"\"Returns the path to a climatology file based either on a timestamp\n", + " or the dayofyear / hourofday combination.\n", + "\n", + " Args:\n", + " timestamp: A timestamp. dayofyear: Day of the year. 1 to 366.\n", + " hourofday: Hour of the day. 0 to 23.\n", + " Returns:\n", + " Path: Path to climatology file.\n", + " \"\"\"\n", + " if timestamp is not None and (\n", + " (dayofyear is not None) or (hourofday is not None)\n", + " ):\n", + " raise ValueError(\n", + " \"Provide either timestamp or both dayofyear and hourofday.\"\n", + " )\n", + "\n", + " if timestamp is not None:\n", + " dayofyear = min(timestamp.dayofyear, 365)\n", + " hourofday = timestamp.hour\n", + "\n", + " file_name = f\"climate_vertical_doy{dayofyear:03}_hour{hourofday:02}.nc\"\n", + " data_file = self._climatology_path_vertical / file_name\n", + " return data_file\n", + "\n", + " def _get_coordinates(self) -> None:\n", + " \"\"\"\n", + " Obtains the coordiantes (latitudes and longitudes) from a single data\n", + " file.\n", + " \"\"\"\n", + " timestamp = next(iter(self.valid_timestamps))\n", + "\n", + " file = self.data_file_surface(timestamp)\n", + " with h5py.File(file, \"r\", libver=\"latest\") as handle:\n", + " self.lats = lats = handle[\"lat\"][()].astype(self.rtype)\n", + " self.lons = lons = handle[\"lon\"][()].astype(self.rtype)\n", + "\n", + " deg_to_rad = np.pi / 180\n", + " self._embed_lat = np.sin(lats * deg_to_rad).reshape(-1, 1)\n", + "\n", + " self._embed_lon = np.empty((2, 1, len(lons)), dtype=self.rtype)\n", + " self._embed_lon[0, 0] = np.cos(lons * deg_to_rad)\n", + " self._embed_lon[1, 0] = np.sin(lons * deg_to_rad)\n", + "\n", + " @ft.cached_property\n", + " def lats(self) -> np.ndarray:\n", + " timestamp = next(iter(self.valid_timestamps))\n", + "\n", + " file = self.data_file_surface(timestamp)\n", + " with h5py.File(file, \"r\", libver=\"latest\") as handle:\n", + " return handle[\"lat\"][()].astype(self.rtype)\n", + "\n", + " @ft.cached_property\n", + " def lons(self) -> np.ndarray:\n", + " timestamp = next(iter(self.valid_timestamps))\n", + "\n", + " file = self.data_file_surface(timestamp)\n", + " with h5py.File(file, \"r\", libver=\"latest\") as handle:\n", + " return handle[\"lon\"][()].astype(self.rtype)\n", + "\n", + " @ft.cached_property\n", + " def position_signal(self) -> np.ndarray:\n", + " \"\"\"Generates the \"position signal\" that is part of the static\n", + " features.\n", + "\n", + " Returns:\n", + " Tensor: Torch tensor of dimension (parameter, lat, lon) containing\n", + " sin(lat), cos(lon), sin(lon).\n", + " \"\"\"\n", + "\n", + " latitudes, longitudes = np.meshgrid(\n", + " self.lats, self.lons, indexing=\"ij\"\n", + " )\n", + "\n", + " if self.positional_encoding == \"absolute\":\n", + " latitudes = latitudes / 360 * 2.0 * np.pi\n", + " longitudes = longitudes / 360 * 2.0 * np.pi\n", + " sur_static = np.stack(\n", + " [np.sin(latitudes), np.cos(longitudes), np.sin(longitudes)],\n", + " axis=0,\n", + " )\n", + " else:\n", + " sur_static = np.stack([latitudes, longitudes], axis=0)\n", + "\n", + " sur_static = sur_static.astype(self.rtype)\n", + "\n", + " return sur_static\n", + "\n", + " @ft.cached_property\n", + " def valid_timestamps(self) -> set[pd.Timestamp]:\n", + " \"\"\"Generates list of valid timestamps based on available files. Only\n", + " timestamps for which both surface and vertical information is available\n", + " are considered valid.\n", + " Returns:\n", + " list: list of timestamps\n", + " \"\"\"\n", + "\n", + " s_glob = self._data_path_surface.glob(\"MERRA2_sfc_????????.nc\")\n", + " s_files = [os.path.basename(f) for f in s_glob]\n", + " v_glob = self._data_path_surface.glob(\"MERRA_pres_????????.nc\")\n", + " v_files = [os.path.basename(f) for f in v_glob]\n", + "\n", + " s_re = re.compile(r\"MERRA2_sfc_(\\d{8}).nc\\Z\")\n", + " v_re = re.compile(r\"MERRA_pres_(\\d{8}).nc\\Z\")\n", + " fmt = \"%Y%m%d\"\n", + "\n", + " s_times = {\n", + " (datetime.strptime(m[1], fmt))\n", + " for f in s_files\n", + " if (m := s_re.match(f))\n", + " }\n", + " v_times = {\n", + " (datetime.strptime(m[1], fmt))\n", + " for f in v_files\n", + " if (m := v_re.match(f))\n", + " }\n", + "\n", + " times = s_times.intersection(v_times)\n", + "\n", + " # Each file contains a day at 3 hour intervals\n", + " times = {\n", + " t + timedelta(hours=i) for i in range(0, 24, 3) for t in times\n", + " }\n", + "\n", + " start_time, end_time = self.time_range\n", + " times = {pd.Timestamp(t) for t in times if start_time <= t <= end_time}\n", + "\n", + " return times\n", + "\n", + " @ft.cached_property\n", + " def valid_climate_timestamps(self) -> set[tuple[int, int]]:\n", + " \"\"\"Generates list of \"timestamps\" (dayofyear, hourofday) for which\n", + " climatology data is present. Only instances for which surface and\n", + " vertical data is available are considered valid.\n", + " Returns:\n", + " list: List of tuples describing valid climatology instances.\n", + " \"\"\"\n", + " if not self._require_clim:\n", + " return set()\n", + "\n", + " s_glob = self._climatology_path_surface.glob(\n", + " \"climate_surface_doy???_hour??.nc\"\n", + " )\n", + " s_files = [os.path.basename(f) for f in s_glob]\n", + "\n", + " v_glob = self._climatology_path_vertical.glob(\n", + " \"climate_vertical_doy???_hour??.nc\"\n", + " )\n", + " v_files = [os.path.basename(f) for f in v_glob]\n", + "\n", + " s_re = re.compile(r\"climate_surface_doy(\\d{3})_hour(\\d{2}).nc\\Z\")\n", + " v_re = re.compile(r\"climate_vertical_doy(\\d{3})_hour(\\d{2}).nc\\Z\")\n", + "\n", + " s_times = {\n", + " (int(m[1]), int(m[2])) for f in s_files if (m := s_re.match(f))\n", + " }\n", + " v_times = {\n", + " (int(m[1]), int(m[2])) for f in v_files if (m := v_re.match(f))\n", + " }\n", + "\n", + " times = s_times.intersection(v_times)\n", + "\n", + " return times\n", + "\n", + " def _data_available(self, spec: SampleSpec) -> bool:\n", + " \"\"\"\n", + " Checks whether data is available for a given SampleSpec object. Does so\n", + " using the internal sets with available data previously constructed. Not\n", + " by checking the file system.\n", + " Args:\n", + " spec: SampleSpec object as returned by SampleSpec.get\n", + " Returns:\n", + " bool: if data is availability.\n", + " \"\"\"\n", + " valid = set(spec.times).issubset(self.valid_timestamps)\n", + "\n", + " if self._require_clim:\n", + " sci = spec.climatology_info\n", + " ci = set(sci) if isinstance(sci, list) else set([sci]) # noqa: C405\n", + " valid &= ci.issubset(self.valid_climate_timestamps)\n", + "\n", + " return valid\n", + "\n", + " @ft.cached_property\n", + " def samples(self) -> list[tuple[pd.Timestamp, int, int]]:\n", + " \"\"\"\n", + " Generates list of all valid samlpes.\n", + " Returns:\n", + " list: List of tuples (timestamp, input time, lead time).\n", + " \"\"\"\n", + " valid_samples = []\n", + " dts = [(it, lt) for it in self.input_times for lt in self.lead_times]\n", + "\n", + " for timestamp in sorted(self.valid_timestamps):\n", + " timestamp_samples = []\n", + " for it, lt in dts:\n", + " spec = SampleSpec.get(timestamp, -it, lt)\n", + "\n", + " if self._data_available(spec):\n", + " timestamp_samples.append((timestamp, it, lt))\n", + "\n", + " if timestamp_samples:\n", + " valid_samples.append(timestamp_samples)\n", + "\n", + " return valid_samples\n", + "\n", + " def _to_torch(\n", + " self,\n", + " data: dict[str, Tensor | list[Tensor]],\n", + " dtype: torch.dtype = torch.float32,\n", + " ) -> dict[str, Tensor | list[Tensor]]:\n", + " out = {}\n", + " for k, v in data.items():\n", + " if isinstance(v, list):\n", + " out[k] = [torch.from_numpy(x).to(dtype) for x in v]\n", + " else:\n", + " out[k] = torch.from_numpy(v).to(dtype)\n", + "\n", + " return out\n", + "\n", + " def _lat_roll(\n", + " self, data: dict[str, Tensor | list[Tensor]], n: int\n", + " ) -> dict[str, Tensor | list[Tensor]]:\n", + " out = {}\n", + " for k, v in data.items():\n", + " if isinstance(v, list):\n", + " out[k] = [torch.roll(x, shifts=n, dims=-1) for x in v]\n", + " else:\n", + " out[k] = torch.roll(v, shifts=n, dims=-1)\n", + "\n", + " return out\n", + "\n", + " def _read_static_data(\n", + " self, file: str | Path, doy: int, hod: int\n", + " ) -> np.ndarray:\n", + " with h5py.File(file, \"r\", libver=\"latest\") as handle:\n", + " lats_surf = handle[\"lat\"]\n", + " lons_surf = handle[\"lon\"]\n", + "\n", + " nll = (len(lats_surf), len(lons_surf))\n", + "\n", + " npos = len(self.position_signal)\n", + " ntime = 4\n", + "\n", + " nstat = npos + ntime + self._nsstat\n", + " data = np.empty((nstat, *nll), dtype=self.rtype)\n", + "\n", + " for i, key in enumerate(self._sstat, start=npos + ntime):\n", + " data[i] = handle[key][()].astype(dtype=self.rtype)\n", + "\n", + " # [possition signal], cos(doy), sin(doy), cos(hod), sin(hod)\n", + " data[0:npos] = self.position_signal\n", + " data[npos + 0] = np.cos(2 * np.pi * doy / 366)\n", + " data[npos + 1] = np.sin(2 * np.pi * doy / 366)\n", + " data[npos + 2] = np.cos(2 * np.pi * hod / 24)\n", + " data[npos + 3] = np.sin(2 * np.pi * hod / 24)\n", + "\n", + " return data\n", + "\n", + " def _read_surface(\n", + " self, tidx: int, nll: tuple[int, int], handle: h5py.File\n", + " ) -> np.ndarray:\n", + " data = np.empty((self._nsvars, *nll), dtype=self.rtype)\n", + "\n", + " for i, key in enumerate(self._svars):\n", + " data[i] = handle[key][tidx][()].astype(dtype=self.rtype)\n", + "\n", + " return data\n", + "\n", + " def _read_levels(\n", + " self, tidx: int, nll: tuple[int, int], handle: h5py.File\n", + " ) -> np.ndarray:\n", + " lvls = handle[\"lev\"][()]\n", + " lidx = self._level_idxs(lvls)\n", + "\n", + " data = np.empty((self._nuvars, self._nlevel, *nll), dtype=self.rtype)\n", + "\n", + " for i, key in enumerate(self._uvars):\n", + " data[i] = handle[key][tidx, lidx][()].astype(dtype=self.rtype)\n", + "\n", + " return np.ascontiguousarray(np.flip(data, axis=1))\n", + "\n", + " def _level_idxs(self, lvls):\n", + " lidx = [np.argwhere(lvls == int(lvl)).item() for lvl in self._level]\n", + " return sorted(lidx)\n", + "\n", + " @staticmethod\n", + " def _date_to_tidx(date: datetime | pd.Timestamp, handle: h5py.File) -> int:\n", + " if isinstance(date, pd.Timestamp):\n", + " date = date.to_pydatetime()\n", + "\n", + " time = handle[\"time\"]\n", + "\n", + " t0 = time.attrs[\"begin_time\"][()].item()\n", + " d0 = f\"{time.attrs['begin_date'][()].item()}\"\n", + "\n", + " offset = datetime.strptime(d0, \"%Y%m%d\")\n", + "\n", + " times = [offset + timedelta(minutes=int(t + t0)) for t in time[()]]\n", + " return times.index(date)\n", + "\n", + " def _read_data(\n", + " self, file_pair: tuple[str, str], date: datetime\n", + " ) -> dict[str, np.ndarray]:\n", + " s_file, v_file = file_pair\n", + "\n", + " with h5py.File(s_file, \"r\", libver=\"latest\") as shandle:\n", + " lats_surf = shandle[\"lat\"]\n", + " lons_surf = shandle[\"lon\"]\n", + "\n", + " nll = (len(lats_surf), len(lons_surf))\n", + "\n", + " tidx = self._date_to_tidx(date, shandle)\n", + "\n", + " sdata = self._read_surface(tidx, nll, shandle)\n", + "\n", + " with h5py.File(v_file, \"r\", libver=\"latest\") as vhandle:\n", + " lats_vert = vhandle[\"lat\"]\n", + " lons_vert = vhandle[\"lon\"]\n", + "\n", + " nll = (len(lats_vert), len(lons_vert))\n", + "\n", + " tidx = self._date_to_tidx(date, vhandle)\n", + "\n", + " vdata = self._read_levels(tidx, nll, vhandle)\n", + "\n", + " data = {\"vert\": vdata, \"surf\": sdata}\n", + "\n", + " return data\n", + "\n", + " def _read_climate(\n", + " self, file_pair: tuple[str, str]\n", + " ) -> dict[str, np.ndarray]:\n", + " s_file, v_file = file_pair\n", + "\n", + " with h5py.File(s_file, \"r\", libver=\"latest\") as shandle:\n", + " lats_surf = shandle[\"lat\"]\n", + " lons_surf = shandle[\"lon\"]\n", + "\n", + " nll = (len(lats_surf), len(lons_surf))\n", + "\n", + " sdata = np.empty((self._nsvars, *nll), dtype=self.rtype)\n", + "\n", + " for i, key in enumerate(self._svars):\n", + " sdata[i] = shandle[key][()].astype(dtype=self.rtype)\n", + "\n", + " with h5py.File(v_file, \"r\", libver=\"latest\") as vhandle:\n", + " lats_vert = vhandle[\"lat\"]\n", + " lons_vert = vhandle[\"lon\"]\n", + "\n", + " nll = (len(lats_vert), len(lons_vert))\n", + "\n", + " lvls = vhandle[\"lev\"][()]\n", + " lidx = self._level_idxs(lvls)\n", + "\n", + " vdata = np.empty(\n", + " (self._nuvars, self._nlevel, *nll), dtype=self.rtype\n", + " )\n", + "\n", + " for i, key in enumerate(self._uvars):\n", + " vdata[i] = vhandle[key][lidx][()].astype(dtype=self.rtype)\n", + "\n", + " data = {\n", + " \"vert\": np.ascontiguousarray(np.flip(vdata, axis=1)),\n", + " \"surf\": sdata,\n", + " }\n", + "\n", + " return data\n", + "\n", + " def get_data_from_sample_spec(\n", + " self, spec: SampleSpec\n", + " ) -> dict[str, Tensor | int | float]:\n", + " \"\"\"Loads and assembles sample data given a SampleSpec object.\n", + "\n", + " Args:\n", + " spec (SampleSpec): Full details regarding the data to be loaded\n", + " Returns:\n", + " dict: Dictionary with the following keys::\n", + "\n", + " 'sur_static': Torch tensor of shape [parameter, lat, lon]. For\n", + " each pixel (lat, lon), the first 7 dimensions index sin(lat),\n", + " cos(lon), sin(lon), cos(doy), sin(doy), cos(hod), sin(hod).\n", + " Where doy is the day of the year [1, 366] and hod the hour of\n", + " the day [0, 23].\n", + " 'sur_vals': Torch tensor of shape [parameter, time, lat, lon].\n", + " 'sur_tars': Torch tensor of shape [parameter, time, lat, lon].\n", + " 'ulv_vals': Torch tensor of shape [parameter, level, time, lat, lon].\n", + " 'ulv_tars': Torch tensor of shape [parameter, level, time, lat, lon].\n", + " 'sur_climate': Torch tensor of shape [parameter, lat, lon].\n", + " 'ulv_climate': Torch tensor of shape [paramter, level, lat, lon].\n", + " 'lead_time': Float.\n", + " 'input_time': Float.\n", + "\n", + " \"\"\" # noqa: E501\n", + "\n", + " # We assemble the unique timestamps for which we need data.\n", + " vals_required = {*spec.times}\n", + " stat_required = {*spec.stat_times}\n", + "\n", + " # We assemble the unique data files from which we need value data\n", + " vals_file_map = defaultdict(list)\n", + " for t in vals_required:\n", + " data_files = (\n", + " self.data_file_surface(t),\n", + " self.data_file_vertical(t),\n", + " )\n", + " vals_file_map[data_files].append(t)\n", + "\n", + " # We assemble the unique data files from which we need static data\n", + " stat_file_map = defaultdict(list)\n", + " for t in stat_required:\n", + " data_files = (\n", + " self.data_file_surface(t),\n", + " self.data_file_vertical(t),\n", + " )\n", + " stat_file_map[data_files].append(t)\n", + "\n", + " # Load the value data\n", + " data = {}\n", + " for data_files, times in vals_file_map.items():\n", + " for time in times:\n", + " data[time] = self._read_data(data_files, time)\n", + "\n", + " # Combine times\n", + " sample_data = {}\n", + "\n", + " input_upl = np.stack([data[t][\"vert\"] for t in spec.inputs], axis=2)\n", + " sample_data[\"ulv_vals\"] = input_upl\n", + "\n", + " target_upl = data[spec.target][\"vert\"]\n", + " sample_data[\"ulv_tars\"] = target_upl[:, :, None]\n", + "\n", + " input_sur = np.stack([data[t][\"surf\"] for t in spec.inputs], axis=1)\n", + " sample_data[\"sur_vals\"] = input_sur\n", + "\n", + " target_sur = data[spec.target][\"surf\"]\n", + " sample_data[\"sur_tars\"] = target_sur[:, None]\n", + "\n", + " # Load the static data\n", + " data_files, times = stat_file_map.popitem()\n", + " time = times[0].dayofyear, times[0].hour\n", + " sample_data[\"sur_static\"] = self._read_static_data(\n", + " data_files[0], *time\n", + " )\n", + "\n", + " # If required load the surface data\n", + " if self._require_clim:\n", + " ci_year, ci_hour = spec.climatology_info\n", + "\n", + " surf_file = self.data_file_surface_climate(\n", + " dayofyear=ci_year,\n", + " hourofday=ci_hour,\n", + " )\n", + "\n", + " vert_file = self.data_file_vertical_climate(\n", + " dayofyear=ci_year,\n", + " hourofday=ci_hour,\n", + " )\n", + "\n", + " clim_data = self._read_climate((surf_file, vert_file))\n", + "\n", + " sample_data[\"sur_climate\"] = clim_data[\"surf\"]\n", + " sample_data[\"ulv_climate\"] = clim_data[\"vert\"]\n", + "\n", + " # Move the data from numpy to torch\n", + " sample_data = self._to_torch(sample_data, dtype=self.dtype)\n", + "\n", + " # Optionally roll\n", + " if len(self._roll_longitudes) > 0:\n", + " roll_by = random.choice(self._roll_longitudes)\n", + " sample_data = self._lat_roll(sample_data, roll_by)\n", + "\n", + " # Now that we have rolled, we can add the static data\n", + " sample_data[\"lead_time\"] = spec.lead_time\n", + " sample_data[\"input_time\"] = spec.input_time\n", + "\n", + " return sample_data\n", + "\n", + " def get_data(\n", + " self, timestamp: pd.Timestamp, input_time: int, lead_time: int\n", + " ) -> dict[str, Tensor | int]:\n", + " \"\"\"\n", + " Loads data based on timestamp and lead time.\n", + " Args:\n", + " timestamp: Timestamp.\n", + " input_time: time between input samples.\n", + " lead_time: lead time.\n", + " Returns:\n", + " Dictionary with keys 'sur_static', 'sur_vals', 'sur_tars',\n", + " 'ulv_vals', 'ulv_tars', 'sur_climate', 'ulv_climate',\n", + " 'lead_time'.\n", + " \"\"\"\n", + " spec = SampleSpec.get(timestamp, -input_time, lead_time)\n", + " sample_data = self.get_data_from_sample_spec(spec)\n", + " return sample_data\n", + "\n", + " def __getitem__(self, idx: int) -> dict[str, Tensor | int]:\n", + " \"\"\"\n", + " Loads data based on sample index and random choice of sample.\n", + " Args:\n", + " idx: Sample index.\n", + " Returns:\n", + " Dictionary with keys 'sur_static', 'sur_vals', 'sur_tars',\n", + " 'ulv_vals', 'ulv_tars', 'sur_climate', 'ulv_climate',\n", + " 'lead_time', 'input_time'.\n", + " \"\"\"\n", + " sample_set = self.samples[idx]\n", + " timestamp, input_time, lead_time, *nsteps = random.choice(sample_set)\n", + " sample_data = self.get_data(timestamp, input_time, lead_time)\n", + " return sample_data\n", + "\n", + " def __len__(self):\n", + " return len(self.samples)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "# from PrithviWxC.dataloaders.merra2 import Merra2Dataset\n", + "\n", + "dataset = Merra2Dataset(\n", + " time_range=time_range,\n", + " lead_times=lead_times,\n", + " input_times=input_times,\n", + " data_path_surface=surf_dir,\n", + " data_path_vertical=vert_dir,\n", + " climatology_path_surface=surf_clim_dir,\n", + " climatology_path_vertical=vert_clim_dir,\n", + " surface_vars=surface_vars,\n", + " static_surface_vars=static_surface_vars,\n", + " vertical_vars=vertical_vars,\n", + " levels=levels,\n", + " positional_encoding=positional_encoding,\n", + ")\n", + "assert len(dataset) > 0, \"There doesn't seem to be any valid data.\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The model\n", + "We are now ready to build the mdoel.\n", + "### Scalers\n", + "Additionally, the model takes as static parameters the mean\n", + "and variance values of the input variables and the variance\n", + "values of the target difference, i.e., the variance between\n", + "climatology and instantaneous variables. We have provided\n", + "data files containing these values, and here we load this data." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# from PrithviWxC.dataloaders.merra2 import (\n", + "# input_scalers,\n", + "# output_scalers,\n", + "# static_input_scalers,\n", + "# )\n", + "\n", + "surf_in_scal_path = Path(\"./climatology/musigma_surface.nc\")\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=f\"climatology/{surf_in_scal_path.name}\",\n", + " local_dir=\".\",\n", + ")\n", + "\n", + "vert_in_scal_path = Path(\"./climatology/musigma_vertical.nc\")\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=f\"climatology/{vert_in_scal_path.name}\",\n", + " local_dir=\".\",\n", + ")\n", + "\n", + "surf_out_scal_path = Path(\"./climatology/anomaly_variance_surface.nc\")\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=f\"climatology/{surf_out_scal_path.name}\",\n", + " local_dir=\".\",\n", + ")\n", + "\n", + "vert_out_scal_path = Path(\"./climatology/anomaly_variance_vertical.nc\")\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=f\"climatology/{vert_out_scal_path.name}\",\n", + " local_dir=\".\",\n", + ")\n", + "\n", + "in_mu, in_sig = input_scalers(\n", + " surface_vars,\n", + " vertical_vars,\n", + " levels,\n", + " surf_in_scal_path,\n", + " vert_in_scal_path,\n", + ")\n", + "\n", + "output_sig = output_scalers(\n", + " surface_vars,\n", + " vertical_vars,\n", + " levels,\n", + " surf_out_scal_path,\n", + " vert_out_scal_path,\n", + ")\n", + "\n", + "static_mu, static_sig = static_input_scalers(\n", + " surf_in_scal_path,\n", + " static_surface_vars,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Task and additional configs\n", + "As previously mentioned, the PrithviWxC model's pretext task\n", + "involved predicting the desired variable at a specific lead\n", + "time. This was achieved by calculating the difference (delta)\n", + "compared to the climatological average at that time. This\n", + "operational mode is activated using the residual flag. Although\n", + "the model includes additional residual options, the core model\n", + "weights were not trained using these modes.\n", + "\n", + "Additionally, for training and evaluation, it is possible to\n", + "mask tokens in the model. The masking occurs after tokenization,\n", + "prior to the encoder layers. The model utilizes multi-axis\n", + "attention, with data broken down into a hierarchy of local and\n", + "global patches. Consequently, masking can be configured to mask\n", + "either small local patches or larger global patches. This\n", + "configuration is achieved via the `masking_mode` flag. It is\n", + "possible to set `masking_mode=both`. This does not mix the modes\n", + "but rather allows both modes to be used and swapped between,\n", + "primarily for training purposes. For this demonstration, we will\n", + "adjust the masking ratio to showcase the reconstruction\n", + "capabilities of the model.\n", + "\n", + "Finally, we can set up shifting. Primarily utilized in the\n", + "decoder, this enables alternate shifting of the attention\n", + "windows, similar to the SWIN model. This option necessitates\n", + "an even number of decoder blocks and is incompatible with the\n", + "encoder when masking is also employed." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "residual = \"climate\"\n", + "masking_mode = \"local\"\n", + "decoder_shifting = True\n", + "masking_ratio = 0.99" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Model init\n", + "We now have all the pieces to build the model. If you are\n", + "using the pretrained weights, a number of the model\n", + "hyperparameters are predetermined and included below. With\n", + "this configuration, the model will have approximately 2.3\n", + "billion parameters. Therefore, if you want to train the fully\n", + "unfrozen model, you will likely need to use a model distribution\n", + "approach, such as fully shared data parallelism (FSDP). To\n", + "further reduce the memory usage of the model when gradients are\n", + "required, there are two variables — `checkpoint_encoder` and\n", + "`checkpoint_decoder` — which enable activation checkpointing of\n", + "desired transformer layers." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "from functools import cached_property\n", + "from importlib.metadata import version\n", + "\n", + "from torch import Tensor\n", + "from torch.utils.checkpoint import checkpoint\n", + "\n", + "if version(\"torch\") > \"2.3.0\":\n", + " from torch.nn.attention import SDPBackend, sdpa_kernel\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "\n", + "\n", + "# DropPath code is straight from timm\n", + "# (https://huggingface.co/spaces/Roll20/pet_score/blame/main/lib/timm/models/layers/drop.py)\n", + "def drop_path(\n", + " x: Tensor,\n", + " drop_prob: float = 0.0,\n", + " training: bool = False,\n", + " scale_by_keep: bool = True,\n", + ") -> Tensor:\n", + " \"\"\"Drop paths (Stochastic Depth) per sample (when applied in main path of\n", + " residual blocks). Taken form timm.\n", + "\n", + " Args:\n", + " x (Tensor): Input tensor.\n", + " drop_prob (float): Probability of dropping `x`, defaults to 0.\n", + " training (bool): Whether model is in in traingin of eval mode,\n", + " defaults to False.\n", + " scale_by_keep (bool): Whether the output should scaled by\n", + " (`1 - drop_prob`), defaults to True.\n", + " Returns:\n", + " Tensor: Tensor that may have randomly dropped with proability\n", + " `drop_path`\n", + " \"\"\"\n", + " if drop_prob == 0.0 or not training:\n", + " return x\n", + " keep_prob = 1 - drop_prob\n", + " shape = (x.shape[0],) + (1,) * (x.ndim - 1)\n", + " random_tensor = x.new_empty(shape).bernoulli_(keep_prob)\n", + " if keep_prob > 0.0 and scale_by_keep:\n", + " random_tensor.div_(keep_prob)\n", + " return x * random_tensor\n", + "\n", + "\n", + "class DropPath(nn.Module):\n", + " \"\"\"\n", + " Drop paths (Stochastic Depth) per sample (when applied in main path of\n", + " residual blocks).\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self, drop_prob: float | None = None, scale_by_keep: bool = True\n", + " ) -> None:\n", + " super(DropPath, self).__init__()\n", + " self.drop_prob = drop_prob\n", + " self.scale_by_keep = scale_by_keep\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"Runs drop path on input tensor\n", + "\n", + " Args:\n", + " x: input\n", + "\n", + " Returns:\n", + " tensor: output after drop_path\n", + " \"\"\"\n", + " return drop_path(x, self.drop_prob, self.training, self.scale_by_keep)\n", + "\n", + "\n", + "class Mlp(nn.Module):\n", + " \"\"\"\n", + " Multi layer perceptron.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self, features: int, hidden_features: int, dropout: float = 0.0\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " features: Input/output dimension.\n", + " hidden_features: Hidden dimension.\n", + " dropout: Dropout.\n", + " \"\"\"\n", + " super().__init__()\n", + " self.net = nn.Sequential(\n", + " nn.Linear(features, hidden_features),\n", + " nn.GELU(),\n", + " nn.Dropout(dropout),\n", + " nn.Linear(hidden_features, features),\n", + " nn.Dropout(dropout),\n", + " )\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Args:\n", + " x (Tesnor): Tensor of shape [..., channel]\n", + " Returns:\n", + " Tenosr: Tensor of same shape as x.\n", + " \"\"\"\n", + " return self.net(x)\n", + "\n", + "\n", + "class LayerNormPassThrough(nn.LayerNorm):\n", + " \"\"\"Normalising layer that allows the attention mask to be passed through\"\"\"\n", + "\n", + " def __init__(self, *args, **kwargs):\n", + " super().__init__(*args, **kwargs)\n", + "\n", + " def forward(\n", + " self, d: tuple[Tensor, Tensor | None]\n", + " ) -> tuple[Tensor, Tensor | None]:\n", + " \"\"\"Forwards function\n", + "\n", + " Args:\n", + " d (tuple): tuple of the data tensor and the attention mask\n", + " Returns:\n", + " output (Tensor): normalised output data\n", + " attn_mask (Tensor): the attention mask that was passed in\n", + " \"\"\"\n", + " input, attn_mask = d\n", + " output = F.layer_norm(\n", + " input, self.normalized_shape, self.weight, self.bias, self.eps\n", + " )\n", + " return output, attn_mask\n", + "\n", + "\n", + "class MultiheadAttention(nn.Module):\n", + " \"\"\"Multihead attention layer for inputs of shape\n", + " [..., sequence, features].\n", + " \"\"\"\n", + "\n", + " def __init__(self, features: int, n_heads: int, dropout: float) -> None:\n", + " \"\"\"\n", + " Args:\n", + " features: Number of features for inputs to the layer.\n", + " n_heads: Number of attention heads. Should be a factor of features.\n", + " (I.e. the layer uses features // n_heads.)\n", + " dropout: Dropout.\n", + " \"\"\" # noqa: E501\n", + " super().__init__()\n", + "\n", + " if (features % n_heads) != 0:\n", + " raise ValueError(\n", + " f\"Features '{features}' is not divisible by heads '{n_heads}'.\"\n", + " )\n", + "\n", + " self.features = features\n", + " self.n_heads = n_heads\n", + " self.dropout = dropout\n", + "\n", + " self.qkv_layer = torch.nn.Linear(features, features * 3, bias=False)\n", + " self.w_layer = torch.nn.Linear(features, features, bias=False)\n", + "\n", + " def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:\n", + " \"\"\"\n", + " Args:\n", + " d (tuple): tuple containing Tensor of shape [..., sequence, features] and the attention mask\n", + " Returns:\n", + " Tensor: Tensor of shape [..., sequence, features]\n", + " \"\"\" # noqa: E501\n", + " x, attn_mask = d\n", + "\n", + " if not x.shape[-1] == self.features:\n", + " raise ValueError(\n", + " f\"Expecting tensor with last dimension size {self.features}.\"\n", + " )\n", + "\n", + " passenger_dims = x.shape[:-2]\n", + " B = passenger_dims.numel()\n", + " S = x.shape[-2]\n", + " C = x.shape[-1]\n", + " x = x.reshape(B, S, C)\n", + "\n", + " # x [B, S, C]\n", + " # q, k, v [B, H, S, C/H]\n", + " q, k, v = (\n", + " self.qkv_layer(x)\n", + " .view(B, S, self.n_heads, 3 * (C // self.n_heads))\n", + " .transpose(1, 2)\n", + " .chunk(chunks=3, dim=3)\n", + " )\n", + "\n", + " # Let us enforce either flash (A100+) or memory efficient attention.\n", + " if version(\"torch\") > \"2.3.0\":\n", + " with sdpa_kernel(\n", + " [SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION]\n", + " ):\n", + " # x [B, H, S, C//H]\n", + " x = F.scaled_dot_product_attention(\n", + " q, k, v, attn_mask=attn_mask, dropout_p=self.dropout\n", + " )\n", + " else:\n", + " with torch.backends.cuda.sdp_kernel(\n", + " enable_flash=True, enable_math=False, enable_mem_efficient=True\n", + " ):\n", + " # x [B, H, S, C//H]\n", + " x = F.scaled_dot_product_attention(\n", + " q, k, v, dropout_p=self.dropout\n", + " )\n", + "\n", + " # x [B, S, C]\n", + " x = x.transpose(1, 2).view(B, S, C)\n", + "\n", + " # x [B, S, C]\n", + " x = self.w_layer(x)\n", + "\n", + " # Back to input shape\n", + " x = x.view(*passenger_dims, S, self.features)\n", + " return x\n", + "\n", + "\n", + "class Transformer(nn.Module):\n", + " \"\"\"\n", + " Transformer for inputs of shape [..., S, features].\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " features: int,\n", + " mlp_multiplier: int,\n", + " n_heads: int,\n", + " dropout: float,\n", + " drop_path: float,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " features: Number of features for inputs to the layer.\n", + " mlp_multiplier: Model uses features*mlp_multiplier hidden units.\n", + " n_heads: Number of attention heads. Should be a factor of features.\n", + " (I.e. the layer uses features // n_heads.) dropout: Dropout.\n", + " drop_path: DropPath.\n", + " \"\"\"\n", + " super().__init__()\n", + "\n", + " self.features = features\n", + " self.mlp_multiplier = mlp_multiplier\n", + " self.n_heads = n_heads\n", + " self.dropout = dropout\n", + " self.drop_path = (\n", + " DropPath(drop_path) if drop_path > 0.0 else nn.Identity()\n", + " )\n", + "\n", + " self.attention = nn.Sequential(\n", + " LayerNormPassThrough(features),\n", + " MultiheadAttention(features, n_heads, dropout),\n", + " )\n", + "\n", + " self.ff = nn.Sequential(\n", + " nn.LayerNorm(features),\n", + " Mlp(\n", + " features=features,\n", + " hidden_features=features * mlp_multiplier,\n", + " dropout=dropout,\n", + " ),\n", + " )\n", + "\n", + " def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:\n", + " \"\"\"\n", + " Args:\n", + " x: Tensor of shape [..., sequence, features]\n", + " Returns:\n", + " Tensor: Tensor of shape [..., sequence, features]\n", + " \"\"\"\n", + " x, attn_mask = d\n", + " if not x.shape[-1] == self.features:\n", + " raise ValueError(\n", + " f\"Expecting tensor with last dimension size {self.features}.\"\n", + " )\n", + "\n", + " attention_x = self.attention(d)\n", + "\n", + " x = x + self.drop_path(attention_x)\n", + " x = x + self.drop_path(self.ff(x))\n", + "\n", + " return x\n", + "\n", + "\n", + "class _Shift(nn.Module):\n", + " \"\"\"Private base class for the shifter. This allows some behaviour to be\n", + " easily handled when the shifter isn't used.\n", + " \"\"\"\n", + "\n", + " def __init__(self):\n", + " super().__init__()\n", + "\n", + " self._shifted = False\n", + "\n", + " @torch.no_grad()\n", + " def reset(self) -> None:\n", + " \"\"\"\n", + " Resets the bool tracking whether the data is shifted\n", + " \"\"\"\n", + " self._shifted: bool = False\n", + "\n", + " def forward(self, data: Tensor) -> tuple[Tensor, dict[bool, None]]:\n", + " return data, {True: None, False: None}\n", + "\n", + "\n", + "class SWINShift(_Shift):\n", + " \"\"\"\n", + " Handles the shifting of patches similar to how SWIN works. However if we\n", + " shift the latitudes then the poles will wrap and potentially that might be\n", + " problematic. The possition tokens should handle it but masking is safer.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " mu_shape: tuple[int, int],\n", + " global_shape: tuple[int, int],\n", + " local_shape: tuple[int, int],\n", + " patch_shape: tuple[int, int],\n", + " n_context_tokens: int = 2,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " mu_shape: the shape to the masking units\n", + " global_shape: number of global patches in lat and lon\n", + " local_shape: size of the local patches\n", + " patch_shape: patch size\n", + " n_context_token: number of additional context tokens at start of\n", + " _each_ local sequence\n", + " \"\"\"\n", + " super().__init__()\n", + "\n", + " self._mu_shape = ms = mu_shape\n", + " self._g_shape = gs = global_shape\n", + " self._l_shape = ls = local_shape\n", + " self._p_shape = ps = patch_shape\n", + " self._lat_patch = (gs[0], ls[0], gs[1], ls[1])\n", + " self._n_context_tokens = n_context_tokens\n", + "\n", + " self._g_shift_to = tuple(\n", + " int(0.5 * x / p) for x, p in zip(ms, ps, strict=False)\n", + " )\n", + " self._g_shift_from = tuple(\n", + " -int(0.5 * x / p) for x, p in zip(ms, ps, strict=False)\n", + " )\n", + "\n", + " # Define the attention masks for the shifted MaxViT.\n", + " nglobal = global_shape[0] * global_shape[1]\n", + " nlocal = (\n", + " local_shape[0] * local_shape[1] + self._n_context_tokens\n", + " ) # \"+ 1\" for leadtime\n", + "\n", + " lm = torch.ones((nglobal, 1, nlocal, nlocal), dtype=bool)\n", + " mwidth = int(0.5 * local_shape[1]) * local_shape[0]\n", + " lm[\n", + " : gs[1],\n", + " :,\n", + " self._n_context_tokens : mwidth + self._n_context_tokens,\n", + " self._n_context_tokens : mwidth + self._n_context_tokens,\n", + " ] = False\n", + " self.register_buffer(\"local_mask\", lm)\n", + "\n", + " gm = torch.ones((nlocal, 1, nglobal, nglobal), dtype=bool)\n", + " gm[: int(0.5 * ls[1]) * ls[0], :, : gs[1], : gs[1]] = False\n", + " self.register_buffer(\"global_mask\", gm)\n", + "\n", + " def _to_grid_global(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Shuffle and reshape the data from the global/local setting back to the\n", + " lat/lon grid setting\n", + " Args:\n", + " x: the data tensor to be shuffled.\n", + " Returns:\n", + " x: data in the global/local setting\n", + " \"\"\"\n", + " nbatch, *other = x.shape\n", + "\n", + " y1 = x.view(nbatch, *self._g_shape, *self._l_shape, -1)\n", + " y2 = y1.permute(0, 5, 1, 3, 2, 4).contiguous()\n", + "\n", + " s = y2.shape\n", + " return y2.view((nbatch, -1, s[2] * s[3], s[4] * s[5]))\n", + "\n", + " def _to_grid_local(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Shuffle and reshape the data from the local/global setting to the\n", + " lat/lon grid setting\n", + " Args:\n", + " x: the data tensor to be shuffled.\n", + " Returns:\n", + " x: data in the lat/lon setting.\n", + " \"\"\"\n", + " x = x.transpose(2, 1).contiguous()\n", + " return self._to_grid_global(x)\n", + "\n", + " def _from_grid_global(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Shuffle and reshape the data from the lat/lon grid to the global/local\n", + " setting\n", + " Args:\n", + " x: the data tensor to be shuffled.\n", + " Returns:\n", + " x: data in the global/local setting\n", + " \"\"\"\n", + " nbatch, *other = x.shape\n", + "\n", + " z1 = x.view(nbatch, -1, *self._lat_patch)\n", + " z2 = z1.permute(0, 2, 4, 3, 5, 1).contiguous()\n", + "\n", + " s = z2.shape\n", + " return z2.view(nbatch, s[1] * s[2], s[3] * s[4], -1)\n", + "\n", + " def _from_grid_local(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Shuffle and reshape the data from the lat/lon grid to the local/global\n", + " setting\n", + " Args:\n", + " x: the data tensor to be shuffled.\n", + " Returns:\n", + " x: data in the local/global setting\n", + " \"\"\"\n", + " x = self._from_grid_global(x)\n", + " return x.transpose(2, 1).contiguous()\n", + "\n", + " def _shift(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Shifts data in the gridded lat/lon setting by half the mask unit shape\n", + " Args:\n", + " x: data to be shifted\n", + " Returns:\n", + " x: either the hsifted or unshifted data\n", + " \"\"\"\n", + " shift = self._g_shift_from if self._shifted else self._g_shift_to\n", + " x_shifted = torch.roll(x, shift, (-2, -1))\n", + "\n", + " self._shifted = not self._shifted\n", + " return x_shifted\n", + "\n", + " def _sep_lt(self, x: Tensor) -> tuple[Tensor, Tensor]:\n", + " \"\"\"\n", + " Seperate off the leadtime from the local patches\n", + " Args:\n", + " x: data to have leadtime removed from\n", + " Returns:\n", + " lt: leadtime\n", + " x: data without the lead time in the local patch\n", + " \"\"\"\n", + " lt_it = x[:, : self._n_context_tokens, :, :]\n", + " x_stripped = x[:, self._n_context_tokens :, :, :]\n", + "\n", + " return lt_it, x_stripped\n", + "\n", + " def forward(self, data: Tensor) -> tuple[Tensor, Tensor]:\n", + " \"\"\"Shift or unshift the the data depending on whether the data is\n", + " already shifted, as defined by self._shifte.\n", + "\n", + " Args:\n", + " data: data to be shifted\n", + " Returns:\n", + " Tensor: shifted data Tensor\n", + " \"\"\"\n", + " lt, x = self._sep_lt(data)\n", + "\n", + " x_grid = self._to_grid_local(x)\n", + " x_shifted = self._shift(x_grid)\n", + " x_patched = self._from_grid_local(x_shifted)\n", + "\n", + " # Mask has to be repeated based on batch size\n", + " n_batch = x_grid.shape[0]\n", + " local_rep = [n_batch] + [1] * (self.local_mask.ndim - 1)\n", + " global_rep = [n_batch] + [1] * (self.global_mask.ndim - 1)\n", + "\n", + " if self._shifted:\n", + " attn_mask = {\n", + " True: self.local_mask.repeat(local_rep),\n", + " False: self.global_mask.repeat(global_rep),\n", + " }\n", + " else:\n", + " attn_mask = {True: None, False: None}\n", + "\n", + " return torch.cat((lt, x_patched), axis=1), attn_mask\n", + "\n", + "\n", + "class LocalGlobalLocalBlock(nn.Module):\n", + " \"\"\"\n", + " Applies alternating block and grid attention. Given a parameter n_blocks,\n", + " the entire module contains 2*n_blocks+1 transformer blocks. The first,\n", + " third, ..., last apply local (block) attention. The second, fourth, ...\n", + " global (grid) attention.\n", + "\n", + " This is heavily inspired by\n", + " Tu et al. \"MaxViT: Multi-Axis Vision Transformer\"\n", + " (https://arxiv.org/abs/2204.01697).\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " features: int,\n", + " mlp_multiplier: int,\n", + " n_heads: int,\n", + " dropout: float,\n", + " n_blocks: int,\n", + " drop_path: float,\n", + " shifter: nn.Module | None = None,\n", + " checkpoint: list[int] | None = None,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " features: Number of features for inputs to the layer.\n", + " mlp_multiplier: Model uses features*mlp_multiplier hidden units.\n", + " n_heads: Number of attention heads. Should be a factor of features.\n", + " (I.e. the layer uses features // n_heads.)\n", + " dropout: Dropout.\n", + " drop_path: DropPath.\n", + " n_blocks: Number of local-global transformer pairs.\n", + " \"\"\"\n", + " super().__init__()\n", + "\n", + " self.features = features\n", + " self.mlp_multiplier = mlp_multiplier\n", + " self.n_heads = n_heads\n", + " self.dropout = dropout\n", + " self.drop_path = drop_path\n", + " self.n_blocks = n_blocks\n", + " self._checkpoint = checkpoint or []\n", + "\n", + " if not all(0 <= c < 2 * n_blocks + 1 for c in self._checkpoint):\n", + " raise ValueError(\n", + " \"Checkpoints should be 0 <= i < 2*n_blocks+1. \"\n", + " f\"{self._checkpoint=}.\"\n", + " )\n", + "\n", + " self.transformers = nn.ModuleList(\n", + " [\n", + " Transformer(\n", + " features=features,\n", + " mlp_multiplier=mlp_multiplier,\n", + " n_heads=n_heads,\n", + " dropout=dropout,\n", + " drop_path=drop_path,\n", + " )\n", + " for _ in range(2 * n_blocks + 1)\n", + " ]\n", + " )\n", + "\n", + " self.evaluator = [\n", + " self._checkpoint_wrapper\n", + " if i in self._checkpoint\n", + " else lambda m, x: m(x)\n", + " for i, _ in enumerate(self.transformers)\n", + " ]\n", + "\n", + " self.shifter = shifter or _Shift()\n", + "\n", + " @staticmethod\n", + " def _checkpoint_wrapper(\n", + " model: nn.Module, data: tuple[Tensor, Tensor | None]\n", + " ) -> Tensor:\n", + " return checkpoint(model, data, use_reentrant=False)\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Args:\n", + " x: Tensor of shape::\n", + "\n", + " [batch, global_sequence, local_sequence, features]\n", + "\n", + " Returns:\n", + " Tensor: Tensor of shape::\n", + "\n", + " [batch, global_sequence, local_sequence, features]\n", + " \"\"\"\n", + " if x.shape[-1] != self.features:\n", + " raise ValueError(\n", + " f\"Expecting tensor with last dimension size {self.features}.\"\n", + " )\n", + " if x.ndim != 4:\n", + " raise ValueError(\n", + " f\"Expecting tensor with exactly four dimensions. {x.shape=}.\"\n", + " )\n", + "\n", + " self.shifter.reset()\n", + " local: bool = True\n", + " attn_mask = {True: None, False: None}\n", + "\n", + " transformer_iter = zip(self.evaluator, self.transformers, strict=False)\n", + "\n", + " # First local block\n", + " evaluator, transformer = next(transformer_iter)\n", + " x = evaluator(transformer, (x, attn_mask[local]))\n", + "\n", + " for evaluator, transformer in transformer_iter:\n", + " local = not local\n", + " # We are making exactly 2*n_blocks transposes.\n", + " # So the output has the same shape as input.\n", + " x = x.transpose(1, 2)\n", + "\n", + " x = evaluator(transformer, (x, attn_mask[local]))\n", + "\n", + " if not local:\n", + " x, attn_mask = self.shifter(x)\n", + "\n", + " return x\n", + "\n", + "\n", + "class PatchEmbed(nn.Module):\n", + " \"\"\"\n", + " Patch embedding via 2D convolution.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self, patch_size: int | tuple[int, ...], channels: int, embed_dim: int\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.patch_size = patch_size\n", + " self.channels = channels\n", + " self.embed_dim = embed_dim\n", + "\n", + " self.proj = nn.Conv2d(\n", + " channels,\n", + " embed_dim,\n", + " kernel_size=patch_size,\n", + " stride=patch_size,\n", + " bias=True,\n", + " )\n", + "\n", + " def forward(self, x: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Args:\n", + " x: Tensor of shape [batch, channels, lat, lon].\n", + " Returns:\n", + " Tensor: Tensor with shape\n", + " [batch, embed_dim, lat//patch_size, lon//patch_size]\n", + " \"\"\"\n", + "\n", + " H, W = x.shape[-2:]\n", + "\n", + " if W % self.patch_size[1] != 0:\n", + " raise ValueError(\n", + " f\"Cannot do patch embedding for tensor of shape {x.size()}\"\n", + " \" with patch size {self.patch_size}. (Dimensions are BSCHW.)\"\n", + " )\n", + " if H % self.patch_size[0] != 0:\n", + " raise ValueError(\n", + " f\"Cannot do patch embedding for tensor of shape {x.size()}\"\n", + " f\" with patch size {self.patch_size}. (Dimensions are BSCHW.)\"\n", + " )\n", + "\n", + " x = self.proj(x)\n", + "\n", + " return x\n", + "\n", + "\n", + "class PrithviWxCEncoderDecoder(nn.Module):\n", + " \"\"\"\n", + " Hiera-MaxViT encoder/decoder code.\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " embed_dim: int,\n", + " n_blocks: int,\n", + " mlp_multiplier: float,\n", + " n_heads: int,\n", + " dropout: float,\n", + " drop_path: float,\n", + " shifter: nn.Module | None = None,\n", + " transformer_cp: list[int] | None = None,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " embed_dim: Embedding dimension\n", + " n_blocks: Number of local-global transformer pairs.\n", + " mlp_multiplier: MLP multiplier for hidden features in feed forward\n", + " networks.\n", + " n_heads: Number of attention heads.\n", + " dropout: Dropout.\n", + " drop_path: DropPath.\n", + " \"\"\"\n", + " super().__init__()\n", + "\n", + " self.embed_dim = embed_dim\n", + " self.n_blocks = n_blocks\n", + " self.mlp_multiplier = mlp_multiplier\n", + " self.n_heads = n_heads\n", + " self.dropout = dropout\n", + " self._transformer_cp = transformer_cp\n", + "\n", + " self.lgl_block = LocalGlobalLocalBlock(\n", + " features=embed_dim,\n", + " mlp_multiplier=mlp_multiplier,\n", + " n_heads=n_heads,\n", + " dropout=dropout,\n", + " drop_path=drop_path,\n", + " n_blocks=n_blocks,\n", + " shifter=shifter,\n", + " checkpoint=transformer_cp,\n", + " )\n", + "\n", + " def forward(self, x: torch.Tensor) -> torch.Tensor:\n", + " \"\"\"\n", + " Args:\n", + " x: Tensor of shape\n", + " [batch, global sequence, local sequence, embed_dim]\n", + " Returns:\n", + " Tensor of shape\n", + " [batch, mask_unit_sequence, local_sequence, embed_dim].\n", + " Identical in shape to the input x.\n", + " \"\"\"\n", + "\n", + " x = self.lgl_block(x)\n", + "\n", + " return x\n", + "\n", + "\n", + "class PrithviWxC(nn.Module):\n", + " \"\"\"Encoder-decoder fusing Hiera with MaxViT. See\n", + " - Ryali et al. \"Hiera: A Hierarchical Vision Transformer without the\n", + " Bells-and-Whistles\" (https://arxiv.org/abs/2306.00989)\n", + " - Tu et al. \"MaxViT: Multi-Axis Vision Transformer\"\n", + " (https://arxiv.org/abs/2204.01697)\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " in_channels: int,\n", + " input_size_time: int,\n", + " in_channels_static: int,\n", + " input_scalers_mu: Tensor,\n", + " input_scalers_sigma: Tensor,\n", + " input_scalers_epsilon: float,\n", + " static_input_scalers_mu: Tensor,\n", + " static_input_scalers_sigma: Tensor,\n", + " static_input_scalers_epsilon: float,\n", + " output_scalers: Tensor,\n", + " n_lats_px: int,\n", + " n_lons_px: int,\n", + " patch_size_px: tuple[int],\n", + " mask_unit_size_px: tuple[int],\n", + " mask_ratio_inputs: float,\n", + " embed_dim: int,\n", + " n_blocks_encoder: int,\n", + " n_blocks_decoder: int,\n", + " mlp_multiplier: float,\n", + " n_heads: int,\n", + " dropout: float,\n", + " drop_path: float,\n", + " parameter_dropout: float,\n", + " residual: str,\n", + " masking_mode: str,\n", + " positional_encoding: str,\n", + " decoder_shifting: bool = False,\n", + " checkpoint_encoder: list[int] | None = None,\n", + " checkpoint_decoder: list[int] | None = None,\n", + " ) -> None:\n", + " \"\"\"\n", + " Args:\n", + " in_channels: Number of input channels.\n", + " input_size_time: Number of timestamps in input.\n", + " in_channels_static: Number of input channels for static data.\n", + " input_scalers_mu: Tensor of size (in_channels,). Used to rescale\n", + " input.\n", + " input_scalers_sigma: Tensor of size (in_channels,). Used to rescale\n", + " input.\n", + " input_scalers_epsilon: Float. Used to rescale input.\n", + " static_input_scalers_mu: Tensor of size (in_channels_static). Used\n", + " to rescale static inputs.\n", + " static_input_scalers_sigma: Tensor of size (in_channels_static).\n", + " Used to rescale static inputs.\n", + " static_input_scalers_epsilon: Float. Used to rescale static inputs.\n", + " output_scalers: Tensor of shape (in_channels,). Used to rescale\n", + " output.\n", + " n_lats_px: Total latitudes in data. In pixels.\n", + " n_lons_px: Total longitudes in data. In pixels.\n", + " patch_size_px: Patch size for tokenization. In pixels lat/lon.\n", + " mask_unit_size_px: Size of each mask unit. In pixels lat/lon.\n", + " mask_ratio_inputs: Masking ratio for inputs. 0 to 1.\n", + " embed_dim: Embedding dimension\n", + " n_blocks_encoder: Number of local-global transformer pairs in\n", + " encoder.\n", + " n_blocks_decoder: Number of local-global transformer pairs in\n", + " decoder.\n", + " mlp_multiplier: MLP multiplier for hidden features in feed forward\n", + " networks.\n", + " n_heads: Number of attention heads.\n", + " dropout: Dropout.\n", + " drop_path: DropPath.\n", + " parameter_dropout: Dropout applied to parameters.\n", + " residual: Indicates whether and how model should work as residual\n", + " model. Accepted values are 'climate', 'temporal' and 'none'\n", + " positional_encoding: possible values are\n", + " ['absolute' (default), 'fourier'].\n", + " 'absolute' lat lon encoded in 3 dimensions using sine and\n", + " cosine\n", + " 'fourier' lat/lon to be encoded using various frequencies\n", + " masking_mode: String ['local', 'global', 'both'] that controls the\n", + " type of masking used.\n", + " checkpoint_encoder: List of integers controlling if gradient\n", + " checkpointing is used on encoder.\n", + " Format: [] for no gradient checkpointing. [3, 7] for\n", + " checkpointing after 4th and 8th layer etc.\n", + " checkpoint_decoder: List of integers controlling if gradient\n", + " checkpointing is used on decoder.\n", + " Format: See `checkpoint_encoder`.\n", + " masking_mode: The type of masking to use\n", + " {'global', 'local', 'both'}\n", + " decoder_shifting: Whether to use swin shifting in the decoder.\n", + " \"\"\"\n", + " super().__init__()\n", + "\n", + " self.in_channels = in_channels\n", + " self.input_size_time = input_size_time\n", + " self.in_channels_static = in_channels_static\n", + " self.n_lats_px = n_lats_px\n", + " self.n_lons_px = n_lons_px\n", + " self.patch_size_px = patch_size_px\n", + " self.mask_unit_size_px = mask_unit_size_px\n", + " self.mask_ratio_inputs = mask_ratio_inputs\n", + " self.embed_dim = embed_dim\n", + " self.n_blocks_encoder = n_blocks_encoder\n", + " self.n_blocks_decoder = n_blocks_decoder\n", + " self.mlp_multiplier = mlp_multiplier\n", + " self.n_heads = n_heads\n", + " self.dropout = dropout\n", + " self.drop_path = drop_path\n", + " self.residual = residual\n", + " self._decoder_shift = decoder_shifting\n", + " self.positional_encoding = positional_encoding\n", + " self._checkpoint_encoder = checkpoint_encoder\n", + " self._checkpoint_decoder = checkpoint_decoder\n", + "\n", + " assert self.n_lats_px % self.mask_unit_size_px[0] == 0\n", + " assert self.n_lons_px % self.mask_unit_size_px[1] == 0\n", + " assert self.mask_unit_size_px[0] % self.patch_size_px[0] == 0\n", + " assert self.mask_unit_size_px[1] % self.patch_size_px[1] == 0\n", + "\n", + " if self.patch_size_px[0] != self.patch_size_px[1]:\n", + " raise NotImplementedError(\n", + " \"Current pixel shuffle symmetric patches.\"\n", + " )\n", + "\n", + " self.local_shape_mu = (\n", + " self.mask_unit_size_px[0] // self.patch_size_px[0],\n", + " self.mask_unit_size_px[1] // self.patch_size_px[1],\n", + " )\n", + " self.global_shape_mu = (\n", + " self.n_lats_px // self.mask_unit_size_px[0],\n", + " self.n_lons_px // self.mask_unit_size_px[1],\n", + " )\n", + "\n", + " assert input_scalers_mu.shape == (in_channels,)\n", + " assert input_scalers_sigma.shape == (in_channels,)\n", + " assert output_scalers.shape == (in_channels,)\n", + "\n", + " if self.positional_encoding != \"fourier\":\n", + " assert static_input_scalers_mu.shape == (in_channels_static,)\n", + " assert static_input_scalers_sigma.shape == (in_channels_static,)\n", + "\n", + " # Input shape [batch, time, parameter, lat, lon]\n", + " self.input_scalers_epsilon = input_scalers_epsilon\n", + " self.register_buffer(\n", + " \"input_scalers_mu\", input_scalers_mu.reshape(1, 1, -1, 1, 1)\n", + " )\n", + " self.register_buffer(\n", + " \"input_scalers_sigma\", input_scalers_sigma.reshape(1, 1, -1, 1, 1)\n", + " )\n", + "\n", + " # Static inputs shape [batch, parameter, lat, lon]\n", + " self.static_input_scalers_epsilon = static_input_scalers_epsilon\n", + " self.register_buffer(\n", + " \"static_input_scalers_mu\",\n", + " static_input_scalers_mu.reshape(1, -1, 1, 1),\n", + " )\n", + " self.register_buffer(\n", + " \"static_input_scalers_sigma\",\n", + " static_input_scalers_sigma.reshape(1, -1, 1, 1),\n", + " )\n", + "\n", + " # Output shape [batch, parameter, lat, lon]\n", + " self.register_buffer(\n", + " \"output_scalers\", output_scalers.reshape(1, -1, 1, 1)\n", + " )\n", + "\n", + " self.parameter_dropout = nn.Dropout2d(p=parameter_dropout)\n", + "\n", + " self.patch_embedding = PatchEmbed(\n", + " patch_size=patch_size_px,\n", + " channels=in_channels * input_size_time,\n", + " embed_dim=embed_dim,\n", + " )\n", + "\n", + " if self.residual == \"climate\":\n", + " self.patch_embedding_static = PatchEmbed(\n", + " patch_size=patch_size_px,\n", + " channels=in_channels + in_channels_static,\n", + " embed_dim=embed_dim,\n", + " )\n", + " else:\n", + " self.patch_embedding_static = PatchEmbed(\n", + " patch_size=patch_size_px,\n", + " channels=in_channels_static,\n", + " embed_dim=embed_dim,\n", + " )\n", + "\n", + " self.input_time_embedding = nn.Linear(1, embed_dim // 4, bias=True)\n", + " self.lead_time_embedding = nn.Linear(1, embed_dim // 4, bias=True)\n", + "\n", + " self.mask_token = nn.Parameter(torch.randn(1, 1, 1, self.embed_dim))\n", + " self._nglobal_mu = np.prod(self.global_shape_mu)\n", + " self._global_idx = torch.arange(self._nglobal_mu)\n", + "\n", + " self._nlocal_mu = np.prod(self.local_shape_mu)\n", + " self._local_idx = torch.arange(self._nlocal_mu)\n", + "\n", + " self.encoder = PrithviWxCEncoderDecoder(\n", + " embed_dim=embed_dim,\n", + " n_blocks=n_blocks_encoder,\n", + " mlp_multiplier=mlp_multiplier,\n", + " n_heads=n_heads,\n", + " dropout=dropout,\n", + " drop_path=drop_path,\n", + " transformer_cp=checkpoint_encoder,\n", + " )\n", + "\n", + " if n_blocks_decoder != 0:\n", + " if self._decoder_shift:\n", + " self.decoder_shifter = d_shifter = SWINShift(\n", + " self.mask_unit_size_px,\n", + " self.global_shape_mu,\n", + " self.local_shape_mu,\n", + " self.patch_size_px,\n", + " n_context_tokens=0,\n", + " )\n", + " else:\n", + " self.decoder_shifter = d_shifter = None\n", + "\n", + " self.decoder = PrithviWxCEncoderDecoder(\n", + " embed_dim=embed_dim,\n", + " n_blocks=n_blocks_decoder,\n", + " mlp_multiplier=mlp_multiplier,\n", + " n_heads=n_heads,\n", + " dropout=dropout,\n", + " drop_path=0.0,\n", + " shifter=d_shifter,\n", + " transformer_cp=checkpoint_decoder,\n", + " )\n", + "\n", + " self.unembed = nn.Linear(\n", + " self.embed_dim,\n", + " self.in_channels\n", + " * self.patch_size_px[0]\n", + " * self.patch_size_px[1],\n", + " bias=True,\n", + " )\n", + "\n", + " self.masking_mode = masking_mode.lower()\n", + " match self.masking_mode:\n", + " case \"local\":\n", + " self.generate_mask = self._gen_mask_local\n", + " case \"global\":\n", + " self.generate_mask = self._gen_mask_global\n", + " case \"both\":\n", + " self._mask_both_local: bool = True\n", + " self.generate_mask = self._gen_mask_both\n", + " case _:\n", + " raise ValueError(\n", + " f\"Masking mode '{masking_mode}' not supported\"\n", + " )\n", + "\n", + " def swap_masking(self) -> None:\n", + " self._mask_both_local = not self._mask_both_local\n", + "\n", + " @cached_property\n", + " def n_masked_global(self):\n", + " return int(self.mask_ratio_inputs * np.prod(self.global_shape_mu))\n", + "\n", + " @cached_property\n", + " def n_masked_local(self):\n", + " return int(self.mask_ratio_inputs * np.prod(self.local_shape_mu))\n", + "\n", + " @staticmethod\n", + " def _shuffle_along_axis(a, axis):\n", + " idx = torch.argsort(input=torch.rand(*a.shape), dim=axis)\n", + " return torch.gather(a, dim=axis, index=idx)\n", + "\n", + " def _gen_mask_local(self, sizes: tuple[int]) -> tuple[Tensor]:\n", + " \"\"\"\n", + " Args:\n", + " batch_size: Number of elements in batch\n", + " Returns:\n", + " Tuple of torch tensors. [indices masked, indices unmasked].\n", + " Each of these is a tensor of shape (batch, global sequene)\n", + " \"\"\"\n", + " # Identify which indices (values) should be masked\n", + "\n", + " maskable_indices = self._local_idx.view(1, -1).expand(*sizes[:2], -1)\n", + "\n", + " maskable_indices = self._shuffle_along_axis(maskable_indices, 2)\n", + "\n", + " indices_masked = maskable_indices[:, :, : self.n_masked_local]\n", + " indices_unmasked = maskable_indices[:, :, self.n_masked_local :]\n", + "\n", + " return indices_masked, indices_unmasked\n", + "\n", + " def _gen_mask_global(self, sizes: tuple[int]) -> tuple[Tensor]:\n", + " \"\"\"\n", + " Args:\n", + " batch_size: Number of elements in batch\n", + " Returns:\n", + " Tuple of torch tensors. [indices masked, indices unmasked].\n", + " Each of these is a tensor of shape (batch, global sequene)\n", + " \"\"\"\n", + " # Identify which indices (values) should be masked\n", + "\n", + " maskable_indices = self._global_idx.view(1, -1).expand(*sizes[:1], -1)\n", + "\n", + " maskable_indices = self._shuffle_along_axis(maskable_indices, 1)\n", + "\n", + " indices_masked = maskable_indices[:, : self.n_masked_global]\n", + " indices_unmasked = maskable_indices[:, self.n_masked_global :]\n", + "\n", + " return indices_masked, indices_unmasked\n", + "\n", + " def _gen_mask_both(self, sizes: tuple[int]) -> tuple[Tensor]:\n", + " if self._mask_both_local:\n", + " return self._gen_mask_local(sizes)\n", + " else:\n", + " return self._gen_mask_global(sizes)\n", + "\n", + " @staticmethod\n", + " def reconstruct_batch(\n", + " idx_masked: Tensor,\n", + " idx_unmasked: Tensor,\n", + " data_masked: Tensor,\n", + " data_unmasked: Tensor,\n", + " ) -> Tensor:\n", + " \"\"\"Reconstructs a tensor along the mask unit dimension. Batched\n", + " version.\n", + "\n", + " Args:\n", + " idx_masked: Tensor of shape `batch, mask unit sequence`.\n", + " idx_unmasked: Tensor of shape `batch, mask unit sequence`.\n", + " data_masked: Tensor of shape `batch, mask unit sequence, ...`.\n", + " Should have same size along mask unit sequence dimension as\n", + " idx_masked. Dimensions beyond the first two, marked here as ...\n", + " will typically be `local_sequence, channel` or\n", + " `channel, lat, lon`. These dimensions should agree with\n", + " data_unmasked.\n", + " data_unmasked: Tensor of shape `batch, mask unit sequence, ...`.\n", + " Should have same size along mask unit sequence dimension as\n", + " idx_unmasked. Dimensions beyond the first two, marked here as\n", + " ... will typically be `local_sequence, channel` or `channel,\n", + " lat, lon`. These dimensions should agree with data_masked.\n", + " Returns:\n", + " Tensor: Tensor of same shape as inputs data_masked and\n", + " data_unmasked. I.e. `batch, mask unit sequence, ...`. Index for\n", + " the total data composed of the masked and the unmasked part.\n", + " \"\"\"\n", + " dim: int = idx_masked.ndim\n", + "\n", + " idx_total = torch.argsort(\n", + " torch.cat([idx_masked, idx_unmasked], dim=-1), dim=-1\n", + " )\n", + " idx_total = idx_total.view(\n", + " *idx_total.shape, *[1] * (data_unmasked.ndim - dim)\n", + " )\n", + " idx_total = idx_total.expand(\n", + " *idx_total.shape[:dim], *data_unmasked.shape[dim:]\n", + " )\n", + "\n", + " data = torch.cat([data_masked, data_unmasked], dim=dim - 1)\n", + " data = torch.gather(data, dim=dim - 1, index=idx_total)\n", + "\n", + " return data, idx_total\n", + "\n", + " def fourier_pos_encoding(self, x_static: Tensor) -> Tensor:\n", + " \"\"\"\n", + " Args\n", + " x_static: B x C x H x W. first two channels are lat, and lon\n", + " Returns\n", + " Tensor: Tensor of shape B x E x H x W where E is the embedding\n", + " dimension.\n", + " \"\"\"\n", + "\n", + " # B x C x H x W -> B x 1 x H/P x W/P\n", + " latitudes_patch = F.avg_pool2d(\n", + " x_static[:, [0]],\n", + " kernel_size=self.patch_size_px,\n", + " stride=self.patch_size_px,\n", + " )\n", + " longitudes_patch = F.avg_pool2d(\n", + " x_static[:, [1]],\n", + " kernel_size=self.patch_size_px,\n", + " stride=self.patch_size_px,\n", + " )\n", + "\n", + " modes = (\n", + " torch.arange(self.embed_dim // 4, device=x_static.device).view(\n", + " 1, -1, 1, 1\n", + " )\n", + " + 1.0\n", + " )\n", + " pos_encoding = torch.cat(\n", + " (\n", + " torch.sin(latitudes_patch * modes),\n", + " torch.sin(longitudes_patch * modes),\n", + " torch.cos(latitudes_patch * modes),\n", + " torch.cos(longitudes_patch * modes),\n", + " ),\n", + " axis=1,\n", + " )\n", + "\n", + " return pos_encoding # B x E x H/P x W/P\n", + "\n", + " def time_encoding(self, input_time, lead_time):\n", + " \"\"\"\n", + " Args:\n", + " input_time: Tensor of shape [batch].\n", + " lead_time: Tensor of shape [batch].\n", + " Returns:\n", + " Tensor: Tensor of shape [batch, embed_dim, 1, 1]\n", + " \"\"\"\n", + " input_time = self.input_time_embedding(input_time.view(-1, 1, 1, 1))\n", + " lead_time = self.lead_time_embedding(lead_time.view(-1, 1, 1, 1))\n", + "\n", + " time_encoding = torch.cat(\n", + " (\n", + " torch.cos(input_time),\n", + " torch.cos(lead_time),\n", + " torch.sin(input_time),\n", + " torch.sin(lead_time),\n", + " ),\n", + " axis=3,\n", + " )\n", + " return time_encoding\n", + "\n", + " def to_patching(self, x: Tensor) -> Tensor:\n", + " \"\"\"Transform data from lat/lon space to two axis patching\n", + "\n", + " Args: ->\n", + " x: Tesnor in lat/lon space (N, C, Nlat//P_0, Nlon//P_1)\n", + "\n", + " Returns:\n", + " Tensor in patch space (N, G, L, C)\n", + " \"\"\"\n", + " n_batch = x.shape[0]\n", + "\n", + " x = x.view(\n", + " n_batch,\n", + " -1,\n", + " self.global_shape_mu[0],\n", + " self.local_shape_mu[0],\n", + " self.global_shape_mu[1],\n", + " self.local_shape_mu[1],\n", + " )\n", + " x = x.permute(0, 2, 4, 3, 5, 1).contiguous()\n", + "\n", + " s = x.shape\n", + " return x.view(n_batch, s[1] * s[2], s[3] * s[4], -1)\n", + "\n", + " def from_patching(self, x: Tensor) -> Tensor:\n", + " \"\"\"Transform data from two axis patching to lat/lon space\n", + "\n", + " Args:\n", + " x: Tensor in patch space with shape (N, G, L, C*P_0*P_1)\n", + "\n", + " Returns:\n", + " Tensor: Tensor in lat/lon space\n", + " (N, C*P_0*P_1, Nlat//P_0, Nlon // P_1)\n", + " \"\"\"\n", + " n_batch = x.shape[0]\n", + "\n", + " x = x.view(\n", + " n_batch,\n", + " self.global_shape_mu[0],\n", + " self.global_shape_mu[1],\n", + " self.local_shape_mu[0],\n", + " self.local_shape_mu[1],\n", + " -1,\n", + " )\n", + " x = x.permute(0, 5, 1, 3, 2, 4).contiguous()\n", + "\n", + " s = x.shape\n", + " return x.view(n_batch, -1, s[2] * s[3], s[4] * s[5])\n", + "\n", + " def forward(self, batch: dict[str, torch.Tensor]) -> torch.Tensor:\n", + " \"\"\"\n", + " Args:\n", + " batch: Dictionary the following keys::\n", + "\n", + " 'x': Tensor of shape [batch, time, parameter, lat, lon]\n", + " 'y': Tensor of shape [batch, parameter, lat, lon]\n", + " 'static': Tensor of shape [batch, channel_static, lat, lon]\n", + " 'climate': Optional tensor of shape [batch, parameter, lat, lon]\n", + " 'input_time': Tensor of shape [batch]. Or none.\n", + " 'lead_time': Tensor of shape [batch]. Or none.\n", + "\n", + " Returns:\n", + " Tensor: Tensor of shape [batch, parameter, lat, lon].\n", + " \"\"\" # noqa: E501\n", + " x_rescaled = (batch[\"x\"] - self.input_scalers_mu) / (\n", + " self.input_scalers_sigma + self.input_scalers_epsilon\n", + " )\n", + " batch_size = x_rescaled.shape[0]\n", + "\n", + " if self.positional_encoding == \"fourier\":\n", + " x_static_pos = self.fourier_pos_encoding(batch[\"static\"])\n", + " x_static = (\n", + " batch[\"static\"][:, 2:] - self.static_input_scalers_mu[:, 3:]\n", + " ) / (\n", + " self.static_input_scalers_sigma[:, 3:]\n", + " + self.static_input_scalers_epsilon\n", + " )\n", + " else:\n", + " x_static = (batch[\"static\"] - self.static_input_scalers_mu) / (\n", + " self.static_input_scalers_sigma\n", + " + self.static_input_scalers_epsilon\n", + " )\n", + "\n", + " if self.residual == \"temporal\":\n", + " # We create a residual of same shape as y\n", + " index = torch.where(\n", + " batch[\"lead_time\"] > 0, batch[\"x\"].shape[1] - 1, 0\n", + " )\n", + " index = index.view(-1, 1, 1, 1, 1)\n", + " index = index.expand(batch_size, 1, *batch[\"x\"].shape[2:])\n", + " x_hat = torch.gather(batch[\"x\"], dim=1, index=index)\n", + " x_hat = x_hat.squeeze(1)\n", + " elif self.residual == \"climate\":\n", + " climate_scaled = (\n", + " batch[\"climate\"] - self.input_scalers_mu.view(1, -1, 1, 1)\n", + " ) / (\n", + " self.input_scalers_sigma.view(1, -1, 1, 1)\n", + " + self.input_scalers_epsilon\n", + " )\n", + "\n", + " # [batch, time, parameter, lat, lon]\n", + " # -> [batch, time x parameter, lat, lon]\n", + " x_rescaled = x_rescaled.flatten(1, 2)\n", + " # Parameter dropout\n", + " x_rescaled = self.parameter_dropout(x_rescaled)\n", + "\n", + " x_embedded = self.patch_embedding(x_rescaled)\n", + "\n", + " if self.residual == \"climate\":\n", + " static_embedded = self.patch_embedding_static(\n", + " torch.cat((x_static, climate_scaled), dim=1)\n", + " )\n", + " else:\n", + " static_embedded = self.patch_embedding_static(x_static)\n", + "\n", + " if self.positional_encoding == \"fourier\":\n", + " static_embedded += x_static_pos\n", + "\n", + " x_embedded = self.to_patching(x_embedded)\n", + " static_embedded = self.to_patching(static_embedded)\n", + "\n", + " time_encoding = self.time_encoding(\n", + " batch[\"input_time\"], batch[\"lead_time\"]\n", + " )\n", + "\n", + " tokens = x_embedded + static_embedded + time_encoding\n", + "\n", + " # Now we generate masks based on masking_mode\n", + " indices_masked, indices_unmasked = self.generate_mask(\n", + " (batch_size, self._nglobal_mu)\n", + " )\n", + " indices_masked = indices_masked.to(device=tokens.device)\n", + " indices_unmasked = indices_unmasked.to(device=tokens.device)\n", + " maskdim: int = indices_masked.ndim\n", + "\n", + " # Unmasking\n", + " unmask_view = (*indices_unmasked.shape, *[1] * (tokens.ndim - maskdim))\n", + " unmasked = torch.gather(\n", + " tokens,\n", + " dim=maskdim - 1,\n", + " index=indices_unmasked.view(*unmask_view).expand(\n", + " *indices_unmasked.shape, *tokens.shape[maskdim:]\n", + " ),\n", + " )\n", + "\n", + " # Encoder\n", + " x_encoded = self.encoder(unmasked)\n", + "\n", + " # Generate and position encode the mask tokens\n", + " # [1, 1, 1, embed_dim]\n", + " # -> [batch, global_seq_masked, local seq, embed_dim]\n", + " mask_view = (*indices_masked.shape, *[1] * (tokens.ndim - maskdim))\n", + " masking = self.mask_token.repeat(*static_embedded.shape[:3], 1)\n", + " masked = masking + static_embedded\n", + " masked = torch.gather(\n", + " masked,\n", + " dim=maskdim - 1,\n", + " index=indices_masked.view(*mask_view).expand(\n", + " *indices_masked.shape, *tokens.shape[maskdim:]\n", + " ),\n", + " )\n", + "\n", + " recon, _ = self.reconstruct_batch(\n", + " indices_masked, indices_unmasked, masked, x_encoded\n", + " )\n", + "\n", + " x_decoded = self.decoder(recon)\n", + "\n", + " # Output: [batch, global sequence, local sequence,\n", + " # in_channels * patch_size[0] * patch_size[1]]\n", + " x_unembed = self.unembed(x_decoded)\n", + "\n", + " # Reshape to [batch, global_lat, global_lon, local_lat, local_lon,\n", + " # in_channels * patch_size[0] * patch_size[1]]\n", + " x_out = self.from_patching(x_unembed)\n", + "\n", + " # Pixel shuffle to [batch, in_channels, lat, lon]\n", + " x_out = F.pixel_shuffle(x_out, self.patch_size_px[0])\n", + "\n", + " if self.residual == \"temporal\":\n", + " x_out = self.output_scalers * x_out + x_hat\n", + " elif self.residual == \"climate\":\n", + " x_out = self.output_scalers * x_out + batch[\"climate\"]\n", + " elif self.residual == \"none\":\n", + " x_out = (\n", + " self.output_scalers * x_out\n", + " + self.input_scalers_mu.reshape(1, -1, 1, 1)\n", + " )\n", + "\n", + " return x_out\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "# from PrithviWxC.model import PrithviWxC\n", + "\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=\"config.yaml\",\n", + " local_dir=\".\",\n", + ")\n", + "\n", + "with open(\"./config.yaml\", \"r\") as f:\n", + " config = yaml.safe_load(f)\n", + "\n", + "model = PrithviWxC(\n", + " in_channels=config[\"params\"][\"in_channels\"],\n", + " input_size_time=config[\"params\"][\"input_size_time\"],\n", + " in_channels_static=config[\"params\"][\"in_channels_static\"],\n", + " input_scalers_mu=in_mu,\n", + " input_scalers_sigma=in_sig,\n", + " input_scalers_epsilon=config[\"params\"][\"input_scalers_epsilon\"],\n", + " static_input_scalers_mu=static_mu,\n", + " static_input_scalers_sigma=static_sig,\n", + " static_input_scalers_epsilon=config[\"params\"][\n", + " \"static_input_scalers_epsilon\"\n", + " ],\n", + " output_scalers=output_sig**0.5,\n", + " n_lats_px=config[\"params\"][\"n_lats_px\"],\n", + " n_lons_px=config[\"params\"][\"n_lons_px\"],\n", + " patch_size_px=config[\"params\"][\"patch_size_px\"],\n", + " mask_unit_size_px=config[\"params\"][\"mask_unit_size_px\"],\n", + " mask_ratio_inputs=masking_ratio,\n", + " embed_dim=config[\"params\"][\"embed_dim\"],\n", + " n_blocks_encoder=config[\"params\"][\"n_blocks_encoder\"],\n", + " n_blocks_decoder=config[\"params\"][\"n_blocks_decoder\"],\n", + " mlp_multiplier=config[\"params\"][\"mlp_multiplier\"],\n", + " n_heads=config[\"params\"][\"n_heads\"],\n", + " dropout=config[\"params\"][\"dropout\"],\n", + " drop_path=config[\"params\"][\"drop_path\"],\n", + " parameter_dropout=config[\"params\"][\"parameter_dropout\"],\n", + " residual=residual,\n", + " masking_mode=masking_mode,\n", + " decoder_shifting=decoder_shifting,\n", + " positional_encoding=positional_encoding,\n", + " checkpoint_encoder=[],\n", + " checkpoint_decoder=[],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load weights\n", + "We have provided unshared pretrained weights for the model,\n", + "which can now be loaded. The model can then be transferred\n", + "to the requested device." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "weights_path = Path(\"./weights/prithvi.wxc.2300m.v1.pt\")\n", + "hf_hub_download(\n", + " repo_id=\"Prithvi-WxC/prithvi.wxc.2300m.v1\",\n", + " filename=weights_path.name,\n", + " local_dir=\"./weights\",\n", + ")\n", + "\n", + "state_dict = torch.load(weights_path, weights_only=False)\n", + "if \"model_state\" in state_dict:\n", + " state_dict = state_dict[\"model_state\"]\n", + "model.load_state_dict(state_dict, strict=True)\n", + "\n", + "if (hasattr(model, \"device\") and model.device != device) or not hasattr(\n", + " model, \"device\"\n", + "):\n", + " model = model.to(device)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference\n", + "We are now ready to perform inference on the model. The data\n", + "returned from the dataset class requires additional\n", + "preprocessing; therefore, after polling the dataset, we process\n", + "the data using the `preproc` function. This processed data is\n", + "then transferred to the device. To recover the masking, we can\n", + "save the torch RNG state and use it later. Finally, we run the\n", + "model in evaluation mode without generating the gradient graph." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "# from PrithviWxC.dataloaders.merra2 import preproc\n", + "\n", + "data = next(iter(dataset))\n", + "batch = preproc([data], padding)\n", + "\n", + "for k, v in batch.items():\n", + " if isinstance(v, torch.Tensor):\n", + " batch[k] = v.to(device)\n", + "\n", + "rng_state_1 = torch.get_rng_state()\n", + "with torch.no_grad():\n", + " model.eval()\n", + " out = model(batch)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "t2m = out[0, 12].cpu().numpy()\n", + "\n", + "lat = np.linspace(-90, 90, out.shape[-2])\n", + "lon = np.linspace(-180, 180, out.shape[-1])\n", + "X, Y = np.meshgrid(lon, lat)\n", + "\n", + "plt.contourf(X, Y, t2m, 100)\n", + "plt.gca().set_aspect(\"equal\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}