Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

ENH: Efficient interpolation on regular grids in arbitrary dimensions #3323

Closed
wants to merge 7 commits into from

4 participants

@andreas-h

Based on Johannes Buchner's regulargrid package (see https://github.com/JohannesBuchner/regulargrid), I implemented a new class RegularGridInterpolator for efficient linear and nearest-neighbour interpolation on regular (possibly unevenly spaced) grids in arbitrary dimensions.

I believe this is a significant addition to the scipy.interpolate package because the existing ND-interpolators perform a triangulation of the input points with becomes unfeasible with medium (~8) dimensions. There, by taking advantage of the regular grid structure, the interpolation can be sped up quite a bit.

Another advantage of this class is that it accepts anthing that can be appropriately indexed as values parameter, meaning that it is possible to use this interpolation object for interpolation of disk-based data sets (netCDF, pytables, ...).

I asked for permission about inclusion in Scipy; the (positive) answer is here: JohannesBuchner/regulargrid#2.

@pv
Owner
pv commented

Yes, this is a very commonly needed feature, and currently missing. What is needed is an interpolation method that does not make a copy of the data or do any expensive preprocessing, and this seems to fit the bill.

However, I suggest at the same time also adding a convenience function interpn. This should be similar in spirit to griddata i.e. a front-end to various interpolation methods, but for data on a rectangular grid, and having a method= keyword arg. The other methods (in addition to "nearest" and "linear" you add here) currently available is "splinef2d" (for RectBivariateSpline) and that's restricted to 2D.

