Mesh Transfer#
Sphedron provides tools to transfer scalar fields between meshes of
different resolutions and topologies. The core idea is to precompute a
sparse weight matrix W of shape (n_receiver, n_sender) so
that transferring a field is a single sparse matrix–vector product:
Each row of W has a fixed number of non-zero entries (the degree),
making it suitable as a differentiable linear layer in deep-learning
pipelines.
Quick start#
from sphedron import Icosphere, UniformMesh
from sphedron.transfer import MeshTransfer
sender = Icosphere.from_base(refine_factor=256) # 655,362 nodes
receiver = UniformMesh(resolution=0.5) # 259,200 nodes
# Create the regridder -- all config in the constructor
regridder = MeshTransfer(sender, receiver,
method="local_rbf", k=16, degree=0)
# Transfer a field (weights are built lazily on first call)
y = regridder.transform(x_sender)
# Equivalent shorthand via @ operator
y = regridder @ x_sender
# Access the sparse matrix directly
W = regridder.weights
The MeshTransfer object#
MeshTransfer is the main entry point for regridding. It accepts
the source and target meshes together with all interpolation parameters:
regridder = MeshTransfer(
sender, # any Mesh subclass or NodesOnlyMesh
receiver, # target mesh
method="local_rbf", # interpolation method
k=16, # number of neighbors
kernel="thin_plate_spline", # RBF kernel
degree=0, # polynomial augmentation degree
)
Key properties and methods:
regridder.transform(data)– regrid data (lazy weight build).regridder @ data– same astransform.regridder.weights– the sparse CSR matrix (lazy build).regridder.shape–(n_receiver, n_sender).regridder.build_weights(...)– explicit build; any argument updates the stored configuration.repr(regridder)– shows config and build status:MeshTransfer(86,096 → 64,800, method='local_rbf', k=16, kernel='thin_plate_spline', degree=0, built)
Interpolation methods#
The method parameter supports several strategies:
Method |
Degree |
Description |
|---|---|---|
|
1 |
Nearest-neighbor assignment. Fast but blocky. |
|
k |
Inverse distance weighting: \(w_i = 1/d_i\), row-normalized. |
|
k |
Gaussian kernel: \(w_i = \exp(-0.2\,(d_i/d_\min)^2)\), row-normalized. |
|
3 |
Projects onto the nearest triangle and computes barycentric
coordinates. Exact for linear fields on the mesh surface but
requires a |
|
4 |
Bilinear interpolation on quad faces ( |
|
k |
Local radial basis function interpolation with polynomial augmentation (default). Best accuracy. |
Local RBF interpolation#
The local_rbf method builds, for each receiver node, a small k×k
RBF kernel system from its k nearest sender neighbors:
where \(\Phi_{ij} = \varphi(\|x_i - x_j\|)\) is the kernel matrix among the k neighbors, \(\boldsymbol{\phi}_i = \varphi(\|x_i - q\|)\) evaluates the kernel at the query point q, and P contains polynomial terms ensuring:
degree=0: partition of unity (weights sum to 1, constant fields reproduced exactly)
degree=1: linear reproduction (linear fields reproduced exactly)
degree=2+: higher-order polynomial reproduction
All M independent systems are solved in a single batched
np.linalg.solve call, making this significantly faster than
scipy’s RBFInterpolator while producing identical results.
Available kernels (all conditionally positive definite):
thin_plate_spline(default): \(\varphi(r) = r^2 \ln r\)cubic: \(\varphi(r) = r^3\)linear: \(\varphi(r) = r\)
For degree ≥ 1, a safety mechanism detects ill-conditioned local systems (e.g. near coastlines with degenerate neighbor configurations) and falls back to degree=0.
Benchmark: spherical harmonics#
Transfer of a spherical harmonic test field \(f(\theta, \varphi) = \cos(3\theta)\sin(2\varphi)\) from an Icosphere (655,362 nodes, factor=256) to a UniformMesh (259,200 nodes, 0.5° resolution):
Method |
RMSE |
||delta||/||y|| |
nnz/row |
Build |
Apply |
|---|---|---|---|---|---|
|
3.64e-2 |
7.3% |
1 |
0.20s |
1.0ms |
|
3.45e-2 |
6.9% |
5 |
0.04s |
2.1ms |
|
3.15e-2 |
6.3% |
5 |
0.05s |
2.1ms |
|
2.20e-2 |
4.4% |
3 |
81.3s |
1.3ms |
|
2.20e-2 |
4.4% |
16 |
2.6s |
3.1ms |
local_rbf(k=16, d=0) achieves the same accuracy as barycentric
(4.4% relative error) in 2.6s vs 81s – a 31× speedup on weight
construction, with apply times under 3ms.
Benchmark: CESM2 real-data roundtrip#
A more rigorous test: regrid 5 CESM2 ocean variables (SSH, SST, SHF, TAUX, TAUY) from the curvilinear POP grid (86,096 ocean cells) through an intermediate grid and back. We subtract the temporal mean (1332 monthly snapshots, 1990–2100) and measure the centered-field roundtrip error – the relative error on anomalies, averaged over all timesteps.
Config |
SSH |
SST |
SHF |
TAUX |
TAUY |
Mean |
Build |
Apply |
|---|---|---|---|---|---|---|---|---|
xESMF bilinear |
7.78% |
1.50% |
3.93% |
2.60% |
2.70% |
3.70% |
– |
38ms |
|
12.81% |
4.00% |
6.74% |
7.45% |
6.82% |
7.56% |
0.0s |
0.21ms |
|
7.05% |
1.06% |
3.12% |
1.79% |
1.92% |
2.99% |
0.5s |
0.34ms |
|
6.65% |
0.84% |
2.79% |
1.22% |
1.42% |
2.58% |
1.5s |
0.47ms |
|
6.57% |
0.75% |
2.69% |
1.00% |
1.22% |
2.44% |
5.3s |
0.74ms |
|
6.89% |
0.86% |
2.87% |
1.31% |
1.51% |
2.69% |
1.4s |
0.47ms |
Config |
SSH |
SST |
SHF |
TAUX |
TAUY |
Mean |
Build |
Apply |
|---|---|---|---|---|---|---|---|---|
xESMF bilinear |
7.78% |
1.50% |
3.93% |
2.60% |
2.70% |
3.70% |
– |
38ms |
|
7.53% |
1.99% |
3.29% |
3.10% |
3.59% |
3.90% |
0.1s |
0.36ms |
|
5.81% |
0.35% |
1.37% |
0.55% |
0.73% |
1.76% |
0.8s |
0.56ms |
|
5.77% |
0.27% |
1.34% |
0.40% |
0.57% |
1.67% |
2.6s |
0.77ms |
|
5.80% |
0.23% |
1.27% |
0.33% |
0.53% |
1.63% |
8.9s |
1.19ms |
|
5.84% |
0.25% |
1.27% |
0.39% |
0.55% |
1.66% |
2.4s |
0.77ms |
Key observations:
Sphedron beats xESMF bilinear on every variable – 2.44% vs 3.70% mean at Uniform 1°, and 1.67% at Icosphere-128.
Apply is 14–50× faster than xESMF’s official
regridder(data)API (sparse matmul vs ESMF’s internal routines).TPS kernel with degree=0 is the best overall choice.
SSH has the highest error (~5.8% at Icosphere-128) due to fine-scale ocean dynamics; all other variables are under 1.4%.
Increasing k beyond 16 gives diminishing returns – the error floor is set by the intermediate grid resolution, not interpolation order.
Use in deep learning#
Since the weight matrix W is a fixed sparse matrix, the transfer
operation \(y = Wx\) can be plugged directly into a deep-learning
graph as a sparse linear layer with no learnable parameters:
import torch
# Convert to PyTorch sparse tensor
W_coo = regridder.weights.tocoo()
indices = torch.LongTensor(np.vstack([W_coo.row, W_coo.col]))
values = torch.FloatTensor(W_coo.data)
W_torch = torch.sparse_coo_tensor(indices, values, W_coo.shape)
# Differentiable transfer
y = torch.sparse.mm(W_torch, x) # gradient flows through x