SEEDS / model /seeds.py
yzt15806542928's picture
Upload folder using huggingface_hub
1ca0208 verified
Raw
History Blame Contribute Delete
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()
@torch.no_grad()
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