| """Periodic one-dimensional CNN for mass-aware data-assimilation correction.""" |
|
|
| from __future__ import annotations |
|
|
| import torch |
| from torch import nn |
| from torch.nn import functional as F |
|
|
|
|
| class PeriodicConv1d(nn.Module): |
| """Conv1d with explicit circular padding and unchanged spatial length.""" |
|
|
| def __init__(self, in_channels: int, out_channels: int, kernel_size: int): |
| super().__init__() |
| if kernel_size % 2 != 1: |
| raise ValueError("kernel_size must be odd") |
| self.pad = kernel_size // 2 |
| self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=0) |
|
|
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: |
| return self.conv(F.pad(inputs, (self.pad, self.pad), mode="circular")) |
|
|
|
|
| class MassConservingCNN(nn.Module): |
| """Four hidden SELU convolutions followed by the u/h/r output layer.""" |
|
|
| def __init__(self, input_channels: int = 4, hidden_channels: int = 32, |
| hidden_layers: int = 4, kernel_size: int = 3): |
| super().__init__() |
| if input_channels != 4 or hidden_layers != 4 or kernel_size != 3: |
| raise ValueError("paper architecture requires 4 inputs, 4 hidden layers, kernel size 3") |
| layers = [] |
| channels = input_channels |
| for _ in range(hidden_layers): |
| layers.extend((PeriodicConv1d(channels, hidden_channels, kernel_size), nn.SELU())) |
| channels = hidden_channels |
| self.hidden = nn.Sequential(*layers) |
| self.output = PeriodicConv1d(hidden_channels, 3, kernel_size) |
|
|
| @property |
| def influence_radius(self) -> int: |
| return 5 |
|
|
| def forward(self, inputs: torch.Tensor) -> torch.Tensor: |
| if inputs.ndim != 3 or inputs.shape[1] != 4 or inputs.shape[2] != 250: |
| raise ValueError(f"expected float tensor [B,4,250], got {tuple(inputs.shape)}") |
| if not inputs.is_floating_point(): |
| raise TypeError("inputs must have a floating-point dtype") |
| raw = self.output(self.hidden(inputs)) |
| return torch.cat((raw[:, :2], F.relu(raw[:, 2:3])), dim=1) |
|
|