diff options
author | danieljfarrell <danieljfarrel@me.com> | 2013-08-12 14:33:11 +0900 |
---|---|---|
committer | Charles Harris <charlesr.harris@gmail.com> | 2013-09-07 11:55:24 -0600 |
commit | 332d628744a0670234585053dbe32a3e82e0c4db (patch) | |
tree | 65ccff2c4c8bed74ad0413a6d536a1b20ed120fa /numpy/lib | |
parent | 1ec94198399e91baf562799548c443040532cd0b (diff) | |
download | numpy-332d628744a0670234585053dbe32a3e82e0c4db.tar.gz |
ENH: Improve accuracy of numpy.gradient at edges
* numpy.gradient has been enhanced to use a second order accurate
one-sided finite difference stencil at boundary elements of the
array. Second order accurate central difference are still used for
the interior elements. The result is a fully second order accurate
approximation of the gradient over the full domain.
* The one-sided stencil uses 3 elements each with a different weight. A
forward difference is used for the first element,
dy/dx ~ -(3.0*y[0] - 4.0*y[1] + y[2]) / (2.0*dx)
and backwards difference is used for the last element,
dy/dx ~ (3.0*y[-1] - 4.0*y[-2] + y[-3]) / (2.0*dx)
* Because the datetime64 datatype cannot be multiplied a view is taken
of datetime64 arrays and cast to int64. The gradient algorithm is
then applied to the view rather than the input array.
* Previously no dimension checks were performed on the input array. Now
if the array size along the differentiation axis is less than 2, a
ValueError is raised which explains that more elements are needed. If
the size is exactly two the function falls back to using a 2 point
stencil (the old behaviour). If the size is 3 and above then the
higher accuracy methods are used.
* A new test has been added which validates the higher accuracy. Old
tests have been updated to pass. Note, this should be expected
because the boundary elements now return different (more accurate)
values.
Diffstat (limited to 'numpy/lib')
-rw-r--r-- | numpy/lib/function_base.py | 95 | ||||
-rw-r--r-- | numpy/lib/tests/test_function_base.py | 16 |
2 files changed, 86 insertions, 25 deletions
diff --git a/numpy/lib/function_base.py b/numpy/lib/function_base.py index 91eace2ff..ada54135e 100644 --- a/numpy/lib/function_base.py +++ b/numpy/lib/function_base.py @@ -835,9 +835,10 @@ def gradient(f, *varargs): """ Return the gradient of an N-dimensional array. - The gradient is computed using central differences in the interior - and first differences at the boundaries. The returned gradient hence has - the same shape as the input array. + The gradient is computed using second order accurate central differences + in the interior and second order accurate one-sides (forward or backwards) + differences at the boundaries. The returned gradient hence has the same + shape as the input array. Parameters ---------- @@ -847,7 +848,6 @@ def gradient(f, *varargs): 0, 1, or N scalars specifying the sample distances in each direction, that is: `dx`, `dy`, `dz`, ... The default distance is 1. - Returns ------- gradient : ndarray @@ -868,6 +868,11 @@ def gradient(f, *varargs): array([[ 1. , 2.5, 4. ], [ 1. , 1. , 1. ]])] + >>> x = np.array([0,1,2,3,4]) + >>> dx = gradient(x) + >>> y = x**2 + >>> gradient(y,dx) + array([0., 2., 4., 6., 8.]) """ f = np.asanyarray(f) N = len(f.shape) # number of dimensions @@ -882,7 +887,8 @@ def gradient(f, *varargs): raise SyntaxError( "invalid number of arguments") - # use central differences on interior and first differences on endpoints + # use central differences on interior and one-sided differences on the + # endpoints. This preserves second order-accuracy over the full domain. outvals = [] @@ -890,6 +896,7 @@ def gradient(f, *varargs): slice1 = [slice(None)]*N slice2 = [slice(None)]*N slice3 = [slice(None)]*N + slice4 = [slice(None)]*N otype = f.dtype.char if otype not in ['f', 'd', 'F', 'D', 'm', 'M']: @@ -903,24 +910,66 @@ def gradient(f, *varargs): # Needs to keep the specific units, can't be a general unit otype = f.dtype + # Convert datetime64 data into ints. Make dummy variable `y` + # that is a view of ints if the data is datetime64, otherwise + # just set y equal to the the array `f`. + if f.dtype.char in ["M", "m"]: + y = f.view('int64') + else: + y = f + for axis in range(N): - # select out appropriate parts for this dimension - out = np.empty_like(f, dtype=otype) - slice1[axis] = slice(1, -1) - slice2[axis] = slice(2, None) - slice3[axis] = slice(None, -2) - # 1D equivalent -- out[1:-1] = (f[2:] - f[:-2])/2.0 - out[slice1] = (f[slice2] - f[slice3])/2.0 - slice1[axis] = 0 - slice2[axis] = 1 - slice3[axis] = 0 - # 1D equivalent -- out[0] = (f[1] - f[0]) - out[slice1] = (f[slice2] - f[slice3]) - slice1[axis] = -1 - slice2[axis] = -1 - slice3[axis] = -2 - # 1D equivalent -- out[-1] = (f[-1] - f[-2]) - out[slice1] = (f[slice2] - f[slice3]) + + if y.shape[axis] < 2: + raise ValueError("Shape of array too small to calculate a numerical gradient, at least two elements are required.") + + # Numerical differentiation: 1st order edges, 2nd order interior + if y.shape[axis] == 2: + # Use first order differences for time data + out = np.empty_like(y, dtype=otype) + + slice1[axis] = slice(1, -1) + slice2[axis] = slice(2, None) + slice3[axis] = slice(None, -2) + # 1D equivalent -- out[1:-1] = (y[2:] - y[:-2])/2.0 + out[slice1] = (y[slice2] - y[slice3])/2.0 + + slice1[axis] = 0 + slice2[axis] = 1 + slice3[axis] = 0 + # 1D equivalent -- out[0] = (y[1] - y[0]) + out[slice1] = (y[slice2] - y[slice3]) + + slice1[axis] = -1 + slice2[axis] = -1 + slice3[axis] = -2 + # 1D equivalent -- out[-1] = (y[-1] - y[-2]) + out[slice1] = (y[slice2] - y[slice3]) + + # Numerical differentiation: 2st order edges, 2nd order interior + else: + # Use second order differences where possible + out = np.empty_like(y, dtype=otype) + + slice1[axis] = slice(1, -1) + slice2[axis] = slice(2, None) + slice3[axis] = slice(None, -2) + # 1D equivalent -- out[1:-1] = (y[2:] - y[:-2])/2.0 + out[slice1] = (y[slice2] - y[slice3])/2.0 + + slice1[axis] = 0 + slice2[axis] = 0 + slice3[axis] = 1 + slice4[axis] = 2 + # 1D equivalent -- out[0] = -(3*y[0] - 4*y[1] + y[2]) / 2.0 + out[slice1] = -(3.0*y[slice2] - 4.0*y[slice3] + y[slice4])/2.0 + + slice1[axis] = -1 + slice2[axis] = -1 + slice3[axis] = -2 + slice4[axis] = -3 + # 1D equivalent -- out[-1] = (3*y[-1] - 4*y[-2] + y[-3]) + out[slice1] = (3.0*y[slice2] - 4.0*y[slice3] + y[slice4])/2.0 # divide by step size outvals.append(out / dx[axis]) @@ -929,13 +978,13 @@ def gradient(f, *varargs): slice1[axis] = slice(None) slice2[axis] = slice(None) slice3[axis] = slice(None) + slice4[axis] = slice(None) if N == 1: return outvals[0] else: return outvals - def diff(a, n=1, axis=-1): """ Calculate the n-th order discrete difference along given axis. diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py index 61113ae7a..f52eb5fbe 100644 --- a/numpy/lib/tests/test_function_base.py +++ b/numpy/lib/tests/test_function_base.py @@ -466,7 +466,7 @@ class TestGradient(TestCase): '1910-10-12', '1910-12-12', '1912-12-12'], dtype='datetime64[D]') dx = np.array( - [-5, -3, 0, 31, 61, 396, 731], + [-7, -3, 0, 31, 61, 396, 1066], dtype='timedelta64[D]') assert_array_equal(gradient(x), dx) assert_(dx.dtype == np.dtype('timedelta64[D]')) @@ -477,11 +477,23 @@ class TestGradient(TestCase): [-5, -3, 10, 12, 61, 321, 300], dtype='timedelta64[D]') dx = np.array( - [2, 7, 7, 25, 154, 119, -21], + [-3, 7, 7, 25, 154, 119, -161], dtype='timedelta64[D]') assert_array_equal(gradient(x), dx) assert_(dx.dtype == np.dtype('timedelta64[D]')) + def test_second_order_accurate(self): + # Testing that the relative numerical error is less that 3% for + # this example problem. This corresponds to second order + # accurate finite differences for all interior and boundary + # points. + x = np.linspace(0, 1, 10) + dx = x[1] - x[0] + y = 2 * x ** 3 + 4 * x ** 2 + 2 * x + analytical = 6 * x ** 2 + 8 * x + 2 + num_error = np.abs((np.gradient(y, dx) / analytical) - 1) + assert_(np.all(num_error < 0.03) == True) + class TestAngle(TestCase): def test_basic(self): |