"""GLONET reference architecture based on the public paper description.""" import torch from torch import nn class SpectralConv2d(nn.Module): def __init__(self, channels, modes): super().__init__() self.modes_y, self.modes_x = modes self.weight = nn.Parameter(torch.randn(channels, channels, self.modes_y, self.modes_x, 2) * 0.02) def forward(self, x): height, width = x.shape[-2:] spectrum = torch.fft.rfft2(x, norm="ortho") out = torch.zeros_like(spectrum) modes_y = min(self.modes_y, height) modes_x = min(self.modes_x, spectrum.shape[-1]) weight = torch.view_as_complex(self.weight[:, :, :modes_y, :modes_x].contiguous()) out[:, :, :modes_y, :modes_x] = torch.einsum( "bixy,ioxy->boxy", spectrum[:, :, :modes_y, :modes_x], weight ) return torch.fft.irfft2(out, s=(height, width), norm="ortho") class SpectralBlock(nn.Module): def __init__(self, channels, modes): super().__init__() self.spectral = SpectralConv2d(channels, modes) self.pointwise = nn.Conv2d(channels, channels, 1) self.activation = nn.GELU() def forward(self, x): return self.activation(self.spectral(x) + self.pointwise(x)) class CNNBranch(nn.Module): def __init__(self, channels): super().__init__() self.net = nn.Sequential( nn.Conv2d(channels, channels, 3, padding=1), nn.GELU(), nn.Conv2d(channels, channels, 3, padding=1), nn.GELU(), ) def forward(self, x): return self.net(x) class GLONET(nn.Module): """Two-day to one-day global ocean forecast reference model. The paper does not publish a complete layer configuration, so all sizing choices remain explicit constructor parameters rather than hidden claims. """ def __init__(self, in_channels, out_channels=None, hidden_channels=32, modes=(6, 8), layers=4): super().__init__() out_channels = out_channels or in_channels self.input_projection = nn.Conv2d(in_channels, hidden_channels, 1) self.fno = nn.Sequential(*[SpectralBlock(hidden_channels, modes) for _ in range(layers)]) self.cnn = CNNBranch(hidden_channels) self.output_projection = nn.Sequential( nn.Conv2d(hidden_channels * 2, hidden_channels, 1), nn.GELU(), nn.Conv2d(hidden_channels, out_channels, 1), ) def forward(self, x): if x.ndim == 5: x = x.flatten(1, 2) features = self.input_projection(x) return self.output_projection(torch.cat((self.fno(features), self.cnn(features)), dim=1))