I have angular data on a domain that is wrapped at pi
radians (i.e. 0 = pi
). The data are 2D
, where one dimension represents the angle. I need to interpolate this data onto another grid in a wrapped way.
In one dimension, the np.interp
function takes a period kwarg
(for NumPy
1.10 and later):
http://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html
This is exactly what I need, but I need it in two dimensions. I'm currently just stepping through columns in my array and using np.interp
, but this is of course slow.
Anything out there that could achieve this same outcome but faster?
np.interp
worksUse the source, Luke!
The numpy doc for np.interp
makes the source particularly easy to find, since it has the link right there, along with the documentation. Let's go through this, line by line.
First, recall the parameters:
"""
x : array_like
The x-coordinates of the interpolated values.
xp : 1-D sequence of floats
The x-coordinates of the data points, must be increasing if argument
`period` is not specified. Otherwise, `xp` is internally sorted after
normalizing the periodic boundaries with ``xp = xp % period``.
fp : 1-D sequence of floats
The y-coordinates of the data points, same length as `xp`.
period : None or float, optional
A period for the x-coordinates. This parameter allows the proper
interpolation of angular x-coordinates. Parameters `left` and `right`
are ignored if `period` is specified.
"""
Let's take a simple example of a triangular wave while going through this:
xp = np.array([-np.pi/2, -np.pi/4, 0, np.pi/4])
fp = np.array([0, -1, 0, 1])
x = np.array([-np.pi/8, -5*np.pi/8]) # Peskiest points possible }:)
period = np.pi
Now, I start off with the period != None
branch in the source code, after all the type-checking happens:
# normalizing periodic boundaries
x = x % period
xp = xp % period
This just ensures that all values of x
and xp
supplied are between 0
and period
. So, since the period is pi
, but we specified x
and xp
to be between -pi/2
and pi/2
, this will adjust for that by adding pi
to all values in the range [-pi/2, 0)
, so that they effectively appear after pi/2
. So our xp
now reads [pi/2, 3*pi/4, 0, pi/4]
.
asort_xp = np.argsort(xp)
xp = xp[asort_xp]
fp = fp[asort_xp]
This is just ordering xp
in increasing order. This is especially required after performing that modulo operation in the previous step. So, now xp
is [0, pi/4, pi/2, 3*pi/4]
. fp
has also been shuffled accordingly, [0, 1, 0, -1]
.
xp = np.concatenate((xp[-1:]-period, xp, xp[0:1]+period))
fp = np.concatenate((fp[-1:], fp, fp[0:1]))
return compiled_interp(x, xp, fp, left, right) # Paraphrasing a little
np.interp
does linear interpolation. When trying to interpolate between two points a
and b
present in xp
, it only uses the values of f(a)
and f(b)
(i.e., the values of fp
at the corresponding indices). So what np.interp
is doing in this last step is to take the point xp[-1]
and put it in front of the array, and take the point xp[0]
and put it after the array, but after subtracting and adding one period respectively. So you now have a new xp
that looks like [-pi/4, 0, pi/4, pi/2, 3*pi/4, pi]
. Likewise, fp[0]
and fp[-1]
have been concatenated around, so fp
is now [-1, 0, 1, 0, -1, 0]
.
Note that after the modulo operations, x
had been brought into the [0, pi]
range too, so x
is now [7*pi/8, 3*pi/8]
. Which lets you easily see that you'll get back [-0.5, 0.5]
.
Say you have a grid and some values. Let's just take all values to be between [0, pi]
off the bat so we don't need to worry about modulos and shufflings.
xp = np.array([0, np.pi/4, np.pi/2, 3*np.pi/4])
yp = np.array([0, 1, 2, 3])
period = np.pi
# Put x on the 1st dim and y on the 2nd dim; f is linear in y
fp = np.array([0, 1, 0, -1])[:, np.newaxis] + yp[np.newaxis, :]
# >>> fp
# array([[ 0, 1, 2, 3],
# [ 1, 2, 3, 4],
# [ 0, 1, 2, 3],
# [-1, 0, 1, 2]])
We now know that all you need to do is to add xp[[-1]]
in front of the array and xp[[0]]
at the end, adjusting for the period. Note how I've indexed using the singleton lists [-1]
and [0]
. This is a trick to ensure that dimensions are preserved.
xp = np.concatenate((xp[[-1]]-period, xp, xp[[0]]+period))
fp = np.concatenate((fp[[-1], :], fp, fp[[0], :]))
Finally, you are free to use scipy.interpolate.interpn
to achieve your result. Let's get the value at x = pi/8
for all y
:
from scipy.interpolate import interpn
interp_points = np.hstack(( (np.pi/8 * np.ones(4))[:, np.newaxis], yp[:, np.newaxis] ))
result = interpn((xp, yp), fp, interp_points)
# >>> result
# array([ 0.5, 1.5, 2.5, 3.5])
interp_points
has to be specified as an Nx2 matrix of points, where the first dimension is for each point you want interpolation at the second dimension gives the x- and y-coordinate of that point. See this answer for a detailed explanation.
If you want to get the value outside of the range [0, period]
, you'll need to modulo it yourself:
x = 21 * np.pi / 8
x_equiv = x % period # Now within [0, period]
interp_points = np.hstack(( (x_equiv * np.ones(4))[:, np.newaxis], yp[:, np.newaxis] ))
result = interpn((xp, yp), fp, interp_points)
# >>> result
# array([-0.5, 0.5, 1.5, 2.5])
Again, if you want to generate interp_points
for a bunch of x- and y- values, look at this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With