Surya / model /surya.py
zhangrenchao's picture
Upload Surya model package
a13f4b9 verified
Raw
History Blame Contribute Delete
7.02 kB
"""Self-contained, paper-aligned Surya forecasting model."""
import torch
from torch import nn
from torch.nn import functional as F
CHANNEL_NAMES = ("AIA_94", "AIA_131", "AIA_171", "AIA_193", "AIA_211", "AIA_304",
"AIA_335", "AIA_1600", "HMI_magnetogram", "HMI_continuum",
"HMI_doppler", "HMI_vector_x", "HMI_vector_y")
def signum_log(x):
"""Compress signed solar products while retaining their sign."""
return torch.sign(x) * torch.log1p(torch.abs(x))
class SpectralGating(nn.Module):
def __init__(self, grid, dim):
super().__init__()
self.grid = grid
self.weight = nn.Parameter(torch.randn(grid, grid // 2 + 1, dim, 2) * 0.02)
self.norm = nn.LayerNorm(dim)
self.mlp = nn.Sequential(nn.Linear(dim, dim * 2), nn.GELU(), nn.Linear(dim * 2, dim))
def forward(self, tokens):
residual = tokens
values = self.norm(tokens).reshape(tokens.shape[0], self.grid, self.grid, -1).float()
spectrum = torch.fft.rfft2(values, dim=(1, 2), norm="ortho")
spectrum = spectrum * torch.view_as_complex(self.weight.float())
values = torch.fft.irfft2(spectrum, s=(self.grid, self.grid), dim=(1, 2), norm="ortho")
return residual + self.mlp(values.reshape_as(tokens).to(tokens.dtype))
class LongShortAttention(nn.Module):
def __init__(self, grid, dim, heads, window, global_tokens):
super().__init__()
self.grid, self.window = grid, window
self.norm = nn.LayerNorm(dim)
self.local = nn.MultiheadAttention(dim, heads, batch_first=True)
self.global_attn = nn.MultiheadAttention(dim, heads, batch_first=True)
self.projection = nn.Linear(dim * 2, dim)
self.score = nn.Linear(dim, global_tokens)
self.global_norm = nn.LayerNorm(dim)
self.output_norm = nn.LayerNorm(dim)
self.mlp_norm = nn.LayerNorm(dim)
self.mlp = nn.Sequential(nn.Linear(dim, dim * 4), nn.GELU(), nn.Linear(dim * 4, dim))
def forward(self, tokens):
residual = tokens
values = self.norm(tokens)
b, _, d = values.shape
grid = values.reshape(b, self.grid, self.grid, d)
w = self.window
# Unfold with padding creates overlapping neighborhoods at every token.
padded = F.pad(grid.permute(0, 3, 1, 2), (w // 2, w // 2, w // 2, w // 2), mode="replicate")
neighborhoods = F.unfold(padded, kernel_size=w, padding=0).transpose(1, 2)
windows = neighborhoods.reshape(b, self.grid * self.grid, d, w * w)
windows = windows.permute(0, 1, 3, 2).reshape(-1, w * w, d)
local = self.local(windows, windows, windows, need_weights=False)[0]
local = local[:, (w * w) // 2].reshape(b, self.grid * self.grid, d)
mixing = self.score(values).transpose(1, 2).softmax(-1)
compressed = self.global_norm(mixing @ values)
global_context = self.global_attn(values, compressed, compressed, need_weights=False)[0]
tokens = residual + self.projection(torch.cat((local, global_context), dim=-1))
tokens = self.output_norm(tokens)
return tokens + self.mlp(self.mlp_norm(tokens))
class Surya(nn.Module):
def __init__(self, image_size=32, patch_size=4, channels=13, input_steps=2,
embed_dim=64, depth=4, spectral_blocks=1, num_heads=4,
window_size=2, global_tokens=4):
super().__init__()
if image_size % patch_size:
raise ValueError("image_size must be divisible by patch_size")
if spectral_blocks >= depth:
raise ValueError("spectral_blocks must be smaller than depth")
if embed_dim % num_heads:
raise ValueError("embed_dim must be divisible by num_heads")
self.image_size, self.patch_size = image_size, patch_size
self.channels, self.input_steps = channels, input_steps
self.grid = image_size // patch_size
if channels != 13:
raise ValueError("Surya requires the 13 SDO AIA/HMI channels")
patch_dim = channels * patch_size ** 2
self.patch_embed = nn.Linear(patch_dim * 2, embed_dim)
self.temporal_embed = nn.Parameter(torch.zeros(1, input_steps, 1, embed_dim))
self.position = nn.Parameter(torch.zeros(1, self.grid ** 2, embed_dim))
blocks = [SpectralGating(self.grid, embed_dim) for _ in range(spectral_blocks)]
blocks += [LongShortAttention(self.grid, embed_dim, num_heads, window_size, global_tokens)
for _ in range(depth - spectral_blocks)]
self.blocks = nn.ModuleList(blocks)
self.norm = nn.LayerNorm(embed_dim)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(embed_dim, embed_dim // 2, patch_size, stride=patch_size),
nn.GELU(), nn.Conv2d(embed_dim // 2, channels, 3, padding=1))
nn.init.normal_(self.position, std=0.02)
nn.init.zeros_(self.decoder[-1].weight)
nn.init.zeros_(self.decoder[-1].bias)
def tokenize(self, frames):
b, t, c, h, w = frames.shape
if (t, c, h, w) != (self.input_steps, self.channels, self.image_size, self.image_size):
raise ValueError("Expected BTCHW input matching configured dimensions")
p = self.patch_size
current = frames[:, -1]
delta = current - frames[:, -2]
values = torch.stack((current, delta), dim=1).reshape(b, 2 * c, h // p, p, w // p, p)
values = values.permute(0, 2, 4, 1, 3, 5).reshape(b, self.grid ** 2, -1)
return self.patch_embed(values) + self.position + self.temporal_embed[:, -1]
def _predict(self, frames):
tokens = self.tokenize(frames)
for block in self.blocks:
tokens = block(tokens)
tokens = self.norm(tokens)
features = tokens.mean(1)
grid = tokens.transpose(1, 2).reshape(tokens.shape[0], -1, self.grid, self.grid)
prediction = frames[:, -1] + self.decoder(grid)
if not torch.isfinite(prediction).all():
raise FloatingPointError("Surya produced a non-finite prediction")
return {"prediction": prediction, "features": features}
def forward(self, frames, steps=None):
if steps is None:
return self._predict(frames)
if steps < 1:
raise ValueError("steps must be positive")
history, predictions = frames, []
for _ in range(steps):
prediction = self._predict(history)["prediction"]
predictions.append(prediction)
history = torch.cat((history[:, 1:], prediction[:, None]), dim=1)
return torch.stack(predictions, dim=1)
def rollout(self, frames, steps):
return self(frames, steps=steps)
def rollout_loss(self, frames, targets):
predictions = self.rollout(frames, targets.shape[1])
step_losses = (predictions - targets).square().mean(dim=(0, 2, 3, 4))
return {"loss": step_losses.mean(), "step_losses": step_losses,
"predictions": predictions}