FluxNet-ShallowWater-LAP / model /FluxNetShallowWater.py
yushuang88's picture
Upload folder using huggingface_hub
e5a6e71 verified
Raw History Blame Contribute Delete
3.92 kB
"""Independent implementation of FluxNet-LAP (arXiv:2602.01941v2).
The transport equations and epsilon follow Sections 3.2, 3.3 and 3.5;
the backbone dimensions follow Appendix C.2. See reproduction_spec.md
for the explicitly identified gaps filled by read-only official references.
"""
import torch
from torch import nn
from torch.nn import functional as F
class FeatureBlock(nn.Module):
"""Two periodic convolutions, with a learned concatenation skip fusion."""
def __init__(self, width, kernel_size):
super().__init__()
self.layers = nn.Sequential(
nn.Conv2d(width, width, kernel_size, padding=kernel_size // 2,
padding_mode="circular"),
nn.BatchNorm2d(width), nn.GELU(),
nn.Conv2d(width, width, kernel_size, padding=kernel_size // 2,
padding_mode="circular"),
nn.BatchNorm2d(width), nn.GELU(),
)
self.fuse = nn.Conv2d(2 * width, width, 1)
def forward(self, features):
return self.fuse(torch.cat((self.layers(features), features), dim=1))
class FluxNetShallowWater(nn.Module):
"""Map physical states [B,3,H,W] ordered h,mx,my to the next state.
This operator redistributes cumulative amounts, with no dt multiplier,
clipping, mass projection, or numerical-solver substeps.
"""
def __init__(self, base_channels=64, num_blocks=6, kernel_size=5,
neighborhood_size=3, epsilon=1e-6):
super().__init__()
if kernel_size % 2 != 1 or neighborhood_size % 2 != 1:
raise ValueError("Convolution and transport sizes must be odd")
if neighborhood_size < 3 or epsilon <= 0:
raise ValueError("Invalid transport size or epsilon")
self.epsilon = float(epsilon)
radius = neighborhood_size // 2
self.offsets = tuple((dy, dx) for dy in range(-radius, radius + 1)
for dx in range(-radius, radius + 1)
if (dy, dx) != (0, 0))
self.encoder = nn.Sequential(
nn.Conv2d(3, base_channels, kernel_size, padding=kernel_size // 2,
padding_mode="circular"),
nn.BatchNorm2d(base_channels), nn.GELU(),
*(FeatureBlock(base_channels, kernel_size) for _ in range(num_blocks)),
)
directions = len(self.offsets)
self.depth_head = nn.Conv2d(base_channels, directions + 1, 1)
self.pressure_x = nn.Conv2d(base_channels, directions, 1)
self.pressure_y = nn.Conv2d(base_channels, directions, 1)
def transport(self, state):
features = self.encoder(state)
h = state[:, :1]
raw = self.depth_head(features)
outgoing_depth = h * raw[:, :1].sigmoid()
depth_transfers = outgoing_depth * raw[:, 1:].softmax(dim=1)
velocity = state[:, 1:] / (h + self.epsilon)
gate = h.square()
momentum_x = depth_transfers * velocity[:, :1] + gate * F.softplus(self.pressure_x(features))
momentum_y = depth_transfers * velocity[:, 1:] + gate * F.softplus(self.pressure_y(features))
transfers = torch.stack((depth_transfers, momentum_x, momentum_y), dim=1)
return outgoing_depth, transfers
def forward(self, state):
outgoing_depth, transfers = self.transport(state)
# For h, use the analytically summed alpha*h to retain the lower bound
# in floating point. Both forms are algebraically the same (Prop. 2).
outgoing = torch.cat((outgoing_depth, transfers[:, 1:].sum(dim=2)), dim=1)
updated = state - outgoing
for index, offset in enumerate(self.offsets):
# Offset denotes receiver minus sender; rolling moves sender's
# outgoing field to its receiver on the periodic torus.
updated = updated + torch.roll(transfers[:, :, index], offset, dims=(-2, -1))
return updated