Benchmarks#
This page compares pyinterp with the tools commonly used for the same tasks: SciPy, xarray and xESMF. The libraries do not all compute the same thing in the same conditions, so the differences that matter are described before the timings.
Longitudes Wrap Around#
pyinterp handles periodic axes: a global grid stored in [0, 360) can be
queried in [-180, 180), beyond 360, and in its last cell, between the last
longitude and the first one plus 360. xarray.DataArray.interp and
scipy.interpolate.RegularGridInterpolator do not: they return NaN for
these points, or wrong values when extrapolation is enabled. With a grid of
0.25 degree storing \(\cos(\lambda)\):
Longitude |
-10 |
0.1 |
359.9 |
370 |
|---|---|---|---|---|
Exact value |
0.984808 |
0.999998 |
0.999998 |
0.984808 |
|
NaN |
0.999996 |
NaN |
NaN |
|
1.000381 |
0.999996 |
1.000008 |
1.001161 |
|
NaN |
0.999996 |
NaN |
NaN |
pyinterp |
0.984808 |
0.999996 |
0.999996 |
0.984808 |
With these libraries, the grid must be padded with a copy of its first
columns, or the coordinates brought back into the range of the grid. Only
scipy.ndimage.map_coordinates wraps an axis (mode="grid-wrap"), on
regular grids whose coordinates are converted to indexes by the user. In the
timings below, scipy and xarray are given points inside the longitudes of the
grid, which favors them.
Setup#
The timings were measured on an Apple M3 Max (10 performance and 4
efficiency cores) under macOS, with Python 3.13, NumPy 2.5.4, SciPy 1.18.1,
xarray 2026.9.0, xESMF 0.9.2 and the development version of pyinterp
following 2026.10.0. Each time is the best of at least three runs. The source
is a global grid of 0.25 degree (1440 × 720 values). pyinterp is timed on one
thread and on all the cores (num_threads=0, the default); the other
libraries run on one thread, except the k-d tree of SciPy, timed with all its
workers too.
To run the comparison on your machine, from a clone of the repository and with the libraries to compare installed:
python -m benchmarks.compare
Bilinear Interpolation at Scattered Points#
One million points drawn at random over the globe, such as the measurements of a satellite track:
Library |
Time |
Points per second |
Speed-up |
|---|---|---|---|
|
99.4 ms |
10.1 M |
1.0 |
|
106.9 ms |
9.4 M |
0.9 |
|
22.2 ms |
45.0 M |
4.5 |
pyinterp, one thread |
20.0 ms |
49.9 M |
5.0 |
pyinterp, all cores |
2.6 ms |
386.8 M |
38.4 |
On one thread, pyinterp is as fast as map_coordinates, which works on
indexes rather than coordinates, and five times faster than
RegularGridInterpolator, on which xarray.DataArray.interp relies.
Regridding#
The global grid is interpolated onto a grid of 0.1 degree, from 80° S to 80° N (5.76 million points), with the bilinear method:
Library |
Time |
Points per second |
Speed-up |
|---|---|---|---|
xESMF, applying the weights |
1457.8 ms |
4.0 M |
1.0 |
|
138.1 ms |
41.7 M |
10.6 |
|
31.7 ms |
181.8 M |
46.0 |
|
9.6 ms |
601.4 M |
152.2 |
|
3.5 ms |
1663.7 M |
421.1 |
xESMF first computes its weights, which took 42.2 s here; they can be saved and reused for other fields on the same grids. xarray and pyinterp exploit the structure of the target grid: each coordinate is located once on its axis instead of once per point. The results of pyinterp and xESMF differ by at most \(1.2 \times 10^{-5}\).
Conservative Regridding#
The global grid of 0.25 degree is coarsened to a global grid of 1 degree: each target cell receives the mean of the source values weighted by the area of their overlap with the cell, which preserves the integral of the field.
Library |
Time |
Source cells per second |
Speed-up |
|---|---|---|---|
xESMF, applying the weights |
118.3 ms |
8.8 M |
1.0 |
|
1.2 ms |
868.3 M |
99.0 |
|
0.7 ms |
1559.3 M |
177.9 |
xESMF first computes its weights, which took 11.2 s here. pyinterp needs no weights to compute: the overlap of two longitude/latitude boxes is the product of the overlaps of their sides, so the regridding applies two sparse 1D operators along the longitudes and the latitudes.
The two libraries do not draw the cells in the same way. pyinterp bounds them by meridians and parallels, while ESMF draws their edges as great circles, which bulge towards the poles: the results differ by up to \(2 \times 10^{-5}\) of the amplitude of a smooth field at mid-latitudes with the 1 degree grid, and \(4 \times 10^{-4}\) with a grid of 5 degrees (see Conservative Regridding).
xESMF also regrids curvilinear and unstructured source grids, with the bilinear and conservative methods, which pyinterp does not handle: its grid interpolators and its conservative regridding need rectilinear grids, and its R*-trees can only treat the nodes of the other grids as scattered points.
Cubic Interpolation#
One million points drawn at random, with a cubic method:
Library |
Time |
Points per second |
Speed-up |
|---|---|---|---|
|
54.4 ms |
18.4 M |
1.0 |
pyinterp bicubic, 4 × 4 window, one thread |
231.2 ms |
4.3 M |
0.2 |
pyinterp bicubic, 4 × 4 window, all cores |
22.9 ms |
43.7 M |
2.4 |
pyinterp bicubic, 6 × 6 window, one thread |
434.2 ms |
2.3 M |
0.1 |
pyinterp bicubic, 6 × 6 window, all cores |
42.0 ms |
23.8 M |
1.3 |
On one thread, map_coordinates is four times faster. The two methods
differ, though: map_coordinates evaluates a B-spline whose coefficients
are computed once for the whole grid (11 ms here, not included above), while
pyinterp fits a spline on a window around each point (half_window_size_x
and half_window_size_y, 3 by default). The global B-spline cannot handle
undefined values: a single NaN in the grid turns all its coefficients, and
then all the interpolated values, into NaN. The window of pyinterp only
affects the points whose window holds an undefined value, and the gaps can be
filled beforehand with pyinterp.fill.
Scattered Observations#
One million observations drawn at random over the globe are interpolated at one million other points, by inverse distance weighting of their 11 nearest neighbors. SciPy has no geodetic tree: its k-d tree indexes the Earth-centered (ECEF) coordinates of the observations, and the weighting is written with NumPy. The comparison is made twice.
Given the longitudes and latitudes of the points to query, pyinterp converts
them to ECEF coordinates itself (RTree3D(spheroid=...)), while the time
of SciPy includes their conversion by NumPy, on one thread:
Library |
Time |
Points per second |
Speed-up |
|---|---|---|---|
|
1849.7 ms |
0.5 M |
1.0 |
|
311.1 ms |
3.2 M |
5.9 |
pyinterp |
888.7 ms |
1.1 M |
2.1 |
pyinterp |
91.4 ms |
10.9 M |
20.2 |
Given the ECEF coordinates of the points, pyinterp works in Cartesian mode
(RTree3D()), exactly as the k-d tree of SciPy:
Library |
Time |
Points per second |
Speed-up |
|---|---|---|---|
|
1959.9 ms |
0.5 M |
1.0 |
|
252.9 ms |
4.0 M |
7.8 |
pyinterp |
855.6 ms |
1.2 M |
2.3 |
pyinterp |
90.4 ms |
11.1 M |
21.7 |
pyinterp is more than twice as fast as SciPy, on one thread as on all cores, with the same results to \(10^{-15}\); from one run to another, the timings vary by about 10 %. Its R*-tree is searched depth first, closest nodes first, and the points are processed along a space-filling curve, so that consecutive searches visit the same nodes. Working on longitudes and latitudes costs pyinterp less than 10 % of the time of the Cartesian mode. pyinterp also offers radial basis functions, kriging and window functions on the same tree.