KNO / model /kno.py
OneScience's picture
Upload folder using huggingface_hub
da0023d verified
Raw
History Blame Contribute Delete
9.28 kB
from __future__ import annotations
import torch
import torch.nn as nn
class EncoderMLP(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Linear(t_len, op_size)
def forward(self, x):
return self.layer(x)
class DecoderMLP(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Linear(op_size, t_len)
def forward(self, x):
return self.layer(x)
class EncoderConv1D(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Conv1d(t_len, op_size, 1)
def forward(self, x):
x = x.permute(0, 2, 1)
x = self.layer(x)
return x.permute(0, 2, 1)
class DecoderConv1D(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Conv1d(op_size, t_len, 1)
def forward(self, x):
x = x.permute(0, 2, 1)
x = self.layer(x)
return x.permute(0, 2, 1)
class EncoderConv2D(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Conv2d(t_len, op_size, 1)
def forward(self, x):
x = x.permute(0, 3, 1, 2)
x = self.layer(x)
return x.permute(0, 2, 3, 1)
class DecoderConv2D(nn.Module):
def __init__(self, t_len: int, op_size: int):
super().__init__()
self.layer = nn.Conv2d(op_size, t_len, 1)
def forward(self, x):
x = x.permute(0, 3, 1, 2)
x = self.layer(x)
return x.permute(0, 2, 3, 1)
class KoopmanOperator1D(nn.Module):
def __init__(self, op_size: int, modes_x: int = 16):
super().__init__()
self.op_size = int(op_size)
self.modes_x = int(modes_x)
if min(self.op_size, self.modes_x) < 1:
raise ValueError("op_size and modes_x must be positive")
scale = 1.0 / (self.op_size * self.op_size)
self.koopman_matrix = nn.Parameter(
scale * torch.rand(self.op_size, self.op_size, self.modes_x, dtype=torch.cfloat)
)
def time_marching(self, input_tensor, weights):
return torch.einsum("btx,tfx->bfx", input_tensor, weights)
def forward(self, x):
x_ft = torch.fft.rfft(x)
out_ft = torch.zeros(x_ft.shape, dtype=torch.cfloat, device=x.device)
modes_x = min(self.modes_x, x_ft.shape[-1])
out_ft[:, :, :modes_x] = self.time_marching(
x_ft[:, :, :modes_x],
self.koopman_matrix[:, :, :modes_x],
)
return torch.fft.irfft(out_ft, n=x.size(-1))
class KNO1D(nn.Module):
def __init__(
self,
encoder: nn.Module,
decoder: nn.Module,
op_size: int,
modes_x: int = 16,
decompose: int = 4,
linear_type: bool = True,
normalization: bool = False,
):
super().__init__()
self.op_size = int(op_size)
self.decompose = int(decompose)
if self.decompose < 1:
raise ValueError("decompose must be positive")
self.enc = encoder
self.dec = decoder
self.koopman_layer = KoopmanOperator1D(self.op_size, modes_x=modes_x)
self.w0 = nn.Conv1d(self.op_size, self.op_size, 1)
self.linear_type = bool(linear_type)
self.normalization = bool(normalization)
if self.normalization:
self.norm_layer = nn.BatchNorm1d(self.op_size)
def forward(self, x):
x_reconstruct = self.dec(torch.tanh(self.enc(x)))
x = torch.tanh(self.enc(x)).permute(0, 2, 1)
x_w = x
for _ in range(self.decompose):
x1 = self.koopman_layer(x)
x = x + x1 if self.linear_type else torch.tanh(x + x1)
shortcut = self.w0(x_w)
if self.normalization:
shortcut = self.norm_layer(shortcut)
x = torch.tanh(shortcut + x).permute(0, 2, 1)
return self.dec(x), x_reconstruct
class KoopmanOperator2D(nn.Module):
def __init__(self, op_size: int, modes_x: int = 12, modes_y: int = 12):
super().__init__()
self.op_size = int(op_size)
self.modes_x = int(modes_x)
self.modes_y = int(modes_y)
if min(self.op_size, self.modes_x, self.modes_y) < 1:
raise ValueError("op_size, modes_x and modes_y must be positive")
scale = 1.0 / (self.op_size * self.op_size)
self.koopman_matrix = nn.Parameter(
scale * torch.rand(
self.op_size,
self.op_size,
self.modes_x,
self.modes_y,
dtype=torch.cfloat,
)
)
def time_marching(self, input_tensor, weights):
return torch.einsum("btxy,tfxy->bfxy", input_tensor, weights)
def forward(self, x):
x_ft = torch.fft.rfft2(x)
out_ft = torch.zeros(x_ft.shape, dtype=torch.cfloat, device=x.device)
modes_x = min(self.modes_x, max(1, x_ft.shape[-2] // 2))
modes_y = min(self.modes_y, x_ft.shape[-1])
weights = self.koopman_matrix[:, :, :modes_x, :modes_y]
out_ft[:, :, :modes_x, :modes_y] = self.time_marching(
x_ft[:, :, :modes_x, :modes_y],
weights,
)
out_ft[:, :, -modes_x:, :modes_y] = self.time_marching(
x_ft[:, :, -modes_x:, :modes_y],
weights,
)
return torch.fft.irfft2(out_ft, s=(x.size(-2), x.size(-1)))
class KNO2D(nn.Module):
def __init__(
self,
encoder: nn.Module,
decoder: nn.Module,
op_size: int,
modes_x: int = 12,
modes_y: int = 12,
decompose: int = 6,
linear_type: bool = True,
normalization: bool = False,
):
super().__init__()
self.op_size = int(op_size)
self.decompose = int(decompose)
if self.decompose < 1:
raise ValueError("decompose must be positive")
self.enc = encoder
self.dec = decoder
self.koopman_layer = KoopmanOperator2D(self.op_size, modes_x=modes_x, modes_y=modes_y)
self.w0 = nn.Conv2d(self.op_size, self.op_size, 1)
self.linear_type = bool(linear_type)
self.normalization = bool(normalization)
if self.normalization:
self.norm_layer = nn.BatchNorm2d(self.op_size)
def forward(self, x):
x_reconstruct = self.dec(torch.tanh(self.enc(x)))
x = torch.tanh(self.enc(x)).permute(0, 3, 1, 2)
x_w = x
for _ in range(self.decompose):
x1 = self.koopman_layer(x)
x = x + x1 if self.linear_type else torch.tanh(x + x1)
shortcut = self.w0(x_w)
if self.normalization:
shortcut = self.norm_layer(shortcut)
x = torch.tanh(shortcut + x).permute(0, 2, 3, 1)
return self.dec(x), x_reconstruct
class KNO2DNavierStokes(nn.Module):
"""KNO2D adapter for flattened Navier-Stokes batches.
``pos`` is accepted for API parity with FNO but is not used by the Koopman
operator. ``fx`` is shaped ``(B, H*W, t_in*out_dim)`` and the output is
shaped ``(B, H*W, out_dim)`` for one-step autoregressive rollout.
"""
def __init__(
self,
input_channels: int,
output_channels: int,
spatial_shape,
op_size: int = 32,
modes_x: int = 12,
modes_y: int = 12,
decompose: int = 6,
linear_type: bool = True,
normalization: bool = False,
):
super().__init__()
if int(output_channels) != 1:
raise ValueError("KNO2DNavierStokes currently expects output_channels=1.")
self.input_channels = int(input_channels)
self.output_channels = int(output_channels)
self.spatial_shape = tuple(int(v) for v in spatial_shape)
if len(self.spatial_shape) != 2 or min(self.spatial_shape) < 1:
raise ValueError("spatial_shape must contain two positive dimensions")
if self.input_channels < 1:
raise ValueError("input_channels must be positive")
encoder = EncoderConv2D(self.input_channels, int(op_size))
decoder = DecoderConv2D(self.output_channels, int(op_size))
self.kno = KNO2D(
encoder=encoder,
decoder=decoder,
op_size=int(op_size),
modes_x=int(modes_x),
modes_y=int(modes_y),
decompose=int(decompose),
linear_type=bool(linear_type),
normalization=bool(normalization),
)
def forward(self, pos, fx):
batch_size, num_points, channels = fx.shape
height, width = self.spatial_shape
if pos.shape[:2] != (batch_size, num_points) or pos.shape[-1] != 2:
raise ValueError(
f"Expected pos [B, {num_points}, 2], got {tuple(pos.shape)}"
)
if num_points != height * width:
raise ValueError(f"Expected {height * width} points, got {num_points}.")
if channels != self.input_channels:
raise ValueError(f"Expected {self.input_channels} channels, got {channels}.")
x = fx.reshape(batch_size, height, width, channels)
pred, _ = self.kno(x)
return pred.reshape(batch_size, num_points, self.output_channels)