#!/usr/bin/env python # coding: utf-8 # In[27]: import numpy as np import warnings def along_axis(y, x, newx, axis, inverse=False, method='linear'): """ Interpolate vertical profiles, e.g. of atmospheric variables using vectorized numpy operations - This function assumes that the x-xoordinate increases monotonically args ---- y : nd-array The variable to be interpolated x : nd-array or 1d-array The coordinate associated with y, along which to interpolate. If nd-array, this variable should have the same shape as y If 1d, len(x) should be equal to the length of y along `axis` newx : nd-array or 1d-array or float The new coordinates to which y should be interpolated. If nd-array, this variable should have the same shape as y If 1d, len(x) should be equal to the length of y along `axis` method : string 'linear', straightforward linear interpolation 'cubic', Algorithm from: http://www.paulinternet.nl/?page=bicubic f(x) = ax**3+bx**2+cx+d with a = 2f(0) - 2f(1) + f'(0) + f'(1) b = -3f(0) + 3f(1) - 2f'(0) - f'(1) c = f'(0) d = f(0) 'hermite', Algorithm from https://en.wikipedia.org/wiki/Cubic_Hermite_spline f(x) = h00(x)*f(0)+h10(x)*f'(0)+h01(x)*f(1)+h11(x)*f'(1) with h00(x) = 2x**3-3x**2+1 h10(x) = x**3-2x**2+x h01(x) = -2x**3+3x**2 h11(x) = x**3-x**2 'cspline', Algorithm from https://en.wikipedia.org/wiki/Spline_interpolation f(x) = (1-x)*f(0) + x*f(1) + x*(1-x)*(a*(10x)+bx) with a = f'(0)*(x_up-x_low) - (f(1)-f(0)) a = -f'(1)*(x_up-x_low) + (f(1)-f(0)) 'natural', Algorithm from https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation#Methods ddf0*a**3/(6*c) + ddf1*b**3/(6*c) + (y0-ddf0*c**2/6)*a/c + y1-ddf1*c**2/6)*b/c a = x1 - x b = x - x0 c = x1 - x0 where ddf is solved for using TDMA. Notes ----- * Updated to work with irregularly spaced x-coordinate. * Updated to work with irregularly spaced newx-coordinate * Updated to easily inverse the direction of the x-coordinate * Updated to fill with nans outside extrapolation range * Updated to include a linear interpolation method as well (it was initially written for a cubic function) * Updated for https://github.com/numpy/numpy/pull/9686 (makes it ugly!) * Updated to work with either 1d or nd input for x and newx. * Added two new algorithms for computing a cubic spline: 'hermite' and 'cspline' Theoretically, they should yield the same results, but it seems to work better than the old method 'cubic' * Added option 'gradient', which let you choose between numpy and a cardinal gradient computation. * Added option 'c': the tension parameter of the cardinal gradient computation. * Added method 'natural', but this seems to work less well. Should it be implemented differently? perhaps using tridiagonal matrix, like here: https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation#Methods * Fixed the 'natural' spline interpolation. This method is (nearly) equivalent to the scipy routine. It finds the optimal curvature at each node under the condition that the curvature is 0 at the start and end points. It uses Thomas' algorithm to solve a tridiagonal matrix for the curvature at the nodes. This involves two extra loops over the target axis, but it's probably still faster than applied scipy1d recursively. * Removed gradient option - fall back to numpy defaults. Cardinal gradient computation did not improve the results substantially, but did complicate the code and call Peter Kalverla March 2018; last update: October 3, 2018 """ # Parse input # ----------- _y = np.atleast_1d(y) _x = np.atleast_1d(x) _newx = np.atleast_1d(newx) # This should make the shapes compatible _x = np.broadcast_to(_x, _y.shape) newshape = list(_y.shape) newshape[axis] = len(_newx) if _newx.ndim==1 else _newx.shape[axis] _newx = np.broadcast_to(_newx, newshape) # View of x and y with axis as first dimension _y = np.moveaxis(_y, axis, 0) _x = np.moveaxis(_x, axis, 0) _newx = np.moveaxis(_newx, axis, 0) # Possibly inverse the arrays if inverse: _y = _y[::-1] _x = _x[::-1] _newx = _newx[::-1] # Sanity checks: valid interpolation range and monotonic increase? if np.any(_newx[0] < _x[0]) or np.any(_newx[-1] > _x[-1]): warnings.warn("Some values are outside the interpolation range. " "These will be filled with NaN") if np.any(np.diff(_x, axis=0) < 0): raise ValueError('x should increase monotonically') if np.any(np.diff(_newx, axis=0) < 0): raise ValueError('newx should increase monotonically') ################################################################## # Preprocessing: different methods need various 'helper' arrays if method=='linear': pass elif method=='cubic': # This algorithm needs a scaled gradient, so don't divide by grid spacing ydx = np.gradient(_y, axis=0, edge_order=2) elif method in ['hermite', 'cspline']: # The other cubic spline algorithms implement their own correction for affine transformation ydx = np.gradient(_y, axis=0, edge_order=2) / np.gradient(_x, axis=0, edge_order=2) elif method=='natural': # Initialize arrays for tridiagonal matrix algorithm # a*ddf(i-1) + b*ddf(i) + c*ddf(i+1) = d ; r is a shorthand that returns often later on a = np.zeros_like(_x) b = np.zeros_like(_x) + 2 # c = 1-a, don't need to contaminate memory for that d = np.zeros_like(_x) ddf = np.zeros_like(_x) r = np.zeros_like(_x) # Type II "natural" BC: a[0] = a[-1] = 0 d[0] = d[-1] = 0 ddf[0] = ddf[-1] = 0 r[1:] = _x[1:]-_x[:-1] a[1:-1] = r[1:-1]/(r[1:-1] + r[2:]) d[1:-1] = 6*np.diff(np.diff(_y, axis=0)/r[1:,...], axis=0)/(_x[2:]-_x[:-2]) # Available alternatives: TMDASolve, TMDAsolver, TMDAsolver2, # TMDAsolver3, TMDA, TMDA2 (see below) ddf = TDMAsolver3(a, b, 1-a, d) else: raise ValueError("interpolation method not understood (got %s)" "(choose 'linear', 'cubic', 'hermite', 'cspline', or 'natural')"%method) ################################################################## # Initialize indexer arrays # This will later be concatenated with a dynamic '0th' index ind = [i for i in np.indices(_y.shape[1:])] # Allocate the output array original_dims = _y.shape newdims = list(original_dims) newdims[0] = len(_newx) newy = np.zeros(newdims) # set initial bounds i_lower = np.zeros(_x.shape[1:], dtype=int) i_upper = np.ones(_x.shape[1:], dtype=int) x_lower = _x[0, ...] x_upper = _x[1, ...] ################################################################## # Pass trough the array along and evaluate f(x) at _newx for i, xi in enumerate(_newx): # Start at the 'bottom' of the array and work upwards # This only works if x and newx increase monotonically # Update bounds where necessary and possible needs_update = (xi > x_upper) & (i_upper+1 x_upper) & (i_upper+1 pass single float value to interpolation function. # Inverse the coordinates for testing (and multiply to magnify the effect of scaling) z = z[..., ::-1]*10 znew = znew[..., ::-1]*10 # Now use own routine ynew = along_axis(testdata, z, znew, axis=2, inverse=True, method='cubic') ynew2 = along_axis(testdata, z, znew, axis=2, inverse=True, method='linear') ynew3 = along_axis(testdata, z, znew, axis=2, inverse=True, method='hermite') ynew4 = along_axis(testdata, z, znew, axis=2, inverse=True, method='cspline') ynew5 = along_axis(testdata, z, znew, axis=2, inverse=True, method='natural') randx = np.random.randint(nx) randy = np.random.randint(ny) if case in [1,2]: # z = nd checkfunc = scipy1d(z[randx, randy], testdata[randx,randy], kind='cubic') else: checkfunc = scipy1d(z, testdata[randx,randy], kind='cubic') if case in [1,4]: # znew = nd checkdata = checkfunc(znew[randx, randy]) else: checkdata = checkfunc(znew) fig, ax = plt.subplots() if case in [1,2]: # z = nd ax.plot(testdata[randx, randy], z[randx, randy], 'x', label='original data') else: ax.plot(testdata[randx, randy], z, 'x', label='original data') if case in [1,4]: # znew = nd ax.plot(checkdata, znew[randx, randy], label='scipy') ax.plot(ynew[randx, randy], znew[randx, randy], '--', label='Peter - Spline') ax.plot(ynew2[randx, randy], znew[randx, randy], '-.', label='Peter - Linear') ax.plot(ynew3[randx, randy], znew[randx, randy], '-.', label='Peter - Hermite') ax.plot(ynew4[randx, randy], znew[randx, randy], '-.', label='Peter - Cspline') ax.plot(ynew5[randx, randy], znew[randx, randy], '-.', label='Peter - Natural') else: ax.plot(checkdata, znew, label='scipy') ax.plot(ynew[randx, randy], znew, '--', label='Peter - Spline') ax.plot(ynew2[randx, randy], znew, '-.', label='Peter - Linear') ax.plot(ynew3[randx, randy], znew, '-.', label='Peter - Hermite') ax.plot(ynew4[randx, randy], znew, '-.', label='Peter - Cspline') ax.plot(ynew5[randx, randy], znew, '-.', label='Peter - Natural') ax.legend() plt.show() # In[ ]: