Download model/ronom.py from OneScience-Group/RONOM: direct link, hf CLI and curl.
- Browser
- Download file 8.09 kB
-
https://huggingface.co/OneScience-Group/RONOM/resolve/main/model/ronom.py
- Command line
-
hf download hf://OneScience-Group/RONOM/model/ronom.py
-
curl -L -o ronom.py https://huggingface.co/OneScience-Group/RONOM/resolve/main/model/ronom.py
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 | |