RONOM / model /ronom.py
yushuang88's picture
Upload folder using huggingface_hub
e456924 verified
Raw History Blame Contribute Delete
8.09 kB
"""Independent implementation of RONOM, arXiv:2507.12814v2, Sec. 2 and SM3.
The projection solves weighted regularized least squares in a Gaussian basis;
it is not a learned Fourier operator. All geometric matrices are path independent.
"""
import math
from typing import List
import numpy as np
from scipy.special import erf
import torch
from torch import nn
def gaussian(x, centers, sigma):
return np.exp(-0.5 * ((x[:, None] - centers[None, :]) / sigma) ** 2)
def laplacian_gram(centers, sigma):
"""Closed-form integral of phi_i'' phi_j'' on [0,1], in float64.
Complete the square at the midpoint and integrate Gaussian moments 0,2,4.
The recurrence follows integration by parts, without numerical quadrature.
"""
mid = (centers[:, None] + centers[None, :]) / 2
dist = centers[:, None] - centers[None, :]
lo, hi = -mid, 1 - mid
ea, eb = np.exp(-(lo / sigma)**2), np.exp(-(hi / sigma)**2)
j0 = sigma * math.sqrt(math.pi) / 2 * (erf(hi / sigma) - erf(lo / sigma))
j2 = sigma**2 / 2 * (j0 - hi * eb + lo * ea)
j4 = sigma**2 / 2 * (3 * j2 - hi**3 * eb + lo**3 * ea)
value = np.exp(-dist**2 / (4 * sigma**2)) / sigma**8 * (
j4 - (dist**2 / 2 + 2 * sigma**2) * j2
+ (dist**2 / 4 - sigma**2)**2 * j0)
return (value + value.T) / 2
def projection_matrix(x_in, x_measure, centers, sigma, alpha, sobolev_reg, bc_reg):
basis = gaussian(x_in, centers, sigma)
# Trapezoidal integral weights; all input-generalization points are uniform.
w = np.empty(len(x_in), dtype=np.float64)
w[0], w[-1] = (x_in[1] - x_in[0]) / 2, (x_in[-1] - x_in[-2]) / 2
w[1:-1] = (x_in[2:] - x_in[:-2]) / 2
edge = gaussian(np.array([0., 1.]), centers, sigma)
# Official residual convention scales the derivative residual by sigma.
deriv = -(np.array([0., 1.])[:, None] - centers) / sigma * edge
residual = np.stack([edge[0] - edge[1], deriv[0] - deriv[1]])
rhs = basis.T * w
normal = (rhs @ basis + sobolev_reg * laplacian_gram(centers, sigma)
+ bc_reg * (residual.T @ residual) + alpha * np.eye(len(centers)))
return gaussian(x_measure, centers, sigma) @ np.linalg.solve(normal, rhs)
class LatentFlow(nn.Module):
"""Autonomous neural ODE with the explicit fourth order 3/8 tableau."""
def __init__(self, dimension, widths, step_size):
super().__init__()
layers = []
prev = dimension
for width in widths:
layers += [nn.Linear(prev, width), nn.GELU()]
prev = width
layers.append(nn.Linear(prev, dimension))
self.velocity = nn.Sequential(*layers)
self.step_size = float(step_size)
def forward(self, initial: torch.Tensor, times: torch.Tensor) -> torch.Tensor:
state = initial
states = torch.jit.annotate(List[torch.Tensor], [])
states.append(state)
for i in range(times.numel() - 1):
# Sampling times are 0,.1,...,1. Substeps also allow a coarser query.
interval = float(times[i + 1] - times[i])
count = max(1, int(math.ceil(interval / self.step_size - 1e-6)))
h = interval / count
for _ in range(count):
a = self.velocity(state)
b = self.velocity(state + (h / 3.) * a)
c = self.velocity(state + h * (b - a / 3.))
d = self.velocity(state + h * (a - b + c))
state = state + (h / 8.) * (a + 3. * b + 3. * c + d)
states.append(state)
return torch.stack(states, dim=1)
class RONOM(nn.Module):
def __init__(self, grid_size=33, features=(64, 128, 128, 256),
latent_channels=4, fourier_features=32, velocity_widths=(64, 64, 64),
padding_size=2, alpha=1e-5, sobolev_reg=1e-8, bc_reg=10.,
step_size=0.1):
super().__init__()
self.grid_size = int(grid_size)
self.latent_channels = int(latent_channels)
self.alpha, self.sobolev_reg, self.bc_reg = alpha, sobolev_reg, bc_reg
self.sigma = 1 / ((grid_size - 1) * math.sqrt(-2 * math.log(0.5)))
self.register_buffer('frequencies', torch.randn(fourier_features, 1))
f = list(features)
self.lift = nn.Sequential(nn.Conv1d(1 + 2 * fourier_features, 64, 3, padding=1),
nn.ReLU(), nn.Conv1d(64, f[0] // 2, 3, padding=1))
self.output_project = nn.Sequential(
nn.ConvTranspose1d(f[0] // 2, 64, 3, padding=1), nn.ReLU(),
nn.ConvTranspose1d(64, 1, 3, padding=1))
enc = []
prev = f[0] // 2
for width in f:
enc += [nn.Conv1d(prev, width, 3, padding=1), nn.ReLU()]
prev = width
self.encoder = nn.Sequential(*enc)
self.to_latent = nn.Conv1d(f[-1], latent_channels, 3, padding=1)
dec = []
back = list(reversed(f)) + [f[0] // 2]
for left, right in zip(back[:-1], back[1:]):
dec += [nn.ConvTranspose1d(left, right, 3, padding=1), nn.ReLU()]
self.decoder = nn.Sequential(*dec)
self.from_latent = nn.ConvTranspose1d(latent_channels, f[-1], 3, padding=1)
x = np.linspace(0., 1., grid_size)
centers = np.arange(-padding_size, grid_size + padding_size) / (grid_size - 1)
proj = projection_matrix(x, x, centers, self.sigma, alpha, sobolev_reg, bc_reg)
kernel = gaussian(x, x, self.sigma)
recovery = np.linalg.solve(kernel + alpha * np.eye(grid_size), kernel.T).T
self.register_buffer('grid', torch.tensor(x, dtype=torch.float32))
self.register_buffer('centers', torch.tensor(centers, dtype=torch.float64))
self.register_buffer('projection', torch.tensor(proj, dtype=torch.float32))
self.register_buffer('recovery', torch.tensor(recovery, dtype=torch.float32))
self.latent_flow = LatentFlow(grid_size * latent_channels, velocity_widths, step_size)
def encode(self, initial, x_input=None):
if initial.ndim == 3:
initial = initial.squeeze(-1)
if x_input is None:
projected = initial @ self.projection.T
else:
matrix = projection_matrix(x_input.detach().cpu().double().numpy(),
self.grid.detach().cpu().double().numpy(), self.centers.cpu().numpy(),
self.sigma, self.alpha, self.sobolev_reg, self.bc_reg)
projected = initial @ torch.as_tensor(matrix.T, device=initial.device, dtype=initial.dtype)
phase = 2 * math.pi * self.grid[:, None] @ self.frequencies.T
positional = torch.cat([phase.cos(), phase.sin()], dim=-1).T
lifted_input = torch.cat([projected[:, None, :],
positional[None].expand(initial.shape[0], -1, -1)], dim=1)
return self.to_latent(self.encoder(self.lift(lifted_input)))
def decode(self, latent, x_output=None):
b, nt = latent.shape[:2]
features = latent.reshape(b * nt, self.latent_channels, self.grid_size)
values = self.output_project(self.decoder(self.from_latent(features)))
values = values.reshape(b, nt, self.grid_size)
if x_output is None:
return values @ self.recovery.T
train_x = self.grid.detach().cpu().double().numpy()
target_x = x_output.detach().cpu().double().numpy()
kernel = gaussian(train_x, train_x, self.sigma)
matrix = np.linalg.solve(kernel + self.alpha * np.eye(self.grid_size),
gaussian(target_x, train_x, self.sigma).T).T
return values @ torch.as_tensor(matrix.T, device=values.device, dtype=values.dtype)
def forward(self, initial, times, return_latent=False):
# CPU time coordinates avoid a host/device synchronization at every RK stage.
z0 = self.encode(initial)
zt = self.latent_flow(z0.flatten(1), times)
zt = zt.reshape(initial.shape[0], times.numel(), self.latent_channels, self.grid_size)
prediction = self.decode(zt)
if return_latent:
return prediction, zt
return prediction