The point of interpn would be to guide newbies away from interp2d (which probably should be deprecated at some point, due to its wonkiness and the fact that it's FITPACK-based).

The interface probably should mimic griddata by being interpn((x, y, z, ...), values, xi).

@andreas-h
@coveralls

Coverage Status

Changes Unknown when pulling e23aa50 on andreas-h:regulargrid into ** on scipy:master**.

@coveralls

Coverage Status

Changes Unknown when pulling e23aa50 on andreas-h:regulargrid into ** on scipy:master**.

@pv
Owner
pv commented

@andreas-h: RectBivariateSpline, and any other methods that may be eventually added

scipy/interpolate/interpolate.py
((33 lines not shown))
+
+ Contrary to LinearNDInterpolator and NearestNDInterpolator, this class
+ avoids expensive triangulation of the input data by taking advantage of the
+ regular grid structure.
+
+ See also
+ --------
+ NearestNDInterpolator : Nearest neighbour interpolation on unstructured
+ data in N dimensions
+
+ LinearNDInterpolator : Piecewise linear interpolant on unstructured data
+ in N dimensions
+
+ References
+ ----------
+ - Python package *regulargrid* by Johannes Buchner, see https://pypi.python.org/pypi/regulargrid/
@pv Owner
pv added a note

RST reference format

.. [1] Python package ...
.. [2] Foo bar. Lorem ipsum.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@andreas-h
@pv
Owner
pv commented

Yes, it's 2D only (as I noted above). It needs to be documented and an error raised for other dimensions.

The optional parameters shouldn't be available in interpn, I believe. This is intended to be a simple interface, for interpolation only. People who want more control, can use the interpolators directly.

@pv pv added this to the 0.14.0 milestone
@pv
Owner
pv commented

We can try to get this (and interpn) to 0.14.0.

@andreas-h

Currently in the nearest neighbor implementation, I'm using yi <= .5 as condition to determine if a given coordinate "belongs" to the next upper or lower gridpoint. Is there any convention on how to handle this? Should I explicitly mention this behaviour in the docstring?

@coveralls

Coverage Status

Changes Unknown when pulling a68a922 on andreas-h:regulargrid into ** on scipy:master**.

@coveralls

Coverage Status

Changes Unknown when pulling 8fbf99b on andreas-h:regulargrid into ** on scipy:master**.

@andreas-h

@pv Do you think it's a good idea to add bounds_error and fill_value to the interpn interface?

scipy/interpolate/interpolate.py
((199 lines not shown))
+ data in N dimensions
+
+ LinearNDInterpolator : Piecewise linear interpolant on unstructured data
+ in N dimensions
+
+ RegularGridInterpolator : Linear and nearest-neighbor Interpolation on a
+ regular grid in arbitrary dimensions
+
+ RectBivariateSpline : Bivariate spline approximation over a rectangular mesh
+
+ """
+ # sanity check 'method' kwarg
+ if method not in ["linear", "nearest", "splinef2d"]:
+ raise ValueError("interpn only understands the methods 'linear', "
+ "'nearest', and 'splinef2d'. You provided "
+ "{}".format(method))
@pv Owner
pv added a note

This is not valid in Python 2.6 which we still support. Better use the % string formatting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pv pv commented on the diff
scipy/interpolate/interpolate.py
((194 lines not shown))
+ .. versionadded:: 0.14
+
+ See also
+ --------
+ NearestNDInterpolator : Nearest neighbour interpolation on unstructured
+ data in N dimensions
+
+ LinearNDInterpolator : Piecewise linear interpolant on unstructured data
+ in N dimensions
+
+ RegularGridInterpolator : Linear and nearest-neighbor Interpolation on a
+ regular grid in arbitrary dimensions
+
+ RectBivariateSpline : Bivariate spline approximation over a rectangular mesh
+
+ """
@pv Owner
pv added a note

I think an example would be very useful to add.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
scipy/interpolate/interpolate.py
((208 lines not shown))
+
+ """
+ # sanity check 'method' kwarg
+ if method not in ["linear", "nearest", "splinef2d"]:
+ raise ValueError("interpn only understands the methods 'linear', "
+ "'nearest', and 'splinef2d'. You provided "
+ "{}".format(method))
+ ndim = values.ndim
+ if ndim > 2 and method == "splinef2d":
+ raise ValueError("The method spline2fd can only be used for "
+ "2-dimensional input data")
+ # sanity check consistency of input dimensions
+ if not len(points) == ndim:
+ raise ValueError("There are {} point arrays, but values has {} "
+ "dimensions".format(len(points), ndim))
+ # sanity check input grid
@pv Owner
pv added a note

Style nitpick: empty line before comment (also helps to make the code more readable)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
scipy/interpolate/interpolate.py
((221 lines not shown))
+ raise ValueError("There are {} point arrays, but values has {} "
+ "dimensions".format(len(points), ndim))
+ # sanity check input grid
+ for i, p in enumerate(points):
+ if not np.all(np.diff(p) > 0.):
+ raise ValueError("The points in dimension {} must be strictly "
+ "ascending".format(i))
+ if not np.asarray(p).ndim == 1:
+ raise ValueError("The points in dimension {} must be "
+ "1-dimensional".format(i))
+ if not values.shape[i] == len(p):
+ raise ValueError("There are {} points and {} values in "
+ "dimension {}".format(len(p), values.shape[i], i))
+ grid = tuple([np.asarray(p) for p in points])
+ # sanity check requested xi
+ xi = np.atleast_2d(xi)
@pv Owner
pv added a note

This should support broadcasting --- there's a _ndim_coords_from_arrays helper function that's used in griddata.

griddata expects xi to be of shape (M, D). My intention was to allow 1D xi of shape (D, ) as well. And I don't see how in that case _ndim_coords_from_arrays helps.

What exactly is the problem with atleast_2d? Is it better to drop support for 1D xi in favor of the solution used in griddata?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pv
Owner
pv commented
@andreas-h
@pv
Owner
pv commented

I don't see problems with that.

@coveralls

Coverage Status

Changes Unknown when pulling e5918ef on andreas-h:regulargrid into ** on scipy:master**.

@ev-br
Collaborator

Re nearest neighbor implementation (y <= 0.5 etc). While I don't have any intelligent suggestions here, I just note that there's some discussion on data model vs pixel model in the roadmap under ndimage.

@pv
Owner
pv commented

I think we want here nearest-neighbor semantics, ie. voronoi cells. I.e. 0.5 seems fine to me.

@pv pv added needs-work PR labels
@andreas-h

one further question is about the kwarg method or kind. interp1d and interp2d use kind, griddata uses method. I personally prefer method, so would go with this for RegularGridInterpolator and interpn.

What do you think?

@pv
Owner
pv commented

I'd go with method, as it's used a bit more often in scipy than kind.

@coveralls

Coverage Status

Coverage remained the same when pulling c5632f7 on andreas-h:regulargrid into 32cd96d on scipy:master.

@coveralls

Coverage Status

Coverage remained the same when pulling c5632f7 on andreas-h:regulargrid into 32cd96d on scipy:master.

@coveralls

Coverage Status

Coverage remained the same when pulling c5632f7 on andreas-h:regulargrid into 32cd96d on scipy:master.

scipy/interpolate/interpolate.py
((238 lines not shown))
+
+ # sanity check input grid
+ for i, p in enumerate(points):
+ if not np.all(np.diff(p) > 0.):
+ raise ValueError("The points in dimension %d must be strictly "
+ "ascending" % i)
+ if not np.asarray(p).ndim == 1:
+ raise ValueError("The points in dimension %d must be "
+ "1-dimensional" % i)
+ if not values.shape[i] == len(p):
+ raise ValueError("There are %d points and %d values in "
+ "dimension %d" % (len(p), values.shape[i], i))
+ grid = tuple([np.asarray(p) for p in points])
+
+ # sanity check requested xi
+ xi = np.atleast_2d(xi)
@pv Owner
pv added a note

I do not fully understand your point """griddata expects xi to be of shape (M, D). My intention was to allow 1D xi of shape (D, ) as well""". You mean you want to evaluate at a single point?

However, I think uniform interface with griddata is of high priority here. The xi parameter has exactly the same meaning, so it should behave the same way.

The problem with atleast_2d is that it does not broadcast. If you want to evaluate it on a grid, you need to do a meshgrid+ravel dance on the user side.

@pv Owner
pv added a note

The other option is to add an "expected dimension" argument to _ndim_coords_from_arrays, and transpose 1D the array if >1D is expected.
Can be done if it doesn't cause some inputs to be ambiguous. (It probably doesn't cause ambiguity.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pv
Owner
pv commented

I think this is almost ready now, and should make it to 0.14.0.

@pv
Owner
pv commented

Some missing stuff is added in: pv@andreas-h:regulargrid...pr-3323

  • uniform xi argument handling in interpn and griddata (and it does as suggested above for 1D inputs)
  • support for non-scalar valued values in regular grid interpolation
  • improve tests and fix up array_like handling at some points
  • minor doc improvements
@andreas-h

Looks great! I wanted to add the lower-dim xi case last night but couldn't figure out how to do the proper slicing ... So thanks for reading my thoughts and implementing them =)

Do I need to do anything with this PR or do you do the necessary git magic before merging?

@andreas-h

I hope I can still write an example for interpn tonight or tomorrow, but don't want to promise that. So probably you go ahead merging and I can submit the example PR against master then.

@pv
Owner
pv commented

@andreas-h: comments? I'd be ready to merge to 0.14.0 with those changes.

@pv
Owner
pv commented

Ok, I'll merge this and any remaining enhancements can then be made later.

@pv
Owner
pv commented

Thanks, merged in a90dc28

@pv pv closed this
@andreas-h andreas-h deleted the andreas-h:regulargrid branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
9 doc/release/0.14.0-notes.rst
@@ -36,6 +36,15 @@ If performance is critical, sorting can be turned off by using the new
Functionality for evaluation of bivariate spline derivatives in
``scipy.interpolate`` has been added.
+A new wrapper function `scipy.interpolate.interpn` for interpolation on regular
+grids has been added. `interpn` supports linear and nearest-neighbor
+interpolation in arbitrary dimensions and spline interpolation in two
+dimensions.
+
+Functionality for fast interpolation on regular, unevenly spaced grids
+in arbitrary dimensions has been added as
+`scipy.interpolate.RegularGridInterpolator` .
+
``scipy.linalg`` improvements
-----------------------------
View
3  scipy/interpolate/__init__.py
@@ -50,7 +50,10 @@
For data on a grid:
.. autosummary::
+ :toctree: generated/
+ interpn
+ RegularGridInterpolator
RectBivariateSpline
.. seealso:: `scipy.ndimage.interpolation.map_coordinates`
View
295 scipy/interpolate/interpolate.py
@@ -3,7 +3,10 @@
from __future__ import division, print_function, absolute_import
__all__ = ['interp1d', 'interp2d', 'spline', 'spleval', 'splmake', 'spltopp',
- 'ppform', 'lagrange', 'PPoly', 'BPoly']
+ 'ppform', 'lagrange', 'PPoly', 'BPoly', 'RegularGridInterpolator',
+ 'interpn']
+
+import itertools
from numpy import shape, sometrue, array, transpose, searchsorted, \
ones, logical_or, atleast_1d, atleast_2d, ravel, \
@@ -23,6 +26,7 @@
from . import _fitpack
from .polyint import _Interpolator1D
from . import _ppoly
+from .fitpack2 import RectBivariateSpline
def reduce_sometrue(a):
all = a
@@ -1391,6 +1395,295 @@ def _raise_degree(c, d):
return out
+class RegularGridInterpolator(object):
+ """
+ Interpolation on a regular grid in arbitrary dimensions
+
+ The data must be defined on a regular grid; the grid spacing however may be
+ uneven. Linear and nearest-neighbour interpolation are supported. After
+ setting up the interpolator object, the interpolation method (*linear* or
+ *nearest*) may be chosen at each evaluation.
+
+ .. versionadded:: 0.14
+
+ Parameters
+ ----------
+ points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
+ The points defining the regular grid in n dimensions.
+
+ values : anything of dtype float that can be indexed like an ndarray of shape (m1, ..., mn)
+ The data on the regular grid in n dimensions.
+
+ method : str
+ The method of interpolation to perform. Supported are "linear" and
+ "nearest". This parameter will become the default for the object's
+ ``__call__`` method.
+
+ bounds_error : bool, optional
+ If True, when interpolated values are requested outside of the
+ domain of the input data, a ValueError is raised.
+ If False, then `fill_value` is used.
+
+ fill_value : number, optional
+ If provided, the value to use for points outside of the
+ interpolation domain. If None, values outside
+ the domain are extrapolated.
+
+ Methods
+ -------
+ __call__
+
+ Notes
+ -----
+ Contrary to LinearNDInterpolator and NearestNDInterpolator, this class
+ avoids expensive triangulation of the input data by taking advantage of the
+ regular grid structure.
+
+ See also
+ --------
+ NearestNDInterpolator : Nearest neighbour interpolation on unstructured
+ data in N dimensions
+
+ LinearNDInterpolator : Piecewise linear interpolant on unstructured data
+ in N dimensions
+
+ References
+ ----------
+ .. [1] Python package *regulargrid* by Johannes Buchner, see
+ https://pypi.python.org/pypi/regulargrid/
+ .. [2] Trilinear interpolation. (2013, January 17). In Wikipedia, The Free
+ Encyclopedia. Retrieved 27 Feb 2013 01:28.
+ http://en.wikipedia.org/w/index.php?title=Trilinear_interpolation&oldid=533448871
+ .. [3] Weiser, Alan, and Sergio E. Zarantonello. "A note on piecewise linear
+ and multilinear table interpolation in many dimensions." MATH.
+ COMPUT. 50.181 (1988): 189-196.
+ http://www.ams.org/journals/mcom/1988-50-181/S0025-5718-1988-0917826-0/S0025-5718-1988-0917826-0.pdf
+
+ """
+ # this class is based on code originally programmed by Johannes Buchner,
+ # see https://github.com/JohannesBuchner/regulargrid
+
+ def __init__(self, points, values, method="linear", bounds_error=True,
+ fill_value=np.nan):
+ if method not in ["linear", "nearest"]:
+ raise ValueError("Method '%s' is not defined" % method)
+ self.method = method
+ self.bounds_error = bounds_error
+ if fill_value is not None and not isinstance(fill_value, float):
+ raise ValueError("fill_value must be either 'None' or a float")
+ self.fill_value = fill_value
+ if not len(points) == values.ndim:
+ raise ValueError("There are %d point arrays, but values has %d "
+ "dimensions" % (len(points), values.ndim))
+ for i, p in enumerate(points):
+ if not np.all(np.diff(p) > 0.):
+ raise ValueError("The points in dimension %d must be strictly "
+ "ascending" % i)
+ if not np.asarray(p).ndim == 1:
+ raise ValueError("The points in dimension %d must be "
+ "1-dimensional" % i)
+ if not values.shape[i] == len(p):
+ raise ValueError("There are %d points and %d values in "
+ "dimension %d" % (len(p), values.shape[i], i))
+ self.grid = tuple([np.asarray(p) for p in points])
+ self.values = values
+
+ def __call__(self, xi, method=None):
+ """
+ interpolation at coordinates
+
+ Parameters
+ ----------
+ xi : ndarray of shape (ndim, ) or (nsamplepoints, ndim)
+ The coordinates to sample the gridded data at
+
+ method : str
+ The method of interpolation to perform. Supported are "linear" and
+ "nearest".
+
+ """
+ method = self.method if method is None else method
+ if method not in ["linear", "nearest"]:
+ raise ValueError("Method '%s' is not defined" % method)
+ xi = np.atleast_2d(xi)
+ if not xi.shape[1] == len(self.grid):
+ raise ValueError("The requested sample points xi have dimension "
+ "%d, but this RegularGridInterpolator has "
+ "dimension %d" % (xi.shape[1], len(self.grid)))
+ if self.bounds_error:
+ for i, p in enumerate(xi.T):
+ if not np.logical_and(np.all(self.grid[i][0] <= p),
+ np.all(p <= self.grid[i][-1])):
+ raise ValueError("One of the requested xi is out of bounds "
+ "in dimension %d" % i)
+
+ indices, norm_distances, out_of_bounds = self._find_indices(xi.T)
+ if method == "linear":
+ result = self._evaluate_linear(indices, norm_distances, out_of_bounds)
+ elif method == "nearest":
+ result = self._evaluate_nearest(indices, norm_distances, out_of_bounds)
+ if not self.bounds_error and self.fill_value is not None:
+ result[out_of_bounds] = self.fill_value
+ return result
+
+ def _evaluate_linear(self, indices, norm_distances, out_of_bounds):
+ # find relevant values
+ # each i and i+1 represents a edge
+ edges = itertools.product(*[[i, i + 1] for i in indices])
+ values = 0.
+ for edge_indices in edges:
+ weight = 1.
+ for ei, i, yi in zip(edge_indices, indices, norm_distances):
+ weight *= np.where(ei == i, 1 - yi, yi)
+ values += self.values[edge_indices] * weight
+ return values
+
+ def _evaluate_nearest(self, indices, norm_distances, out_of_bounds):
+ idx_res = []
+ for i, yi in zip(indices, norm_distances):
+ idx_res.append(np.where(yi <= .5, i, i + 1))
+ return self.values[idx_res]
+
+ def _find_indices(self, xi):
+ # find relevant edges between which xi are situated
+ indices = []
+ # compute distance to lower edge in unity units
+ norm_distances = []
+ # check for out of bounds xi
+ out_of_bounds = np.zeros((xi.shape[1]), dtype=bool)
+ # iterate through dimensions
+ for x, grid in zip(xi, self.grid):
+ i = np.searchsorted(grid, x) - 1
+ i[i < 0] = 0
+ i[i > grid.size - 2] = grid.size - 2
+ indices.append(i)
+ norm_distances.append((x - grid[i]) /
+ (grid[i + 1] - grid[i]))
+ if not self.bounds_error:
+ out_of_bounds += x < grid[0]
+ out_of_bounds += x > grid[-1]
+ return indices, norm_distances, out_of_bounds
+
+
+def interpn(points, values, xi, method="linear", bounds_error=True,
+ fill_value=np.nan):
+ """
+ Multidimensional interpolation on regular grids.
+
+ .. versionadded:: 0.14
+
+ Parameters
+ ----------
+ points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
+ The points defining the regular grid in n dimensions.
+
+ values : anything of dtype float that can be indexed like an ndarray of shape (m1, ..., mn)
+ The data on the regular grid in n dimensions.
+
+ xi : ndarray of shape (nsamplepoints, n)
+ The coordinates to sample the gridded data at
+
+ method : str
+ The method of interpolation to perform. Supported are "linear" and
+ "nearest", and "splinef2d". "splinef2d" is only supported for
+ 2-dimensional data.
+
+ bounds_error : bool, optional
+ If True, when interpolated values are requested outside of the
+ domain of the input data, a ValueError is raised.
+ If False, then `fill_value` is used.
+
+ fill_value : number, optional
+ If provided, the value to use for points outside of the
+ interpolation domain. If None, values outside
+ the domain are extrapolated. Extrapolation is not supported by method
+ "splinef2d".
+
+ See also
+ --------
+ NearestNDInterpolator : Nearest neighbour interpolation on unstructured
+ data in N dimensions
+
+ LinearNDInterpolator : Piecewise linear interpolant on unstructured data
+ in N dimensions
+
+ RegularGridInterpolator : Linear and nearest-neighbor Interpolation on a
+ regular grid in arbitrary dimensions
+
+ RectBivariateSpline : Bivariate spline approximation over a rectangular mesh
+
+ """
@pv Owner
pv added a note

I think an example would be very useful to add.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # sanity check 'method' kwarg
+ if method not in ["linear", "nearest", "splinef2d"]:
+ raise ValueError("interpn only understands the methods 'linear', "
+ "'nearest', and 'splinef2d'. You provided %s." %
+ method)
+ ndim = values.ndim
+ if ndim > 2 and method == "splinef2d":
+ raise ValueError("The method spline2fd can only be used for "
+ "2-dimensional input data")
+ if not bounds_error and fill_value is None and method == "splinef2d":
+ raise ValueError("The method spline2fd does not support extrapolation.")
+
+ # sanity check consistency of input dimensions
+ if not len(points) == ndim:
+ raise ValueError("There are %d point arrays, but values has %d "
+ "dimensions" % (len(points), ndim))
+
+ # sanity check input grid
+ for i, p in enumerate(points):
+ if not np.all(np.diff(p) > 0.):
+ raise ValueError("The points in dimension %d must be strictly "
+ "ascending" % i)
+ if not np.asarray(p).ndim == 1:
+ raise ValueError("The points in dimension %d must be "
+ "1-dimensional" % i)
+ if not values.shape[i] == len(p):
+ raise ValueError("There are %d points and %d values in "
+ "dimension %d" % (len(p), values.shape[i], i))
+ grid = tuple([np.asarray(p) for p in points])
+
+ # sanity check requested xi
+ if not xi.ndim == 2:
+ raise ValueError("The requested sample points xi must have "
+ "shape (nsamplepoints, ndim), so xi must have "
+ "dimension 2. The xi you provided has shape %s." %
+ xi.shape)
+ if not xi.shape[1] == len(grid):
+ raise ValueError("The requested sample points xi have dimension "
+ "%d, but this RegularGridInterpolator has "
+ "dimension %d" % (xi.shape[1], len(grid)))
+ for i, p in enumerate(xi.T):
+ if bounds_error and not np.logical_and(np.all(grid[i][0] <= p),
+ np.all(p <= grid[i][-1])):
+ raise ValueError("One of the requested xi is out of bounds "
+ "in dimension %d" % i)
+
+ # perform interpolation
+ if method == "linear":
+ interp = RegularGridInterpolator(points, values, method="linear",
+ bounds_error=bounds_error,
+ fill_value=fill_value)
+ return interp(xi)
+ elif method == "nearest":
+ interp = RegularGridInterpolator(points, values, method="nearest",
+ bounds_error=bounds_error,
+ fill_value=fill_value)
+ return interp(xi)
+ elif method == "splinef2d":
+ # RectBivariateSpline doesn't support fill_value; we need to wrap here
+ idx_valid = np.all((grid[0][0] <= xi[:, 0], xi[:, 0] <= grid[0][-1],
+ grid[1][0] <= xi[:, 1], xi[:, 1] <= grid[1][-1]),
+ axis=0)
+ result = np.empty_like(xi[:, 0])
+
+ # make a copy of values for RectBivariateSpline
+ interp = RectBivariateSpline(points[0], points[1], values[:])
+ result[idx_valid] = interp.ev(xi[idx_valid, 0], xi[idx_valid, 1])
+ result[np.logical_not(idx_valid)] = fill_value
+ return result
+
+
# backward compatibility wrapper
class ppform(PPoly):
"""
View
309 scipy/interpolate/tests/test_interpolate.py
@@ -1,5 +1,6 @@
from __future__ import division, print_function, absolute_import
+import itertools
import warnings
from numpy.testing import (assert_, assert_equal, assert_almost_equal,
@@ -12,7 +13,9 @@
from scipy.lib._version import NumpyVersion
from scipy.interpolate import (interp1d, interp2d, lagrange, PPoly, BPoly,
- ppform, splrep, splev, splantider, splint, sproot)
+ ppform, splrep, splev, splantider, splint, sproot, interpn,
+ RegularGridInterpolator, LinearNDInterpolator,
+ NearestNDInterpolator, RectBivariateSpline)
from scipy.interpolate import _ppoly
@@ -1118,5 +1121,309 @@ def _ppoly_eval_2(coeffs, breaks, xnew, fill=np.nan):
return res
+class TestRegularGridInterpolator(TestCase):
+ def test_linear_xi1d(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values)
+ sample = np.asarray([0.1, 0.1, 10., 9.])
+ wanted = 1001.1
+ assert_array_almost_equal(interp(sample), wanted)
+
+ def test_linear_xi3d(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values)
+ sample = np.asarray([[0.1, 0.1, 1., .9], [0.2, 0.1, .45, .8],
+ [0.5, 0.5, .5, .5]])
+ wanted = np.asarray([1001.1, 846.2, 555.5])
+ assert_array_almost_equal(interp(sample), wanted)
+
+ def test_nearest(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values, method="nearest")
+ sample = np.asarray([0.1, 0.1, .9, .9])
+ wanted = 1100.
+ assert_array_almost_equal(interp(sample), wanted)
+ sample = np.asarray([0.1, 0.1, 0.1, 0.1])
+ wanted = 0.
+ assert_array_almost_equal(interp(sample), wanted)
+ sample = np.asarray([0., 0., 0., 0.])
+ wanted = 0.
+ assert_array_almost_equal(interp(sample), wanted)
+ sample = np.asarray([1., 1., 1., 1.])
+ wanted = 1111.
+ assert_array_almost_equal(interp(sample), wanted)
+ sample = np.asarray([0.1, 0.4, 0.6, 0.9])
+ wanted = 1055.
+ assert_array_almost_equal(interp(sample), wanted)
+
+ def test_linear_edges(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values)
+ sample = np.asarray([[0., 0., 0., 0.], [1., 1., 1., 1.]])
+ wanted = np.asarray([0., 1111.])
+ assert_array_almost_equal(interp(sample), wanted)
+
+ def test_valid_create(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.), (0., 1., .5)]
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis]
+ values1 = values[np.newaxis, :]
+ values = (values0 + values1 * 10)
+ assert_raises(ValueError, RegularGridInterpolator, points, values)
+ points = [((0., .5, 1.), ), (0., .5, 1.)]
+ assert_raises(ValueError, RegularGridInterpolator, points, values)
+ points = [(0., .5, .75, 1.), (0., .5, 1.)]
+ assert_raises(ValueError, RegularGridInterpolator, points, values)
+ points = [(0., .5, 1.), (0., .5, 1.), (0., .5, 1.)]
+ assert_raises(ValueError, RegularGridInterpolator, points, values)
+ points = [(0., .5, 1.), (0., .5, 1.)]
+ assert_raises(ValueError, RegularGridInterpolator, points, values,
+ method="undefmethod")
+
+ def test_valid_call(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values)
+ sample = np.asarray([[0., 0., 0., 0.], [1., 1., 1., 1.]])
+ assert_raises(ValueError, interp, sample, "undefmethod")
+ sample = np.asarray([[0., 0., 0.], [1., 1., 1.]])
+ assert_raises(ValueError, interp, sample)
+ sample = np.asarray([[0., 0., 0., 0.], [1., 1., 1., 1.1]])
+ assert_raises(ValueError, interp, sample)
+
+ def test_out_of_bounds_extrap(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values, bounds_error=False,
+ fill_value=None)
+ sample = np.asarray([[-.1, -.1, -.1, -.1], [1.1, 1.1, 1.1, 1.1],
+ [21, 2.1, -1.1, -11], [2.1, 2.1, -1.1, -1.1]])
+ wanted = np.asarray([0., 1111., 11., 11.])
+ assert_array_almost_equal(interp(sample, method="nearest"), wanted)
+ wanted = np.asarray([-111.1, 1222.1, -11068., -1186.9])
+ assert_array_almost_equal(interp(sample, method="linear"), wanted)
+
+ def test_out_of_bounds_extrap2(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values, bounds_error=False,
+ fill_value=None)
+ sample = np.asarray([[-.1, -.1, -.1, -.1], [1.1, 1.1, 1.1, 1.1],
+ [21, 2.1, -1.1, -11], [2.1, 2.1, -1.1, -1.1]])
+ wanted = np.asarray([0., 11., 11., 11.])
+ assert_array_almost_equal(interp(sample, method="nearest"), wanted)
+ wanted = np.asarray([-12.1, 133.1, -1069., -97.9])
+ assert_array_almost_equal(interp(sample, method="linear"), wanted)
+
+ def test_out_of_bounds_fill(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values, bounds_error=False,
+ fill_value=np.nan)
+ sample = np.asarray([[-.1, -.1, -.1, -.1], [1.1, 1.1, 1.1, 1.1],
+ [2.1, 2.1, -1.1, -1.1]])
+ wanted = np.asarray([np.nan, np.nan, np.nan])
+ assert_array_almost_equal(interp(sample, method="nearest"), wanted)
+ assert_array_almost_equal(interp(sample, method="linear"), wanted)
+ sample = np.asarray([[0.1, 0.1, 1., .9], [0.2, 0.1, .45, .8],
+ [0.5, 0.5, .5, .5]])
+ wanted = np.asarray([1001.1, 846.2, 555.5])
+ assert_array_almost_equal(interp(sample), wanted)
+
+ def test_nearest_compare_qhull(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values, method="nearest")
+ points_qhull = itertools.product(*points)
+ points_qhull = [p for p in points_qhull]
+ points_qhull = np.asarray(points_qhull)
+ values_qhull = values.reshape(-1)
+ interp_qhull = NearestNDInterpolator(points_qhull, values_qhull)
+ sample = np.asarray([[0.1, 0.1, 1., .9], [0.2, 0.1, .45, .8],
+ [0.5, 0.5, .5, .5]])
+ assert_array_almost_equal(interp(sample), interp_qhull(sample))
+
+ def test_linear_compare_qhull(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 4
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp = RegularGridInterpolator(points, values)
+ points_qhull = itertools.product(*points)
+ points_qhull = [p for p in points_qhull]
+ points_qhull = np.asarray(points_qhull)
+ values_qhull = values.reshape(-1)
+ interp_qhull = LinearNDInterpolator(points_qhull, values_qhull)
+ sample = np.asarray([[0.1, 0.1, 1., .9], [0.2, 0.1, .45, .8],
+ [0.5, 0.5, .5, .5]])
+ assert_array_almost_equal(interp(sample), interp_qhull(sample))
+
+class TestInterpN(TestCase):
+ def test_spline_2d(self):
+ x = np.arange(1, 6)
+ x = np.array([.5, 2., 3., 4., 5.5])
+ y = np.arange(1, 6)
+ y = np.array([.5, 2., 3., 4., 5.5])
+ z = np.array([[1, 2, 1, 2, 1], [1, 2, 1, 2, 1], [1, 2, 3, 2, 1],
+ [1, 2, 2, 2, 1], [1, 2, 1, 2, 1]])
+ lut = RectBivariateSpline(x, y, z)
+
+ xi = np.array([[1, 2.3, 5.3, 0.5, 3.3, 1.2, 3],
+ [1, 3.3, 1.2, 4.0, 5.0, 1.0, 3]]).T
+ assert_array_almost_equal(interpn((x, y), z, xi, method="splinef2d"),
+ lut.ev(xi[:, 0], xi[:, 1]))
+
+ def test_spline_2d_outofbounds(self):
+ x = np.array([.5, 2., 3., 4., 5.5])
+ y = np.array([.5, 2., 3., 4., 5.5])
+ z = np.array([[1, 2, 1, 2, 1], [1, 2, 1, 2, 1], [1, 2, 3, 2, 1],
+ [1, 2, 2, 2, 1], [1, 2, 1, 2, 1]])
+ lut = RectBivariateSpline(x, y, z)
+
+ xi = np.array([[1, 2.3, 6.3, 0.5, 3.3, 1.2, 3],
+ [1, 3.3, 1.2, -4.0, 5.0, 1.0, 3]]).T
+ actual = interpn((x, y), z, xi, method="splinef2d",
+ bounds_error=False, fill_value=999.99)
+ expected = lut.ev(xi[:, 0], xi[:, 1])
+ expected[2:4] = 999.99
+ assert_array_almost_equal(actual, expected)
+
+ # no extrapolation for splinef2d
+ assert_raises(ValueError, interpn, (x, y), z, xi, method="splinef2d",
+ bounds_error=False, fill_value=None)
+
+ def test_linear_4d(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp_rg = RegularGridInterpolator(points, values)
+ sample = np.asarray([[0.1, 0.1, 10., 9.]])
+ wanted = interpn(points, values, sample, method="linear")
+ assert_array_almost_equal(interp_rg(sample), wanted)
+
+ def test_4d_linear_outofbounds(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ sample = np.asarray([[0.1, -0.1, 10.1, 9.]])
+ wanted = 999.99
+ actual = interpn(points, values, sample, method="linear",
+ bounds_error=False, fill_value=999.99)
+ assert_array_almost_equal(actual, wanted)
+
+ def test_nearest_4d(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ interp_rg = RegularGridInterpolator(points, values, method="nearest")
+ sample = np.asarray([[0.1, 0.1, 10., 9.]])
+ wanted = interpn(points, values, sample, method="nearest")
+ assert_array_almost_equal(interp_rg(sample), wanted)
+
+ def test_4d_nearest_outofbounds(self):
+ # create a 4d grid of 3 points in each dimension
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ sample = np.asarray([[0.1, -0.1, 10.1, 9.]])
+ wanted = 999.99
+ actual = interpn(points, values, sample, method="nearest",
+ bounds_error=False, fill_value=999.99)
+ assert_array_almost_equal(actual, wanted)
+
+ def test_raises_xi1d(self):
+ # verify that xi has to be 2D
+ points = [(0., .5, 1.)] * 2 + [(0., 5., 10.)] * 2
+ values = np.asarray([0., .5, 1.])
+ values0 = values[:, np.newaxis, np.newaxis, np.newaxis]
+ values1 = values[np.newaxis, :, np.newaxis, np.newaxis]
+ values2 = values[np.newaxis, np.newaxis, :, np.newaxis]
+ values3 = values[np.newaxis, np.newaxis, np.newaxis, :]
+ values = (values0 + values1 * 10 + values2 * 100 + values3 * 1000)
+ sample = np.asarray([0.1, -0.1, 10.1, 9.])
+ assert_raises(ValueError, interpn, points, values, sample)
+
if __name__ == "__main__":
run_module_suite()
Something went wrong with that request. Please try again.