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:

\[\mathbf{y} = W \, \mathbf{x}\]

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 as transform.

  • 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

nearest

1

Nearest-neighbor assignment. Fast but blocky.

idw

k

Inverse distance weighting: \(w_i = 1/d_i\), row-normalized.

gaussian

k

Gaussian kernel: \(w_i = \exp(-0.2\,(d_i/d_\min)^2)\), row-normalized.

barycentric

3

Projects onto the nearest triangle and computes barycentric coordinates. Exact for linear fields on the mesh surface but requires a trimesh proximity query (slower to build).

bilinear

4

Bilinear interpolation on quad faces (RectangularMesh only).

local_rbf

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:

\[\begin{split}\begin{bmatrix} \Phi & P \\ P^T & 0 \end{bmatrix} \begin{bmatrix} \mathbf{w} \\ \mathbf{c} \end{bmatrix} = \begin{bmatrix} \boldsymbol{\phi} \\ \mathbf{p}_q \end{bmatrix}\end{split}\]

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

nearest

3.64e-2

7.3%

1

0.20s

1.0ms

idw(k=5)

3.45e-2

6.9%

5

0.04s

2.1ms

gaussian(k=5)

3.15e-2

6.3%

5

0.05s

2.1ms

barycentric

2.20e-2

4.4%

3

81.3s

1.3ms

local_rbf(k=16, d=0)

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.

Uniform 1° intermediate (64,800 nodes)#

Config

SSH

SST

SHF

TAUX

TAUY

Mean

Build

Apply

xESMF bilinear

7.78%

1.50%

3.93%

2.60%

2.70%

3.70%

38ms

nearest

12.81%

4.00%

6.74%

7.45%

6.82%

7.56%

0.0s

0.21ms

rbf tps k=8 d=0

7.05%

1.06%

3.12%

1.79%

1.92%

2.99%

0.5s

0.34ms

rbf tps k=16 d=0

6.65%

0.84%

2.79%

1.22%

1.42%

2.58%

1.5s

0.47ms

rbf tps k=32 d=0

6.57%

0.75%

2.69%

1.00%

1.22%

2.44%

5.3s

0.74ms

rbf cubic k=16 d=0

6.89%

0.86%

2.87%

1.31%

1.51%

2.69%

1.4s

0.47ms

Icosphere-128 intermediate (163,842 nodes)#

Config

SSH

SST

SHF

TAUX

TAUY

Mean

Build

Apply

xESMF bilinear

7.78%

1.50%

3.93%

2.60%

2.70%

3.70%

38ms

nearest

7.53%

1.99%

3.29%

3.10%

3.59%

3.90%

0.1s

0.36ms

rbf tps k=8 d=0

5.81%

0.35%

1.37%

0.55%

0.73%

1.76%

0.8s

0.56ms

rbf tps k=16 d=0

5.77%

0.27%

1.34%

0.40%

0.57%

1.67%

2.6s

0.77ms

rbf tps k=32 d=0

5.80%

0.23%

1.27%

0.33%

0.53%

1.63%

8.9s

1.19ms

rbf cubic k=16 d=0

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