Download scripts/data_loader.py from OneScience-Group/ClimODE: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/OneScience-Group/ClimODE/resolve/main/scripts/data_loader.py
- Command line
-
hf download hf://OneScience-Group/ClimODE/scripts/data_loader.py
-
curl -L -o data_loader.py https://huggingface.co/OneScience-Group/ClimODE/resolve/main/scripts/data_loader.py
12.2 kB
| """OneScience ERA5Dataset adapter for ClimODE's 32x64 global grid.""" | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Iterable, Sequence | |
| import h5py | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from torch.utils.data import DataLoader, Dataset | |
| try: | |
| from onescience.datapipes.climate.era5 import ERA5Dataset | |
| except ImportError as exc: # pragma: no cover - exercised only without OneScience | |
| ERA5Dataset = None | |
| _ERA5_IMPORT_ERROR = exc | |
| else: | |
| _ERA5_IMPORT_ERROR = None | |
| OFFICIAL_VARIABLES = ("z", "t", "t2m", "u10", "v10") | |
| def _require_era5dataset() -> None: | |
| if ERA5Dataset is None: | |
| raise ImportError( | |
| "ClimODE data loading requires OneScience ERA5Dataset; " | |
| "activate an environment containing onescience before running." | |
| ) from _ERA5_IMPORT_ERROR | |
| def _as_channel_vector(values: np.ndarray | torch.Tensor) -> torch.Tensor: | |
| tensor = torch.as_tensor(values, dtype=torch.float32) | |
| return tensor.reshape(-1) | |
| def _regrid_periodic(frame: torch.Tensor, target_size: tuple[int, int]) -> torch.Tensor: | |
| """Bilinearly sample [C,H,W] on WeatherBench cell centers.""" | |
| if frame.ndim != 3: | |
| raise ValueError(f"Expected [C,H,W], got {tuple(frame.shape)}") | |
| target_height, target_width = target_size | |
| if frame.shape[-2:] == target_size: | |
| return frame | |
| periodic = torch.cat([frame, frame[..., :1]], dim=-1).unsqueeze(0) | |
| latitude = torch.linspace( | |
| 90.0 - 90.0 / target_height, | |
| -90.0 + 90.0 / target_height, | |
| target_height, | |
| device=frame.device, | |
| dtype=frame.dtype, | |
| ) | |
| longitude = ( | |
| torch.arange(target_width, device=frame.device, dtype=frame.dtype) | |
| * (360.0 / target_width) | |
| ) | |
| lat2d, lon2d = torch.meshgrid(latitude, longitude, indexing="ij") | |
| grid = torch.stack([lon2d / 180.0 - 1.0, -lat2d / 90.0], dim=-1) | |
| return F.grid_sample( | |
| periodic, | |
| grid.unsqueeze(0), | |
| mode="bilinear", | |
| padding_mode="border", | |
| align_corners=True, | |
| )[0] | |
| def _load_stats(stats_dir: str | Path, channels: int) -> tuple[torch.Tensor, torch.Tensor]: | |
| stats_path = Path(stats_dir) | |
| minimum = _as_channel_vector(np.load(stats_path / "min_values.npy")) | |
| maximum = _as_channel_vector(np.load(stats_path / "max_values.npy")) | |
| if minimum.numel() != channels or maximum.numel() != channels: | |
| raise ValueError( | |
| f"Expected {channels} channel statistics, got {minimum.numel()} and {maximum.numel()}" | |
| ) | |
| if torch.any(maximum <= minimum): | |
| raise ValueError("All max_values must be greater than min_values") | |
| return minimum, maximum | |
| def _normalize(frame: torch.Tensor, minimum: torch.Tensor, maximum: torch.Tensor) -> torch.Tensor: | |
| scale = (maximum - minimum).clamp_min(torch.finfo(frame.dtype).eps) | |
| return (frame - minimum[:, None, None]) / scale[:, None, None] | |
| class ClimODEDataset(Dataset): | |
| """Return three history frames and the following target frame. | |
| The underlying annual files are always read by OneScience ``ERA5Dataset``. | |
| No direct HDF5 field access is used here, which keeps the model adapter | |
| compatible with the OneScience ERA5 contract. | |
| """ | |
| def __init__( | |
| self, | |
| data_dir: str | Path, | |
| years: Sequence[int], | |
| used_variables: Sequence[str] = OFFICIAL_VARIABLES, | |
| stats_dir: str | Path | None = None, | |
| model_size: tuple[int, int] = (32, 64), | |
| normalize: bool = True, | |
| input_steps: int = 1, | |
| output_steps: int = 1, | |
| ) -> None: | |
| _require_era5dataset() | |
| if tuple(used_variables) != OFFICIAL_VARIABLES: | |
| raise ValueError( | |
| "ClimODE requires the exact channel order ['z','t','t2m','u10','v10']" | |
| ) | |
| if input_steps != 1 or output_steps != 1: | |
| raise ValueError("The ClimODE adapter currently uses one input and one target step") | |
| if len(years) == 0: | |
| raise ValueError("At least one year is required") | |
| # ``data_dir`` is the OneScience dataset root containing data/*.h5, | |
| # rather than the nested data/ directory itself. | |
| self.data_dir = Path(data_dir) | |
| self.years = [int(year) for year in years] | |
| self.variables = tuple(used_variables) | |
| self.model_size = tuple(model_size) | |
| self.normalize = normalize | |
| self.era5 = ERA5Dataset( | |
| dataset_dir=str(self.data_dir), | |
| used_years=self.years, | |
| used_variables=list(self.variables), | |
| input_steps=1, | |
| output_steps=1, | |
| normalize=False, | |
| ) | |
| if (self.era5.H, self.era5.W) != (721, 1440): | |
| raise ValueError( | |
| "ClimODE raw-data adapter expects (721,1440), " | |
| f"got ({self.era5.H},{self.era5.W})" | |
| ) | |
| self.samples_per_year = self.era5.samples_per_year | |
| if self.samples_per_year < 3: | |
| raise ValueError("Each year needs at least four frames for a three-frame history and target") | |
| self.samples_per_year_with_history = self.samples_per_year - 2 | |
| self.minimum, self.maximum = ( | |
| _load_stats(stats_dir or self.data_dir / "static", len(self.variables)) | |
| if normalize | |
| else (torch.zeros(len(self.variables)), torch.ones(len(self.variables))) | |
| ) | |
| def __len__(self) -> int: | |
| return len(self.years) * self.samples_per_year_with_history | |
| def _frame(self, sample_index: int, target: bool = False) -> torch.Tensor: | |
| invar, outvar, _, _, _ = self.era5[sample_index] | |
| frame = outvar if target else invar | |
| frame = _regrid_periodic(torch.as_tensor(frame, dtype=torch.float32), self.model_size) | |
| if self.normalize: | |
| frame = _normalize(frame, self.minimum, self.maximum) | |
| return frame | |
| def __getitem__(self, index: int) -> dict[str, torch.Tensor | int | str]: | |
| if index < 0: | |
| index += len(self) | |
| if index < 0 or index >= len(self): | |
| raise IndexError(index) | |
| year_index = index // self.samples_per_year_with_history | |
| local_index = index % self.samples_per_year_with_history | |
| base = year_index * self.samples_per_year + local_index + 2 | |
| history = torch.stack([self._frame(base - 2), self._frame(base - 1), self._frame(base)]) | |
| target = self._frame(base, target=True) | |
| return { | |
| "history": history, | |
| "input": history[-1], | |
| "target": target, | |
| "year": self.years[year_index], | |
| "step_index": local_index + 2, | |
| } | |
| class ClimODESeriesDataset(Dataset): | |
| """Official-style sequence batches with years as the inner batch axis. | |
| Each item contains a contiguous sequence for every requested year. The | |
| outer DataLoader should use ``batch_size=1``; the sequence length plays the | |
| role of the official training batch of time points. | |
| """ | |
| def __init__( | |
| self, | |
| data_dir: str | Path, | |
| years: Sequence[int], | |
| used_variables: Sequence[str] = OFFICIAL_VARIABLES, | |
| stats_dir: str | Path | None = None, | |
| model_size: tuple[int, int] = (32, 64), | |
| sequence_length: int = 8, | |
| normalize: bool = True, | |
| ) -> None: | |
| _require_era5dataset() | |
| if tuple(used_variables) != OFFICIAL_VARIABLES: | |
| raise ValueError("ClimODE requires the exact channel order ['z','t','t2m','u10','v10']") | |
| if sequence_length < 1: | |
| raise ValueError("sequence_length must be positive") | |
| self.data_dir = Path(data_dir) | |
| self.years = [int(year) for year in years] | |
| self.variables = tuple(used_variables) | |
| self.model_size = tuple(model_size) | |
| self.sequence_length = int(sequence_length) | |
| self.normalize = normalize | |
| self.era5 = ERA5Dataset( | |
| dataset_dir=str(self.data_dir), | |
| used_years=self.years, | |
| used_variables=list(self.variables), | |
| input_steps=1, | |
| output_steps=1, | |
| normalize=False, | |
| ) | |
| if (self.era5.H, self.era5.W) != (721, 1440): | |
| raise ValueError( | |
| "ClimODE raw-data adapter expects (721,1440), " | |
| f"got ({self.era5.H},{self.era5.W})" | |
| ) | |
| self.samples_per_year = self.era5.samples_per_year | |
| self.frames_per_year = self.era5.T | |
| first_start = 2 | |
| # The official DataLoader keeps its final, possibly shorter batch. | |
| self.starts = list(range(first_start, self.frames_per_year, self.sequence_length)) | |
| if not self.starts: | |
| raise ValueError( | |
| f"Not enough frames ({self.era5.T}) for sequence_length={sequence_length} " | |
| "and a three-frame history" | |
| ) | |
| self.minimum, self.maximum = ( | |
| _load_stats(stats_dir or self.data_dir / "static", len(self.variables)) | |
| if normalize | |
| else (torch.zeros(len(self.variables)), torch.ones(len(self.variables))) | |
| ) | |
| def __len__(self) -> int: | |
| return len(self.starts) | |
| def _frame(self, year_index: int, frame_index: int) -> torch.Tensor: | |
| if frame_index < 0 or frame_index >= self.frames_per_year: | |
| raise IndexError(frame_index) | |
| sample_index = year_index * self.samples_per_year + min( | |
| frame_index, self.samples_per_year - 1 | |
| ) | |
| invar, outvar, _, _, _ = self.era5[sample_index] | |
| # ERA5Dataset's final input index is T-2; its paired target is frame T-1. | |
| frame = outvar if frame_index == self.frames_per_year - 1 else invar | |
| frame = _regrid_periodic(torch.as_tensor(frame, dtype=torch.float32), self.model_size) | |
| if self.normalize: | |
| frame = _normalize(frame, self.minimum, self.maximum) | |
| return frame | |
| def __getitem__(self, index: int) -> dict[str, torch.Tensor | int]: | |
| start = self.starts[index] | |
| history_per_year = [] | |
| sequence_per_year = [] | |
| for year_index in range(len(self.years)): | |
| history_per_year.append( | |
| torch.stack( | |
| [ | |
| self._frame(year_index, start - 2), | |
| self._frame(year_index, start - 1), | |
| self._frame(year_index, start), | |
| ] | |
| ) | |
| ) | |
| stop = min(start + self.sequence_length, self.frames_per_year) | |
| sequence_per_year.append( | |
| torch.stack( | |
| [self._frame(year_index, step) for step in range(start, stop)] | |
| ) | |
| ) | |
| stop = min(start + self.sequence_length, self.frames_per_year) | |
| return { | |
| "history": torch.stack(history_per_year, dim=0), | |
| "observations": torch.stack(sequence_per_year, dim=1), | |
| "time_steps": torch.arange(start, stop, dtype=torch.float32), | |
| "sequence_index": index, | |
| } | |
| def load_constants( | |
| static_file: str | Path, | |
| expected_size: tuple[int, int] = (32, 64), | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """Load [orography, lsm], latitude and longitude from constants.h5.""" | |
| with h5py.File(static_file, "r") as handle: | |
| constants = torch.stack( | |
| [ | |
| torch.as_tensor(handle["orography"][:], dtype=torch.float32), | |
| torch.as_tensor(handle["lsm"][:], dtype=torch.float32), | |
| ] | |
| ).unsqueeze(0) | |
| lat2d = torch.as_tensor(handle["lat2d"][:], dtype=torch.float32) | |
| lon2d = torch.as_tensor(handle["lon2d"][:], dtype=torch.float32) | |
| if tuple(constants.shape[-2:]) != expected_size: | |
| raise ValueError(f"Static constants have shape {tuple(constants.shape[-2:])}") | |
| return constants, lat2d, lon2d | |
| def make_dataloader( | |
| dataset: Dataset, | |
| batch_size: int, | |
| shuffle: bool, | |
| num_workers: int = 0, | |
| pin_memory: bool = False, | |
| ) -> DataLoader: | |
| return DataLoader( | |
| dataset, | |
| batch_size=batch_size, | |
| shuffle=shuffle, | |
| num_workers=num_workers, | |
| pin_memory=pin_memory, | |
| drop_last=False, | |
| ) | |