| """ACE 40-channel input, 44-channel output and autoregressive rollout.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import asdict, dataclass |
| from typing import Callable |
|
|
| import torch |
| from torch import nn |
|
|
| from ACE.model.variables import ( |
| FORCING_CHANNELS, |
| INPUT_CHANNELS, |
| OUTPUT_CHANNELS, |
| PROGNOSTIC_CHANNELS, |
| split_output, |
| validate_channels, |
| ) |
| from ACE.model.sfno import SFNOAdapter, SFNOConfig |
|
|
|
|
| @dataclass |
| class ACEModelConfig: |
| nlat: int = 180 |
| nlon: int = 360 |
| input_channels: int = len(INPUT_CHANNELS) |
| output_channels: int = len(OUTPUT_CHANNELS) |
| prognostic_channels: int = len(PROGNOSTIC_CHANNELS) |
| forcing_channels: int = len(FORCING_CHANNELS) |
| 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 |
|
|
| def to_dict(self) -> dict: |
| return asdict(self) |
|
|
|
|
| class ACEModel(nn.Module): |
| def __init__(self, config: ACEModelConfig | None = None) -> None: |
| super().__init__() |
| self.config = config or ACEModelConfig() |
| if self.config.input_channels != len(INPUT_CHANNELS) or self.config.output_channels != len(OUTPUT_CHANNELS): |
| raise ValueError("ACE channel contract must remain 40 input and 44 output channels") |
| sfno_config = SFNOConfig( |
| nlat=self.config.nlat, |
| nlon=self.config.nlon, |
| in_channels=self.config.input_channels, |
| out_channels=self.config.output_channels, |
| embed_dim=self.config.embed_dim, |
| num_layers=self.config.num_layers, |
| filter_type=self.config.filter_type, |
| operator_type=self.config.operator_type, |
| scale_factor=self.config.scale_factor, |
| spectral_layers=self.config.spectral_layers, |
| grid=self.config.grid, |
| grid_internal=self.config.grid_internal, |
| mlp_ratio=self.config.mlp_ratio, |
| fallback=self.config.fallback, |
| ) |
| self.sfno = SFNOAdapter(sfno_config) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| validate_channels(x, self.config.input_channels, "ACE input") |
| return self.sfno(x) |
|
|
| def step(self, prognostic: torch.Tensor, forcing: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: |
| validate_channels(prognostic, self.config.prognostic_channels, "prognostic state") |
| validate_channels(forcing, self.config.forcing_channels, "forcing") |
| predicted = self.forward(torch.cat([prognostic, forcing], dim=1)) |
| return split_output(predicted) |
|
|
| @torch.no_grad() |
| def rollout( |
| self, |
| initial_prognostic: torch.Tensor, |
| forcings: torch.Tensor | Callable[[int, torch.Tensor], torch.Tensor], |
| steps: int | None = None, |
| ) -> torch.Tensor: |
| """Return predictions with shape ``[B,T,44,H,W]``. |
| |
| ``forcings`` is either `[B,T,6,H,W]` or a callable receiving |
| `(step, current_prognostic)` and returning `[B,6,H,W]`. |
| """ |
| validate_channels(initial_prognostic, self.config.prognostic_channels, "initial prognostic") |
| if callable(forcings): |
| if steps is None or steps < 1: |
| raise ValueError("steps is required for callable forcing") |
| forcing_steps = steps |
| else: |
| if forcings.ndim != 5 or forcings.shape[2] != self.config.forcing_channels: |
| raise ValueError("tensor forcings must have shape [B,T,6,H,W]") |
| forcing_steps = forcings.shape[1] if steps is None else min(steps, forcings.shape[1]) |
| state = initial_prognostic |
| outputs = [] |
| for step in range(forcing_steps): |
| forcing = forcings[:, step] if not callable(forcings) else forcings(step, state) |
| predicted_state, diagnostics = self.step(state, forcing) |
| outputs.append(torch.cat([predicted_state, diagnostics], dim=1)) |
| state = predicted_state |
| return torch.stack(outputs, dim=1) |
|
|