| from __future__ import annotations |
|
|
| from collections.abc import Sequence |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| def _activation(name: str) -> nn.Module: |
| activations = { |
| "gelu": nn.GELU, |
| "relu": nn.ReLU, |
| "silu": nn.SiLU, |
| "tanh": nn.Tanh, |
| } |
| try: |
| return activations[name.lower()]() |
| except KeyError as error: |
| raise ValueError(f"Unsupported activation: {name}") from error |
|
|
|
|
| class MLP(nn.Module): |
| def __init__(self, input_dim: int, hidden_dim: int, output_dim: int, activation: str) -> None: |
| super().__init__() |
| self.layers = nn.Sequential( |
| nn.Linear(input_dim, hidden_dim), |
| _activation(activation), |
| nn.Linear(hidden_dim, output_dim), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| return self.layers(x) |
|
|
|
|
| class SpectralConv2d(nn.Module): |
| """Two-dimensional Fourier convolution over the lowest frequency modes.""" |
|
|
| def __init__(self, in_channels: int, out_channels: int, modes1: int, modes2: int) -> None: |
| super().__init__() |
| self.in_channels = in_channels |
| self.out_channels = out_channels |
| self.modes1 = modes1 |
| self.modes2 = modes2 |
| scale = 1.0 / (in_channels * out_channels) |
| shape = (in_channels, out_channels, modes1, modes2) |
| self.weights1 = nn.Parameter(scale * torch.rand(*shape, dtype=torch.cfloat)) |
| self.weights2 = nn.Parameter(scale * torch.rand(*shape, dtype=torch.cfloat)) |
|
|
| @staticmethod |
| def _multiply(inputs: torch.Tensor, weights: torch.Tensor) -> torch.Tensor: |
| return torch.einsum("bixy,ioxy->boxy", inputs, weights) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| batch_size, _, height, width = x.shape |
| x_ft = torch.fft.rfft2(x) |
| out_ft = torch.zeros( |
| batch_size, |
| self.out_channels, |
| height, |
| width // 2 + 1, |
| dtype=torch.cfloat, |
| device=x.device, |
| ) |
| modes1 = min(self.modes1, max(1, height // 2)) |
| modes2 = min(self.modes2, width // 2 + 1) |
| out_ft[:, :, :modes1, :modes2] = self._multiply( |
| x_ft[:, :, :modes1, :modes2], |
| self.weights1[:, :, :modes1, :modes2], |
| ) |
| out_ft[:, :, -modes1:, :modes2] = self._multiply( |
| x_ft[:, :, -modes1:, :modes2], |
| self.weights2[:, :, :modes1, :modes2], |
| ) |
| return torch.fft.irfft2(out_ft, s=(height, width)) |
|
|
|
|
| def _normalization(normtype: str, channels: int) -> nn.Module: |
| normalized = normtype.lower() |
| if normalized == "bn": |
| return nn.BatchNorm2d(channels) |
| if normalized == "in": |
| return nn.InstanceNorm2d(channels, affine=True) |
| if normalized in {"none", "identity"}: |
| return nn.Identity() |
| raise ValueError(f"Unsupported normtype: {normtype}") |
|
|
|
|
| class DoubleConv2d(nn.Module): |
| def __init__( |
| self, |
| in_channels: int, |
| out_channels: int, |
| mid_channels: int | None = None, |
| normtype: str = "in", |
| ) -> None: |
| super().__init__() |
| mid_channels = mid_channels or out_channels |
| self.layers = nn.Sequential( |
| nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False), |
| _normalization(normtype, mid_channels), |
| nn.ReLU(inplace=True), |
| nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False), |
| _normalization(normtype, out_channels), |
| nn.ReLU(inplace=True), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| return self.layers(x) |
|
|
|
|
| class Down2d(nn.Module): |
| def __init__(self, in_channels: int, out_channels: int, normtype: str) -> None: |
| super().__init__() |
| self.layers = nn.Sequential( |
| nn.MaxPool2d(2), |
| DoubleConv2d(in_channels, out_channels, normtype=normtype), |
| ) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| return self.layers(x) |
|
|
|
|
| class Up2d(nn.Module): |
| def __init__( |
| self, |
| in_channels: int, |
| out_channels: int, |
| bilinear: bool, |
| normtype: str, |
| ) -> None: |
| super().__init__() |
| self.bilinear = bilinear |
| if bilinear: |
| self.up = None |
| mid_channels = in_channels // 2 |
| else: |
| self.up = nn.ConvTranspose2d( |
| in_channels, |
| in_channels // 2, |
| kernel_size=2, |
| stride=2, |
| ) |
| mid_channels = None |
| self.conv = DoubleConv2d( |
| in_channels, |
| out_channels, |
| mid_channels=mid_channels, |
| normtype=normtype, |
| ) |
|
|
| def forward(self, x: torch.Tensor, skip: torch.Tensor) -> torch.Tensor: |
| if self.bilinear: |
| x = F.interpolate(x, size=skip.shape[-2:], mode="bilinear", align_corners=True) |
| else: |
| x = self.up(x) |
| if x.shape[-2:] != skip.shape[-2:]: |
| x = F.interpolate(x, size=skip.shape[-2:], mode="nearest") |
| return self.conv(torch.cat((skip, x), dim=1)) |
|
|
|
|
| class UNO(nn.Module): |
| """Two-dimensional U-shaped neural operator for regular CFD grids. |
| |
| ``pos`` has shape ``[B, H*W, 2]`` and ``fx`` has shape |
| ``[B, H*W, in_dim]``. The returned tensor has shape |
| ``[B, H*W, out_dim]``. |
| """ |
|
|
| def __init__( |
| self, |
| in_dim: int, |
| out_dim: int, |
| spatial_shape: Sequence[int], |
| hidden_dim: int = 64, |
| modes: int = 12, |
| space_dim: int = 2, |
| include_pos: bool = True, |
| normtype: str = "in", |
| bilinear: bool = True, |
| activation: str = "gelu", |
| pad_to_multiple: int = 16, |
| ) -> None: |
| super().__init__() |
| self.in_dim = int(in_dim) |
| self.out_dim = int(out_dim) |
| self.spatial_shape = tuple(int(value) for value in spatial_shape) |
| self.hidden_dim = int(hidden_dim) |
| self.modes = int(modes) |
| self.space_dim = int(space_dim) |
| self.include_pos = bool(include_pos) |
| self.pad_to_multiple = int(pad_to_multiple) |
| if len(self.spatial_shape) != 2: |
| raise ValueError(f"This standalone UNO supports 2D grids, got {self.spatial_shape}") |
| if self.space_dim != 2: |
| raise ValueError("space_dim must be 2 for the standalone 2D UNO") |
| if min(self.hidden_dim, self.modes, self.pad_to_multiple) < 1: |
| raise ValueError("hidden_dim, modes and pad_to_multiple must be positive") |
|
|
| input_dim = self.in_dim + (self.space_dim if self.include_pos else 0) |
| self.preprocess = MLP(input_dim, self.hidden_dim * 2, self.hidden_dim, activation) |
| factor = 2 if bilinear else 1 |
|
|
| self.inc = DoubleConv2d(self.hidden_dim, self.hidden_dim, normtype=normtype) |
| self.down1 = Down2d(self.hidden_dim, self.hidden_dim * 2, normtype) |
| self.down2 = Down2d(self.hidden_dim * 2, self.hidden_dim * 4, normtype) |
| self.down3 = Down2d(self.hidden_dim * 4, self.hidden_dim * 8, normtype) |
| self.down4 = Down2d(self.hidden_dim * 8, self.hidden_dim * 16 // factor, normtype) |
|
|
| self.up1 = Up2d(self.hidden_dim * 16, self.hidden_dim * 8 // factor, bilinear, normtype) |
| self.up2 = Up2d(self.hidden_dim * 8, self.hidden_dim * 4 // factor, bilinear, normtype) |
| self.up3 = Up2d(self.hidden_dim * 4, self.hidden_dim * 2 // factor, bilinear, normtype) |
| self.up4 = Up2d(self.hidden_dim * 2, self.hidden_dim, bilinear, normtype) |
| self.outc = nn.Conv2d(self.hidden_dim, self.hidden_dim, kernel_size=1) |
|
|
| augmented = self._augmented_shape(self.spatial_shape) |
| self.padding = tuple(target - source for target, source in zip(augmented, self.spatial_shape)) |
| down_channels = [ |
| self.hidden_dim, |
| self.hidden_dim * 2, |
| self.hidden_dim * 4, |
| self.hidden_dim * 8, |
| self.hidden_dim * 16 // factor, |
| ] |
| up_channels = [ |
| self.hidden_dim * 16 // factor, |
| self.hidden_dim * 8 // factor, |
| self.hidden_dim * 4 // factor, |
| self.hidden_dim * 2 // factor, |
| self.hidden_dim, |
| ] |
| divisors = [2, 4, 8, 16, 32] |
| self.spectral_down = nn.ModuleList( |
| self._spectral(channels, augmented, divisor) |
| for channels, divisor in zip(down_channels, divisors) |
| ) |
| self.pointwise_down = nn.ModuleList( |
| nn.Conv2d(channels, channels, kernel_size=1) for channels in down_channels |
| ) |
| self.spectral_up = nn.ModuleList( |
| self._spectral(channels, augmented, divisor) |
| for channels, divisor in zip(up_channels, reversed(divisors)) |
| ) |
| self.pointwise_up = nn.ModuleList( |
| nn.Conv2d(channels, channels, kernel_size=1) for channels in up_channels |
| ) |
| self.fc1 = nn.Linear(self.hidden_dim, self.hidden_dim * 2) |
| self.fc2 = nn.Linear(self.hidden_dim * 2, self.out_dim) |
|
|
| def _augmented_shape(self, shape: Sequence[int]) -> tuple[int, int]: |
| return tuple( |
| size + (self.pad_to_multiple - size % self.pad_to_multiple) % self.pad_to_multiple |
| for size in shape |
| ) |
|
|
| def _spectral( |
| self, |
| channels: int, |
| augmented_shape: Sequence[int], |
| divisor: int, |
| ) -> SpectralConv2d: |
| modes = [ |
| max(1, min(self.modes, max(1, size // divisor))) |
| for size in augmented_shape |
| ] |
| return SpectralConv2d(channels, channels, modes[0], modes[1]) |
|
|
| @staticmethod |
| def _operator( |
| x: torch.Tensor, |
| spectral: nn.Module, |
| pointwise: nn.Module, |
| ) -> torch.Tensor: |
| return F.gelu(spectral(x) + pointwise(x)) |
|
|
| def _pad(self, x: torch.Tensor) -> torch.Tensor: |
| pad_height, pad_width = self.padding |
| return F.pad(x, (0, pad_width, 0, pad_height)) if any(self.padding) else x |
|
|
| def _unpad(self, x: torch.Tensor) -> torch.Tensor: |
| height, width = self.spatial_shape |
| return x[..., :height, :width] |
|
|
| def forward(self, pos: torch.Tensor, fx: torch.Tensor | None = None) -> torch.Tensor: |
| batch_size, point_count, coordinates = pos.shape |
| expected_points = self.spatial_shape[0] * self.spatial_shape[1] |
| if point_count != expected_points or coordinates != self.space_dim: |
| raise ValueError( |
| f"Expected pos [B, {expected_points}, {self.space_dim}], got {tuple(pos.shape)}" |
| ) |
| if fx is None: |
| if self.in_dim: |
| raise ValueError("fx is required when in_dim > 0") |
| features = pos if self.include_pos else pos.new_empty(batch_size, point_count, 0) |
| else: |
| if fx.shape != (batch_size, point_count, self.in_dim): |
| raise ValueError( |
| f"Expected fx [B, {point_count}, {self.in_dim}], got {tuple(fx.shape)}" |
| ) |
| features = torch.cat((pos, fx), dim=-1) if self.include_pos else fx |
|
|
| x = self.preprocess(features) |
| x = x.permute(0, 2, 1).reshape(batch_size, self.hidden_dim, *self.spatial_shape) |
| x = self._pad(x) |
|
|
| x1 = self._operator(self.inc(x), self.spectral_down[0], self.pointwise_down[0]) |
| x2 = self._operator(self.down1(x1), self.spectral_down[1], self.pointwise_down[1]) |
| x3 = self._operator(self.down2(x2), self.spectral_down[2], self.pointwise_down[2]) |
| x4 = self._operator(self.down3(x3), self.spectral_down[3], self.pointwise_down[3]) |
| x5 = self._operator(self.down4(x4), self.spectral_down[4], self.pointwise_down[4]) |
| x = self._operator(x5, self.spectral_up[0], self.pointwise_up[0]) |
| x = self._operator(self.up1(x, x4), self.spectral_up[1], self.pointwise_up[1]) |
| x = self._operator(self.up2(x, x3), self.spectral_up[2], self.pointwise_up[2]) |
| x = self._operator(self.up3(x, x2), self.spectral_up[3], self.pointwise_up[3]) |
| x = self._operator(self.up4(x, x1), self.spectral_up[4], self.pointwise_up[4]) |
| x = self._unpad(self.outc(x)) |
|
|
| x = x.reshape(batch_size, self.hidden_dim, -1).permute(0, 2, 1) |
| return self.fc2(F.gelu(self.fc1(x))) |
|
|