"""ACE's spherical Fourier neural operator implementation. The paper uses SFNO with Gauss-Legendre transforms. This module builds the implementation distributed by ``torch_harmonics``. All trainable layers use random initialization, so a checkpoint can be created from scratch. """ from __future__ import annotations from dataclasses import dataclass import torch from torch import nn try: from torch_harmonics.examples.models.sfno import SphericalFourierNeuralOperator except ImportError as exc: raise ImportError( "ACE requires torch_harmonics. Activate the configured training " "environment or install torch_harmonics before running ACE." ) from exc @dataclass class SFNOConfig: nlat: int = 180 nlon: int = 360 in_channels: int = 40 out_channels: int = 44 embed_dim: int = 256 num_layers: int = 8 filter_type: str = "linear" operator_type: str = "dhconv" scale_factor: int = 1 spectral_layers: int = 3 grid: str = "legendre-gauss" grid_internal: str = "legendre-gauss" mlp_ratio: float = 2.0 fallback: bool = False class SFNOAdapter(nn.Module): """Construct the paper-compatible spherical SFNO for one grid size.""" def __init__(self, config: SFNOConfig) -> None: super().__init__() if config.fallback: raise ValueError("The planar fallback was removed; use the spherical SFNO implementation") if config.nlat < 5 or config.nlon < 8: raise ValueError("SFNO grid must be at least 5 x 8") self.config = config self.model = SphericalFourierNeuralOperator( img_size=(config.nlat, config.nlon), grid=config.grid, grid_internal=config.grid_internal, scale_factor=config.scale_factor, in_chans=config.in_channels, out_chans=config.out_channels, embed_dim=config.embed_dim, num_layers=config.num_layers, use_mlp=True, mlp_ratio=config.mlp_ratio, normalization_layer="none", residual_prediction=False, pos_embed="none", bias=True, ) def forward(self, x: torch.Tensor) -> torch.Tensor: if x.ndim != 4: raise ValueError(f"SFNO input must be [B,C,H,W], got {tuple(x.shape)}") expected = (self.config.in_channels, self.config.nlat, self.config.nlon) if tuple(x.shape[1:]) != expected: raise ValueError(f"SFNO expects trailing shape {expected}, got {tuple(x.shape[1:])}") return self.model(x.float())