Download model/seeds.py from OneScience-Group/SEEDS: direct link, hf CLI and curl.
- Browser
- Download file 9.77 kB
-
https://huggingface.co/OneScience-Group/SEEDS/resolve/main/model/seeds.py
- Command line
-
hf download hf://OneScience-Group/SEEDS/model/seeds.py
-
curl -L -o seeds.py https://huggingface.co/OneScience-Group/SEEDS/resolve/main/model/seeds.py
9.77 kB
| """PyTorch implementation of the SEEDS conditional diffusion core.""" | |
| from __future__ import annotations | |
| import math | |
| from typing import Optional | |
| import torch | |
| from torch import Tensor, nn | |
| def _fourier_embedding(value: Tensor, dim: int, max_period: float = 10000.0) -> Tensor: | |
| """Return a deterministic sinusoidal embedding for diffusion time.""" | |
| half = dim // 2 | |
| frequencies = torch.exp( | |
| -math.log(max_period) * torch.arange(half, device=value.device, dtype=value.dtype) / max(half, 1) | |
| ) | |
| angles = value[..., None] * frequencies | |
| embedding = torch.cat((angles.sin(), angles.cos()), dim=-1) | |
| if dim % 2: | |
| embedding = torch.nn.functional.pad(embedding, (0, 1)) | |
| return embedding | |
| class _AxialBlock(nn.Module): | |
| def __init__(self, dim: int, heads: int, mlp_ratio: int, dropout: float) -> None: | |
| super().__init__() | |
| self.norm1 = nn.LayerNorm(dim) | |
| self.attention = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True) | |
| self.norm2 = nn.LayerNorm(dim) | |
| hidden = dim * mlp_ratio | |
| self.mlp = nn.Sequential(nn.Linear(dim, hidden), nn.GELU(), nn.Linear(hidden, dim), nn.Dropout(dropout)) | |
| def forward(self, sequence: Tensor) -> Tensor: | |
| normalized = self.norm1(sequence) | |
| attended, _ = self.attention(normalized, normalized, normalized, need_weights=False) | |
| sequence = sequence + attended | |
| return sequence + self.mlp(self.norm2(sequence)) | |
| class SEEDS(nn.Module): | |
| """Conditional score network for cubed-sphere atmospheric fields. | |
| Inputs use ``[batch, channels, faces, height, width]`` for one snapshot and | |
| ``[batch, seeds, channels, faces, height, width]`` for seed forecasts. | |
| The output is the normalized noise prediction with the target snapshot shape. | |
| """ | |
| def __init__( | |
| self, | |
| channels: int = 8, | |
| faces: int = 6, | |
| height: int = 48, | |
| width: int = 48, | |
| patch_size: int = 12, | |
| embed_dim: int = 768, | |
| spatial_layers: int = 6, | |
| field_layers: int = 4, | |
| sequence_layers: int = 6, | |
| mlp_ratio: int = 4, | |
| dropout: float = 0.0, | |
| seed_count: int = 2, | |
| sigma_min: float = 0.01, | |
| sigma_max: float = 100.0, | |
| ) -> None: | |
| super().__init__() | |
| if height % patch_size or width % patch_size: | |
| raise ValueError("height and width must be divisible by patch_size") | |
| if embed_dim % 2: | |
| raise ValueError("embed_dim must be even") | |
| self.channels, self.faces = channels, faces | |
| self.height, self.width = height, width | |
| self.patch_size, self.seed_count = patch_size, seed_count | |
| self.patch_rows, self.patch_cols = height // patch_size, width // patch_size | |
| self.patch_count = faces * self.patch_rows * self.patch_cols | |
| self.sigma_min, self.sigma_max = sigma_min, sigma_max | |
| heads = max(1, min(12, embed_dim // 64)) | |
| while embed_dim % heads: | |
| heads -= 1 | |
| self.patch_embedding = nn.Conv2d(channels, embed_dim, patch_size, patch_size) | |
| self.output_projection = nn.Linear(embed_dim, patch_size * patch_size) | |
| self.position_embedding = nn.Parameter(torch.zeros(1, 1, 1, self.patch_count, embed_dim)) | |
| self.field_embedding = nn.Parameter(torch.zeros(1, 1, channels, 1, embed_dim)) | |
| self.snapshot_embedding = nn.Parameter(torch.zeros(1, seed_count + 2, 1, 1, embed_dim)) | |
| self.time_projection = nn.Sequential(nn.Linear(embed_dim, embed_dim), nn.SiLU(), nn.Linear(embed_dim, embed_dim)) | |
| self.spatial_transformer = nn.ModuleList( | |
| [_AxialBlock(embed_dim, heads, mlp_ratio, dropout) for _ in range(spatial_layers)] | |
| ) | |
| self.field_transformer = nn.ModuleList( | |
| [_AxialBlock(embed_dim, heads, mlp_ratio, dropout) for _ in range(field_layers)] | |
| ) | |
| self.sequence_transformer = nn.ModuleList( | |
| [_AxialBlock(embed_dim, heads, mlp_ratio, dropout) for _ in range(sequence_layers)] | |
| ) | |
| nn.init.normal_(self.position_embedding, std=0.02) | |
| nn.init.normal_(self.field_embedding, std=0.02) | |
| nn.init.normal_(self.snapshot_embedding, std=0.02) | |
| def _check_inputs(self, noisy: Tensor, seeds: Tensor, climate: Optional[Tensor]) -> None: | |
| expected = (self.channels, self.faces, self.height, self.width) | |
| if noisy.ndim != 5 or tuple(noisy.shape[1:]) != expected: | |
| raise ValueError(f"noisy must have shape [B, {expected}], got {tuple(noisy.shape)}") | |
| if seeds.ndim != 6 or tuple(seeds.shape[2:]) != expected or seeds.shape[1] != self.seed_count: | |
| raise ValueError(f"seeds must have shape [B, {self.seed_count}, {expected}], got {tuple(seeds.shape)}") | |
| if climate is not None and (climate.ndim != 5 or tuple(climate.shape[1:]) != expected): | |
| raise ValueError(f"climate must have shape [B, {expected}], got {tuple(climate.shape)}") | |
| def _embed_snapshot(self, snapshot: Tensor) -> Tensor: | |
| batch, channels, faces, _, _ = snapshot.shape | |
| embedded = self.patch_embedding(snapshot.permute(0, 2, 1, 3, 4).reshape(batch * faces, channels, self.height, self.width)) | |
| embedded = embedded.flatten(2).transpose(1, 2).reshape(batch, faces * self.patch_rows * self.patch_cols, -1) | |
| return embedded | |
| def forward(self, noisy: Tensor, seeds: Tensor, climate: Optional[Tensor] = None, diffusion_time: Optional[Tensor] = None) -> Tensor: | |
| self._check_inputs(noisy, seeds, climate) | |
| batch = noisy.shape[0] | |
| if climate is None: | |
| climate = torch.zeros_like(noisy) | |
| snapshots = torch.cat((noisy[:, None], seeds, climate[:, None]), dim=1) | |
| sequence = torch.stack([self._embed_snapshot(snapshots[:, index]) for index in range(snapshots.shape[1])], dim=1) | |
| sequence = sequence[:, :, None] + self.position_embedding + self.field_embedding | |
| if diffusion_time is None: | |
| diffusion_time = torch.zeros(batch, device=noisy.device, dtype=noisy.dtype) | |
| time = self.time_projection(_fourier_embedding(diffusion_time, sequence.shape[-1])).to(sequence.dtype) | |
| sequence[:, 0] = sequence[:, 0] + time[:, None, None] | |
| sequence = sequence.expand(-1, -1, self.channels, -1, -1) + self.snapshot_embedding[:, : sequence.shape[1]] | |
| shape = sequence.shape | |
| sequence = sequence.reshape(batch * shape[1] * shape[2], shape[3], shape[4]) | |
| for block in self.spatial_transformer: | |
| sequence = block(sequence) | |
| sequence = sequence.reshape(batch * shape[1] * shape[3], shape[2], shape[4]) | |
| for block in self.field_transformer: | |
| sequence = block(sequence) | |
| sequence = sequence.reshape(batch * shape[2] * shape[3], shape[1], shape[4]) | |
| for block in self.sequence_transformer: | |
| sequence = block(sequence) | |
| sequence = sequence.reshape(batch, shape[1], shape[2], shape[3], shape[4])[:, 0] | |
| patches = self.output_projection(sequence).reshape(batch, self.channels, self.faces, self.patch_rows, self.patch_cols, self.patch_size, self.patch_size) | |
| return patches.permute(0, 1, 2, 3, 5, 4, 6).reshape(batch, self.channels, self.faces, self.height, self.width) | |
| def sigma(self, diffusion_time: Tensor) -> Tensor: | |
| return self.sigma_min * (self.sigma_max / self.sigma_min) ** diffusion_time | |
| def denoising_loss( | |
| self, | |
| clean: Tensor, | |
| seeds: Tensor, | |
| climate: Optional[Tensor] = None, | |
| diffusion_time: Optional[Tensor] = None, | |
| noise: Optional[Tensor] = None, | |
| ) -> Tensor: | |
| if diffusion_time is None: | |
| diffusion_time = torch.rand(clean.shape[0], device=clean.device, dtype=clean.dtype) | |
| if noise is None: | |
| noise = torch.randn_like(clean) | |
| sigma = self.sigma(diffusion_time).view(-1, 1, 1, 1, 1) | |
| noisy = clean + sigma * noise | |
| model_input = noisy / torch.sqrt(1.0 + sigma.square()) | |
| prediction = self(model_input, seeds, climate, diffusion_time) | |
| return ((prediction - noise) ** 2).flatten(1).mean() | |
| def sample( | |
| self, | |
| seeds: Tensor, | |
| climate: Optional[Tensor] = None, | |
| members: int = 1, | |
| steps: int = 64, | |
| member_batch_size: Optional[int] = None, | |
| ) -> Tensor: | |
| if members < 1 or steps < 1: | |
| raise ValueError("members and steps must be positive") | |
| chunk_size = min(member_batch_size or members, members) | |
| generated = [] | |
| schedule = torch.linspace(1.0, 0.0, steps + 1, device=seeds.device, dtype=seeds.dtype) | |
| sigma_schedule = self.sigma(schedule) | |
| for start in range(0, members, chunk_size): | |
| current_members = min(chunk_size, members - start) | |
| expanded_seeds = seeds.repeat_interleave(current_members, dim=0) | |
| expanded_climate = None if climate is None else climate.repeat_interleave(current_members, dim=0) | |
| sample = torch.randn_like(expanded_seeds[:, 0]) * sigma_schedule[0] | |
| for index, current in enumerate(schedule[:-1]): | |
| current_time = torch.full((sample.shape[0],), current, device=sample.device, dtype=sample.dtype) | |
| sigma = sigma_schedule[index] | |
| model_input = sample / torch.sqrt(1.0 + sigma.square()) | |
| predicted_noise = self(model_input, expanded_seeds, expanded_climate, current_time) | |
| sample = sample + (sigma_schedule[index + 1] - sigma) * predicted_noise | |
| generated.append( | |
| sample.reshape(seeds.shape[0], current_members, self.channels, self.faces, self.height, self.width) | |
| ) | |
| return torch.cat(generated, dim=1) | |
| SEEDSModel = SEEDS | |