This repository implements WaSP: Wasserstein Data Synthesis Pipeline for Solver-Free Operator Learning.
WaSP trains neural operators on PDEs without a solver. Supervised operator learning needs input-solution pairs from a numerical solver, which is the cost the surrogate is meant to remove. WaSP instead manufactures exact pairs by applying the PDE operator to sampled solutions, and moves the input distribution of those pairs along a Wasserstein interpolation toward the test-time distribution. Training becomes a sequence of supervised problems, breaking the one large distribution shift into manageable pieces.
- Problem Setting
- Why Existing Approaches Fall Short
- The WaSP Algorithm
- Supported PDEs
- Methods and Baselines
- Installation
- Data Preparation
- Training
- Evaluation
Note
Input and condition are the same object. The paper calls the PDE input
EqCondition, map_condition, condition_loss, the cond* loss weights,
and the cond_loss logs all refer to the input
A PDE is written as a bijection
where
where
PINO constrains
Two difficulties follow. A residual measured in a weak norm need not control the solution
error, and a norm strong enough to control it inflates the variance of the mini-batch
gradient.
Manufactured data (MD) samples solutions
Every pair is exact and the problem is a well-conditioned regression, but the induced
input distribution
WaSP splits the gap between
Step 1. Choose a manufactured solution distribution
Step 2. Estimate a transport map
Step 3. Warm up on the exact manufactured pairs at
Step 4. For
- Interpolate the input distribution:
$\nu_s = \bigl((1-s)\mathrm{Id} + sT\bigr)_{\sharp} \nu_0$ - Synthesize solutions with the current operator:
$\hat\mu_s = (G_{\theta})_{\sharp} \nu_s$ - Apply
$N$ to recover the exact input of each synthesized solution, and take a gradient step on the pairs.
Each step minimizes
The implementation measures both terms with the unsquared, grid-normalized wasp.utils.l2_norm) rather than its square. The first term is the solution loss
Structure of the input space. Each input
Optimal transport is therefore solved on the spatial grid, not the space-time grid.
Curriculum. At small
| Acronym | Equation | Config | Script |
|---|---|---|---|
| AC | Allen-Cahn: |
config/ac_config.py |
train_ac.py |
| KS | Kuramoto-Sivashinsky: |
config/ks_config.py |
train_ks.py |
| CGLE | Complex Ginzburg-Landau: |
config/gl_config.py |
train_gl.py |
All three are one-dimensional evolution equations with periodic spatial boundaries. The
forcing
Allen-Cahn (
Kuramoto-Sivashinsky (
Complex Ginzburg-Landau (
The five configurations reported in the paper share the optimizer, schedule, step budget, and total batch size of 64, and differ only in the transport schedule and in how the batch is split between the two loss terms.
| Method | transport_enabled |
batch_size_buffer |
batch_size_gen |
Objective |
|---|---|---|---|---|
| WaSP (ours) | True | 32 | 32 | Both terms along the path |
| PINO | False | 0 | 64 | PDE residual at |
| PINO+OT | True | 0 | 64 | PDE residual along the path |
| MD | False | 64 | 0 | Solution loss on the fixed |
| MD+PINO | False | 32 | 32 | Both terms frozen at |
PINO and PINO+OT raise cond_ic from 0.1 to 1.0, so that the interior and
initial-condition residuals are weighted equally; without the change PINO ignores the
initial condition and training degenerates. The two ablations isolate the two components of
WaSP: PINO+OT keeps the transport curriculum and drops the self-generated supervision,
MD+PINO keeps the combined objective and drops the curriculum.
Clone the repository:
git clone git@github.com:neuraloperator/WaSP.gitCreate the environment:
conda create -n wasp python=3.13
conda activate waspInstall torch and WaSP:
# Adjust --index-url to match your CUDA version
pip install torch==2.8.0 torchvision==0.23.0 torchaudio==2.8.0 --index-url https://download.pytorch.org/whl/cu128
pip install -e .pykeops is sensitive to the CUDA and torch versions. If installation fails, check
CUDA_HOME, LIBRARY_PATH, and LD_LIBRARY_PATH before reinstalling. For development,
install the hook:
pre-commit installDatasets are not shipped with the repository and have to be generated once. Each equation
has a create_dataset() class method that runs the ETD-RK4 solver from random initial
conditions with no forcing and writes the trajectories to disk. The filename encodes the
equation parameters and the resolution, so setup() locates it from the training config.
scripts/datasets/create_{ac,ks,gl}_dataset.py expose the same parameters as a CLI through
zencfg; any field can be overridden with --key value.
The dataset serves two purposes. test_samples trajectories are held out for evaluation.
For KS and CGLE, fitting_samples further trajectories supply the unpaired target
inputs used to fit the power spectrum of
For the two chaotic equations the solver runs on a long time domain, the first burn_in
steps are discarded, and the remainder is cut into consecutive windows of the target
resolution, so every window is a sample of the invariant measure.
python scripts/datasets/create_ks_dataset.py --nu 0.01 --n_samples 10000 --root_dir ./data/ks_datasetsOr through the Python API:
from wasp.equations.ks_equation import KSEquation
# Generates ./data/ks_datasets/nu_1e-02_0.128t@128_5.0x@256.pt
KSEquation.create_dataset(
nu=0.01,
n_samples=10000,
batch_size=10,
domain=(0.128, 5.0), # target (T, L)
resolution=(128, 256), # target (N_t, N_x)
solver_domain=(10.0, 5.0), # solver time domain, long enough for burn-in
solver_resolution=(20000, 512),
burn_in=1000, # solver steps discarded
root_dir="./data/ks_datasets",
verbose=True,
)python scripts/datasets/create_ac_dataset.py --nu 0.01 --n_samples 1000 --root_dir ./data/ac_datasetsfrom wasp.equations.ac_equation import ACEquation
# Generates ./data/ac_datasets/nu_1e-02_amp_1e-03_decay_1.0e+00_5.0t@128_6.283185307179586x@128.pt
ACEquation.create_dataset(
nu=0.01,
ic_amplitude=1e-3, # amplitude of the initial-condition power spectrum
power_decay=1.0, # P(k) proportional to exp(-k)
n_samples=1000,
batch_size=10,
domain=(5.0, 6.283185307179586), # (T, L) = (5, 2 pi)
resolution=(128, 128),
solver_resolution=(256, 256),
root_dir="./data/ac_datasets",
)Allen-Cahn is not chaotic, so it needs no burn-in: each trajectory is one sample and the solver runs directly on the target time domain.
python scripts/datasets/create_gl_dataset.py --alpha 2.0 --beta -2.0 --n_samples 10000 --root_dir ./data/gl_datasetsfrom wasp.equations.gl_equation import GLEquation
# Generates ./data/gl_datasets/alpha_2e+00_beta_-2e+00_6.4t@128_50.0x@256.pt
GLEquation.create_dataset(
alpha=2.0,
beta=-2.0,
n_samples=10000,
batch_size=10,
domain=(6.4, 50.0),
resolution=(128, 256),
solver_domain=(500.0, 50.0),
solver_resolution=(20000, 512),
burn_in=1000,
root_dir="./data/gl_datasets",
verbose=True,
)WaSP on KS (100k steps):
python train_ks.py --wandb.name wasp_ksCheckpoints are written every save_freq steps to ./checkpoints/wasp_ks/. Evaluation on
the held-out test set runs every eval_freq steps during training.
PINO (PDE residual loss at
python train_ks.py \
--wandb.name pino_ks \
--trainer.batch_size_buffer 0 \
--trainer.batch_size_gen 64 \
--trainer.transport_start_s 1.0 \
--trainer.transport_enabled False \
--equation.loss_weights.cond 1.0 \
--equation.loss_weights.cond_ic 1.0PINO+OT (PDE residual loss along the path):
python train_ks.py \
--wandb.name pino_ot_ks \
--trainer.batch_size_buffer 0 \
--trainer.batch_size_gen 64 \
--equation.loss_weights.cond 1.0 \
--equation.loss_weights.cond_ic 1.0 \
--trainer.transport_min_sample 2000MD (manufactured solution loss on the fixed
python train_ks.py \
--wandb.name md_ks \
--trainer.transport_enabled False \
--trainer.batch_size_buffer 64 \
--trainer.batch_size_gen 0MD+PINO (both terms frozen at
python train_ks.py --wandb.name md_pino_ks --trainer.transport_enabled FalseReplace ks and train_ks.py with ac and train_ac.py or gl and train_gl.py for
the other equations. Full hyperparameters for every run are listed in the appendix of
the paper.
scripts/eval.py measures pointwise accuracy and the fidelity of the long-time statistics
through five metrics:
-
Solution relative
$\ell_2$ , on the test set with zero forcing: $$\frac{\mathbb{E}{v \sim \nu_1} |G\theta(v) - G(v)|2}{\mathbb{E}{v \sim \nu_1} |G(v)|_2}$$ -
Invariant-measure distance: Sinkhorn
$W_2$ between the snapshot ensembles of the model and the solver, both collected from autoregressive rollouts on the attractor. A second solver rollout gives the reference offset of the estimator. The coupling is$O(n^2)$ , so the rollout is subsampled to at mostoptimal_transport.num_samplessnapshots (10,000, equal to the default coarse snapshot count, so the paper's setting uses every snapshot); the estimate is converged well below that, but its finite-sample bias means the value is only comparable at a fixednum_samples. -
Spatial autocorrelation RMSE: relative RMSE of
$\mathbb{E}[u(x)\overline{u(x+\Delta x)}]/\mathrm{Var}[u(x)]$ against the solver. -
Temporal autocorrelation RMSE: relative RMSE of
$\mathbb{E}[u(t)\overline{u(t+\Delta t)}]/\mathrm{Var}[u(x)]$ against the solver. -
Energy spectrum RMSE: relative RMSE of
$\mathbb{E}_{u}|\mathcal{F}_k(u)|^2$ against the solver.
Metrics 2 to 5 require an invariant measure and are computed for KS and CGLE only. They
draw on two cached rollouts: a coarse one, subsampled at coarse_rollout.delta_t, for
metrics 2, 3, and 5, and a fine one at the native dt for metric 4. Both discard
burn_in time units before recording. long_horizon_rollout is an extra diagnostic, the
rollout error against the solver as a function of time; it is not one of the five reported
metrics.
KS:
python scripts/eval.py \
--checkpoint ./checkpoints/wasp_ks/model_step_100000.pt \
--test_data_path ./data/ks_datasets/nu_1e-02_0.128t@128_5.0x@256.pt \
--coarse_rollout.delta_t 0.5 \
--coarse_rollout.burn_in 10 \
--fine_rollout.burn_in 10 \
--optimal_transport.eps 0.01 \
--long_horizon_rollout FalseAC (metric 1 only):
python scripts/eval.py \
--checkpoint ./checkpoints/wasp_ac/model_step_20000.pt \
--test_data_path ./data/ac_datasets/nu_1e-02_amp_1e-03_decay_1.0e+00_5.0t@128_6.283185307179586x@128.pt \
--attractor_dist False \
--energy_spectrum False \
--spatial_correlation False \
--temporal_correlation False \
--long_horizon_rollout FalseCGLE (a longer burn-in and tighter Sinkhorn regularization):
python scripts/eval.py \
--checkpoint ./checkpoints/wasp_gl/model_step_200000.pt \
--test_data_path ./data/gl_datasets/alpha_2e+00_beta_-2e+00_6.4t@128_50.0x@256.pt \
--coarse_rollout.delta_t 25 \
--coarse_rollout.burn_in 500 \
--fine_rollout.burn_in 500 \
--optimal_transport.eps 0.001 \
--long_horizon_rollout FalseMetrics are written to ./logs/eval/{wandb_name}/metrics.json and the rollouts are cached
next to them, so re-running skips the rollout.