| name | neural-operator |
| description | Train neural operators (FNO, DeepONet) to learn solution maps for parametric PDE families. Once trained, solve new PDE instances in milliseconds. Use when you need to solve many instances of the same PDE with different parameters/ICs/BCs. |
| category | physics |
| version | 1.0.0 |
| author | Synthetic Sciences |
| license | MIT |
| tags | ["Neural Operator","FNO","DeepONet","PDE","Surrogate Model","Deep Learning"] |
| dependencies | ["neuraloperator>=0.3.0","torch>=2.1.0","numpy>=1.24.0","matplotlib>=3.7.0"] |
Neural Operators (FNO / DeepONet)
Overview
Neural operators learn mappings between function spaces — given an input function (initial condition, forcing, boundary), they predict the output function (PDE solution). Once trained on a dataset of PDE solutions, they solve new instances in milliseconds.
When to Use
- You need to solve the SAME PDE many times with different parameters
- Real-time predictions needed (design optimization, control)
- Building a surrogate model for expensive simulations
- The PDE family is known but expensive to solve numerically
Do NOT Use When
- Solving a single PDE instance (use
pde-solver — faster)
- You don't have training data (solve the PDE a few hundred times first)
- You need high accuracy (< 0.1% error is hard for neural operators)
- The PDE changes fundamentally between instances (different physics)
Installation
pip install neuraloperator torch
Core Workflows
1. Fourier Neural Operator (FNO) — 1D Burgers Equation
import torch
import numpy as np
from neuraloperator.models import FNO1d
from neuraloperator.datasets import load_darcy_flow_small
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.fft import fft, ifft, fftfreq
def solve_burgers(u0, nu=0.01, T=1.0, N=256, dt=0.001):
"""Solve Burgers equation using pseudospectral method."""
dx = 2*np.pi / N
x = np.linspace(0, 2*np.pi, N, endpoint=False)
k = fftfreq(N, d=dx) * 2*np.pi
def rhs(t, u_hat):
u = np.real(ifft(u_hat))
u_x = np.real(ifft(1j * k * u_hat))
return -fft(u * u_x) - nu * k**2 * u_hat
u0_hat = fft(u0)
sol = solve_ivp(rhs, (0, T), u0_hat, method='RK45',
rtol=1e-8, atol=1e-10)
return np.real(ifft(sol.y[:, -1]))
N = 256
n_train = 500
n_test = 100
x = np.linspace(0, 2*np.pi, N, endpoint=)
np.random.seed()
inputs = []
outputs = []
i (n_train + n_test):
u0 = np.zeros(N)
k (, ):
u0 += np.random.randn() * np.sin(k*x) + np.random.randn() * np.cos(k*x)
u0 *=
u_final = solve_burgers(u0)
inputs.append(u0)
outputs.append(u_final)
inputs = np.array(inputs)
outputs = np.array(outputs)
x_train = torch.tensor(inputs[:n_train], dtype=torch.float32).unsqueeze(-)
y_train = torch.tensor(outputs[:n_train], dtype=torch.float32).unsqueeze(-)
x_test = torch.tensor(inputs[n_train:], dtype=torch.float32).unsqueeze(-)
y_test = torch.tensor(outputs[n_train:], dtype=torch.float32).unsqueeze(-)
()
()
2. Training the FNO
model = FNO1d(
n_modes_height=16,
hidden_channels=64,
in_channels=1,
out_channels=1,
n_layers=4,
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=100, gamma=0.5)
n_epochs = 500
batch_size = 32
for epoch in range(n_epochs):
model.train()
perm = torch.randperm(n_train)
total_loss = 0
n_batches = 0
for i in range(0, n_train, batch_size):
idx = perm[i:i+batch_size]
x_batch = x_train[idx]
y_batch = y_train[idx]
pred = model(x_batch)
loss = torch.nn.functional.mse_loss(pred, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
n_batches += 1
scheduler.step()
if (epoch + 1) % 50 == 0:
model.eval()
with torch.no_grad():
pred_test = model(x_test)
test_loss = torch.nn.functional.mse_loss(pred_test, y_test)
rel_err = torch.mean(
torch.norm(pred_test - y_test, dim=1) / torch.norm(y_test, dim=1)
)
(
)
3. DeepONet (Branch-Trunk Architecture)
class DeepONet(torch.nn.Module):
def __init__(self, branch_input_dim, trunk_input_dim, hidden_dim=128, p=64):
super().__init__()
self.branch = torch.nn.Sequential(
torch.nn.Linear(branch_input_dim, hidden_dim),
torch.nn.Tanh(),
torch.nn.Linear(hidden_dim, hidden_dim),
torch.nn.Tanh(),
torch.nn.Linear(hidden_dim, p),
)
self.trunk = torch.nn.Sequential(
torch.nn.Linear(trunk_input_dim, hidden_dim),
torch.nn.Tanh(),
torch.nn.Linear(hidden_dim, hidden_dim),
torch.nn.Tanh(),
torch.nn.Linear(hidden_dim, p),
)
self.bias = torch.nn.Parameter(torch.zeros(1))
def forward(self, u_input, x_query):
"""
u_input: (batch, n_sensors) — input function values at sensor locations
x_query: (batch, n_query, dim) — query locations
Returns: (batch, n_query) — predicted output function values
"""
b = self.branch(u_input)
t = self.trunk(x_query)
out = torch.einsum('bp,bqp->bq', b, t) + self.bias
return out
4. Evaluation and Visualization
model.eval()
with torch.no_grad():
pred = model(x_test)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for i, ax in enumerate(axes):
idx = np.random.randint(n_test)
ax.plot(x, x_test[idx, :, 0].numpy(), 'b-', label='Input IC')
ax.plot(x, y_test[idx, :, 0].numpy(), 'k-', linewidth=2, label='True')
ax.plot(x, pred[idx, :, 0].numpy(), 'r--', linewidth=2, label='FNO')
ax.set_xlabel('x')
ax.set_ylabel('u')
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
rel = torch.norm(pred[idx] - y_test[idx]) / torch.norm(y_test[idx])
ax.set_title(f'Test {idx}: rel. error = {rel:.3f}')
plt.suptitle('FNO: Burgers Equation', fontsize=14)
plt.tight_layout()
plt.savefig('fno_predictions.png', dpi=150, bbox_inches='tight')
Architecture Selection Guide
| Architecture | Best For | Input | Limitations |
|---|
| FNO | Regular grids, periodic BCs | Full field on grid | Fixed resolution, periodic |
| DeepONet | Irregular data, different resolutions | Function at sensors + query points | Needs sensor placement |
| GNO (Graph NO) | Unstructured meshes, complex geometry | Graph-structured data | More complex implementation |
Key Hyperparameters (FNO)
| Parameter | Typical Range | Effect |
|---|
n_modes | 8-32 | Fourier modes kept (frequency resolution) |
hidden_channels | 32-128 | Network width |
n_layers | 4-6 | Network depth |
| Learning rate | 1e-3 to 1e-4 | Standard Adam |
| Batch size | 16-64 | Larger is more stable |
| Training samples | 500-5000 | More data = better generalization |
Tips
- Generate training data using traditional solvers (finite differences, spectral, FEM)
- Normalize inputs and outputs to zero mean, unit variance
- Start with FNO for regular grids — it's the simplest and most robust
- Use relative L2 error as the metric, not MSE (scale-invariant)
- Test on out-of-distribution inputs to check generalization limits
Troubleshooting
| Symptom | Fix |
|---|
| Training loss doesn't decrease | Reduce LR, increase network size, check data loading |
| Good train, bad test error | Overfitting — add weight decay, reduce model size, get more data |
| Predictions are smooth but wrong | Too few Fourier modes — increase n_modes |
| GPU out of memory | Reduce batch size or hidden_channels |
| Resolution mismatch train/test | FNO supports different resolutions if trained properly |