CFD_Benchmark / model /U_NO.py
OneScience's picture
Upload folder using huggingface_hub
ff0fadf verified
Raw
History Blame Contribute Delete
12.5 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from onescience.modules.fourier.fno_layers import (
SpectralConv1d,
SpectralConv2d,
SpectralConv3d,
)
from onescience.modules.mlp.MLP import StandardMLP
from onescience.modules.embedding import timestep_embedding, unified_pos_embedding
from onescience.modules.fourier.geo_spectral import GeoSpectralConv2d, IPHI
# --- 引入 U-Net 基础组件 (替代 UNet_Blocks) ---
from onescience.modules.layer.unet_layer import (
DoubleConv1D, Down1D, Up1D, OutConv1D,
DoubleConv2D, Down2D, Up2D, OutConv2D,
DoubleConv3D, Down3D, Up3D, OutConv3D,
)
ConvList = [None, DoubleConv1D, DoubleConv2D, DoubleConv3D]
DownList = [None, Down1D, Down2D, Down3D]
UpList = [None, Up1D, Up2D, Up3D]
OutList = [None, OutConv1D, OutConv2D, OutConv3D]
SpectralConvList = [None, SpectralConv1d, SpectralConv2d, SpectralConv3d]
class Model(nn.Module):
"""
U-NO (U-Net Neural Operator) 模型。
结合了 U-Net 的多尺度结构和 FNO 的谱卷积能力。
在 U-Net 的每个 Encoder 和 Decoder 层级之间插入了 FNO Block (SpectralConv) 和 1x1 卷积残差。
"""
def __init__(self, args, device, bilinear=True, s1=96, s2=96):
super(Model, self).__init__()
self.__name__ = "U_NO"
self.args = args
if args.task == "steady":
normtype = "bn"
else:
normtype = "in"
# 1. Embedding & Preprocessing
# -----------------------------------------------------------
input_dim = args.fun_dim
if args.unified_pos and args.geotype != "unstructured":
self.pos = unified_pos_embedding(args.shapelist, args.ref, device=device)
input_dim += args.ref ** len(args.shapelist)
else:
input_dim += args.space_dim
self.preprocess = StandardMLP(
input_dim=input_dim,
output_dim=args.n_hidden,
hidden_dims=[args.n_hidden * 2],
activation=args.act,
use_bias=True
)
if args.time_input:
self.time_fc = nn.Sequential(
nn.Linear(args.n_hidden, args.n_hidden),
nn.SiLU(),
nn.Linear(args.n_hidden, args.n_hidden),
)
# 2. Geometry Projection (GeoFNO for unstructured)
# -----------------------------------------------------------
if self.args.geotype == "unstructured":
self.fftproject_in = GeoSpectralConv2d(
in_channels=args.n_hidden,
out_channels=args.n_hidden,
modes1=args.modes,
modes2=args.modes,
s1=s1,
s2=s2
)
self.fftproject_out = GeoSpectralConv2d(
in_channels=args.n_hidden,
out_channels=args.n_hidden,
modes1=args.modes,
modes2=args.modes,
s1=s1,
s2=s2
)
self.iphi = IPHI()
patch_size = [(size + (16 - size % 16) % 16) // 16 for size in [s1, s2]]
self.padding = [(16 - size % 16) % 16 for size in [s1, s2]]
self.augmented_resolution = [s1, s2]
else:
patch_size = [
(size + (16 - size % 16) % 16) // 16 for size in args.shapelist
]
self.padding = [(16 - size % 16) % 16 for size in args.shapelist]
self.augmented_resolution = [
shape + padding for shape, padding in zip(args.shapelist, self.padding)
]
dim = len(patch_size)
# 3. Multiscale U-Net Modules
self.inc = ConvList[dim](args.n_hidden, args.n_hidden, normtype=normtype)
self.down1 = DownList[dim](args.n_hidden, args.n_hidden * 2, normtype=normtype)
self.down2 = DownList[dim](args.n_hidden * 2, args.n_hidden * 4, normtype=normtype)
self.down3 = DownList[dim](args.n_hidden * 4, args.n_hidden * 8, normtype=normtype)
factor = 2 if bilinear else 1
self.down4 = DownList[dim](args.n_hidden * 8, args.n_hidden * 16 // factor, normtype=normtype)
self.up1 = UpList[dim](args.n_hidden * 16, args.n_hidden * 8 // factor, bilinear, normtype=normtype)
self.up2 = UpList[dim](args.n_hidden * 8, args.n_hidden * 4 // factor, bilinear, normtype=normtype)
self.up3 = UpList[dim](args.n_hidden * 4, args.n_hidden * 2 // factor, bilinear, normtype=normtype)
self.up4 = UpList[dim](args.n_hidden * 2, args.n_hidden, bilinear, normtype=normtype)
self.outc = OutList[dim](args.n_hidden, args.n_hidden)
# 4. FNO Blocks
def get_fno_layer(in_c, out_c, res_list, divisor):
modes_list = [
max(1, min(args.modes, res // divisor))
for res in res_list
]
kwargs = {
"in_channels": in_c,
"out_channels": out_c
}
mode_names = ["modes1", "modes2", "modes3"]
for i, m in enumerate(modes_list):
if i < len(mode_names):
kwargs[mode_names[i]] = m
return SpectralConvList[dim](**kwargs)
# Down Path FNOs
self.process1_down = get_fno_layer(args.n_hidden, args.n_hidden, self.augmented_resolution, 2)
self.process2_down = get_fno_layer(args.n_hidden * 2, args.n_hidden * 2, self.augmented_resolution, 4)
self.process3_down = get_fno_layer(args.n_hidden * 4, args.n_hidden * 4, self.augmented_resolution, 8)
self.process4_down = get_fno_layer(args.n_hidden * 8, args.n_hidden * 8, self.augmented_resolution, 16)
self.process5_down = get_fno_layer(args.n_hidden * 16 // factor, args.n_hidden * 16 // factor, self.augmented_resolution, 32)
# Residual Weights (1x1 Conv)
self.w1_down = ConvList[dim](args.n_hidden, args.n_hidden, 1) # kernel_size=1
self.w2_down = ConvList[dim](args.n_hidden * 2, args.n_hidden * 2, 1)
self.w3_down = ConvList[dim](args.n_hidden * 4, args.n_hidden * 4, 1)
self.w4_down = ConvList[dim](args.n_hidden * 8, args.n_hidden * 8, 1)
self.w5_down = ConvList[dim](args.n_hidden * 16 // factor, args.n_hidden * 16 // factor, 1)
# Up Path FNOs
self.process1_up = get_fno_layer(args.n_hidden, args.n_hidden, self.augmented_resolution, 2)
self.process2_up = get_fno_layer(args.n_hidden * 2 // factor, args.n_hidden * 2 // factor, self.augmented_resolution, 4)
self.process3_up = get_fno_layer(args.n_hidden * 4 // factor, args.n_hidden * 4 // factor, self.augmented_resolution, 8)
self.process4_up = get_fno_layer(args.n_hidden * 8 // factor, args.n_hidden * 8 // factor, self.augmented_resolution, 16)
self.process5_up = get_fno_layer(args.n_hidden * 16 // factor, args.n_hidden * 16 // factor, self.augmented_resolution, 32)
self.w1_up = ConvList[dim](args.n_hidden, args.n_hidden, 1)
self.w2_up = ConvList[dim](args.n_hidden * 2 // factor, args.n_hidden * 2 // factor, 1)
self.w3_up = ConvList[dim](args.n_hidden * 4 // factor, args.n_hidden * 4 // factor, 1)
self.w4_up = ConvList[dim](args.n_hidden * 8 // factor, args.n_hidden * 8 // factor, 1)
self.w5_up = ConvList[dim](args.n_hidden * 16 // factor, args.n_hidden * 16 // factor, 1)
# 5. Projectors
self.fc1 = nn.Linear(args.n_hidden, args.n_hidden * 2)
self.fc2 = nn.Linear(args.n_hidden * 2, args.out_dim)
def structured_geo(self, x, fx, T=None):
B, N, _ = x.shape
if self.args.unified_pos:
x = self.pos.repeat(x.shape[0], 1, 1)
if fx is not None:
fx = torch.cat((x, fx), -1)
fx = self.preprocess(fx)
else:
fx = self.preprocess(x)
if T is not None:
Time_emb = timestep_embedding(T, self.args.n_hidden)
Time_emb = self.time_fc(Time_emb)
if Time_emb.ndim == 2:
Time_emb = Time_emb.unsqueeze(1)
fx = fx + Time_emb # Broadcast
# Reshape to Grid (B, C, H, W...)
x = fx.permute(0, 2, 1).reshape(B, self.args.n_hidden, *self.args.shapelist)
# Padding
if not all(item == 0 for item in self.padding):
pad_arg = []
for p in reversed(self.padding):
pad_arg.extend([0, p])
x = F.pad(x, pad_arg)
# === U-NO Body ===
# Level 1 Down
x1 = self.inc(x)
# FNO Processing + Residual
x1 = F.gelu(self.process1_down(x1) + self.w1_down(x1))
# Level 2 Down
x2 = self.down1(x1)
x2 = F.gelu(self.process2_down(x2) + self.w2_down(x2))
# Level 3 Down
x3 = self.down2(x2)
x3 = F.gelu(self.process3_down(x3) + self.w3_down(x3))
# Level 4 Down
x4 = self.down3(x3)
x4 = F.gelu(self.process4_down(x4) + self.w4_down(x4))
# Level 5 (Bottleneck)
x5 = self.down4(x4)
x5 = F.gelu(self.process5_down(x5) + self.w5_down(x5))
# Bottleneck Up Process
x5 = F.gelu(self.process5_up(x5) + self.w5_up(x5))
# Level 4 Up
x = self.up1(x5, x4)
x = F.gelu(self.process4_up(x) + self.w4_up(x))
# Level 3 Up
x = self.up2(x, x3)
x = F.gelu(self.process3_up(x) + self.w3_up(x))
# Level 2 Up
x = self.up3(x, x2)
x = F.gelu(self.process2_up(x) + self.w2_up(x))
# Level 1 Up
x = self.up4(x, x1)
x = F.gelu(self.process1_up(x) + self.w1_up(x))
x = self.outc(x)
# Un-padding
if not all(item == 0 for item in self.padding):
if len(self.args.shapelist) == 1:
x = x[..., : -self.padding[0]]
elif len(self.args.shapelist) == 2:
x = x[..., : -self.padding[0], : -self.padding[1]]
elif len(self.args.shapelist) == 3:
x = x[..., : -self.padding[0], : -self.padding[1], : -self.padding[2]]
# Output Projection
x = x.reshape(B, self.args.n_hidden, -1).permute(0, 2, 1)
x = self.fc1(x)
x = F.gelu(x)
x = self.fc2(x)
return x
def unstructured_geo(self, x, fx, T=None):
original_pos = x
if fx is not None:
fx = torch.cat((x, fx), -1)
fx = self.preprocess(fx)
else:
fx = self.preprocess(x)
if T is not None:
Time_emb = timestep_embedding(T, self.args.n_hidden)
Time_emb = self.time_fc(Time_emb)
if Time_emb.ndim == 2:
Time_emb = Time_emb.unsqueeze(1)
fx = fx + Time_emb
# Projection to Grid
x = self.fftproject_in(
fx.permute(0, 2, 1), x_in=original_pos, iphi=self.iphi, code=None
)
# === U-NO Body===
x1 = self.inc(x)
x1 = F.gelu(self.process1_down(x1) + self.w1_down(x1))
x2 = self.down1(x1)
x2 = F.gelu(self.process2_down(x2) + self.w2_down(x2))
x3 = self.down2(x2)
x3 = F.gelu(self.process3_down(x3) + self.w3_down(x3))
x4 = self.down3(x3)
x4 = F.gelu(self.process4_down(x4) + self.w4_down(x4))
x5 = self.down4(x4)
x5 = F.gelu(self.process5_down(x5) + self.w5_down(x5))
x5 = F.gelu(self.process5_up(x5) + self.w5_up(x5))
x = self.up1(x5, x4)
x = F.gelu(self.process4_up(x) + self.w4_up(x))
x = self.up2(x, x3)
x = F.gelu(self.process3_up(x) + self.w3_up(x))
x = self.up3(x, x2)
x = F.gelu(self.process2_up(x) + self.w2_up(x))
x = self.up4(x, x1)
x = F.gelu(self.process1_up(x) + self.w1_up(x))
x = self.outc(x)
# Projection Back to Points
x = self.fftproject_out(
x, x_out=original_pos, iphi=self.iphi, code=None
).permute(0, 2, 1)
x = self.fc1(x)
x = F.gelu(x)
x = self.fc2(x)
return x
def forward(self, x, fx, T=None, geo=None):
if self.args.geotype == "unstructured":
return self.unstructured_geo(x, fx, T)
else:
return self.structured_geo(x, fx, T)