Skip to content

Commit c799cb5

Browse files
authored
Add GPU support for curvature (#150)
* add GPU support for curvature * correct position check condition * border edge effect, set to nan * curvature: update test, cpu against gpu version * refactor * remove use_cuda and use_gpu params * support dask numpy * typo fixes
1 parent d26cbda commit c799cb5

File tree

3 files changed

+211
-99
lines changed

3 files changed

+211
-99
lines changed

xrspatial/curvature.py

100644100755
Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,138 @@
1+
# std lib
2+
from functools import partial
3+
from typing import Union
4+
5+
# 3rd-party
6+
try:
7+
import cupy
8+
except ImportError:
9+
class cupy(object):
10+
ndarray = False
11+
12+
import dask.array as da
13+
14+
from numba import cuda
15+
116
import numpy as np
217
import xarray as xr
3-
from numba import stencil, vectorize
418

19+
# local modules
20+
from xrspatial.utils import cuda_args
21+
from xrspatial.utils import get_dataarray_resolution
22+
from xrspatial.utils import has_cuda
23+
from xrspatial.utils import ngjit
24+
from xrspatial.utils import is_cupy_backed
25+
26+
27+
@ngjit
28+
def _cpu(data, cellsize):
29+
out = np.empty(data.shape, np.float64)
30+
out[:, :] = np.nan
31+
rows, cols = data.shape
32+
for y in range(1, rows - 1):
33+
for x in range(1, cols - 1):
34+
d = (data[y + 1, x] + data[y - 1, x]) / 2 - data[y, x]
35+
e = (data[y, x + 1] + data[y, x - 1]) / 2 - data[y, x]
36+
out[y, x] = -2 * (d + e) * 100 / (cellsize * cellsize)
37+
return out
38+
39+
40+
def _run_numpy(data: np.ndarray,
41+
cellsize: Union[int, float]) -> np.ndarray:
42+
# TODO: handle border edge effect
43+
out = _cpu(data, cellsize)
44+
return out
45+
46+
47+
def _run_dask_numpy(data: da.Array,
48+
cellsize: Union[int, float]) -> da.Array:
49+
_func = partial(_cpu,
50+
cellsize=cellsize)
51+
52+
out = data.map_overlap(_func,
53+
depth=(1, 1),
54+
boundary=np.nan,
55+
meta=np.array(()))
56+
return out
57+
58+
59+
@cuda.jit(device=True)
60+
def _gpu(arr, cellsize):
61+
d = (arr[1, 0] + arr[1, 2]) / 2 - arr[1, 1]
62+
e = (arr[0, 1] + arr[2, 1]) / 2 - arr[1, 1]
63+
curv = -2 * (d + e) * 100 / (cellsize[0] * cellsize[0])
64+
return curv
565

6-
@stencil
7-
def kernel_D(arr):
8-
return (arr[0, -1] + arr[0, 1]) / 2 - arr[0, 0]
966

67+
@cuda.jit
68+
def _run_gpu(arr, cellsize, out):
69+
i, j = cuda.grid(2)
70+
di = 1
71+
dj = 1
72+
if (i - di >= 0 and i + di <= out.shape[0] - 1 and
73+
j - dj >= 0 and j + dj <= out.shape[1] - 1):
74+
out[i, j] = _gpu(arr[i - di:i + di + 1, j - dj:j + dj + 1], cellsize)
1075

11-
@stencil
12-
def kernel_E(arr):
13-
return (arr[-1, 0] + arr[1, 0]) / 2 - arr[0, 0]
1476

77+
def _run_cupy(data: cupy.ndarray,
78+
cellsize: Union[int, float]) -> cupy.ndarray:
1579

16-
@vectorize(["float64(float64, float64)"], nopython=True, target="parallel")
17-
def _curvature(matrix_D, matrix_E):
18-
curv = -2 * (matrix_D + matrix_E) * 100
19-
return curv
80+
cellsize_arr = cupy.array([float(cellsize)], dtype='f4')
81+
82+
# TODO: add padding
83+
griddim, blockdim = cuda_args(data.shape)
84+
out = cupy.empty(data.shape, dtype='f4')
85+
out[:] = cupy.nan
2086

87+
_run_gpu[griddim, blockdim](data, cellsize_arr, out)
2188

22-
def curvature(raster):
23-
"""Compute the curvature (second derivatives) of a raster surface.
89+
return out
90+
91+
92+
def _run_dask_cupy(data: da.Array,
93+
cellsize: Union[int, float]) -> da.Array:
94+
msg = 'Upstream bug in dask prevents cupy backed arrays'
95+
raise NotImplementedError(msg)
96+
97+
98+
def curvature(agg, name='curvature'):
99+
"""Compute the curvature (second derivatives) of a agg surface.
24100
25101
Parameters
26102
----------
27-
raster: xarray.DataArray
28-
2D input raster image with shape=(height, width)
103+
agg: xarray.xr.DataArray
104+
2D input agg image with shape=(height, width)
29105
30106
Returns
31107
-------
32-
curvature: xarray.DataArray
108+
curvature: xarray.xr.DataArray
33109
Curvature image with shape=(height, width)
34110
"""
35111

36-
if not isinstance(raster, xr.DataArray):
37-
raise TypeError("`raster` must be instance of DataArray")
38-
39-
if raster.ndim != 2:
40-
raise ValueError("`raster` must be 2D")
41-
42-
if not (issubclass(raster.values.dtype.type, np.integer) or
43-
issubclass(raster.values.dtype.type, np.floating)):
44-
raise ValueError(
45-
"`raster` must be an array of integers or float")
46-
47-
raster_values = raster.values
112+
cellsize_x, cellsize_y = get_dataarray_resolution(agg)
113+
cellsize = (cellsize_x + cellsize_y) / 2
48114

49-
cell_size_x = 1
50-
cell_size_y = 1
115+
# numpy case
116+
if isinstance(agg.data, np.ndarray):
117+
out = _run_numpy(agg.data, cellsize)
51118

52-
# calculate cell size from input `raster`
53-
for dim in raster.dims:
54-
if (dim.lower().count('x')) > 0 or (dim.lower().count('lon')) > 0:
55-
# dimension of x-coordinates
56-
if len(raster[dim]) > 1:
57-
cell_size_x = raster[dim].values[1] - raster[dim].values[0]
58-
elif (dim.lower().count('y')) > 0 or (dim.lower().count('lat')) > 0:
59-
# dimension of y-coordinates
60-
if len(raster[dim]) > 1:
61-
cell_size_y = raster[dim].values[1] - raster[dim].values[0]
119+
# cupy case
120+
elif has_cuda() and isinstance(agg.data, cupy.ndarray):
121+
out = _run_cupy(agg.data, cellsize)
62122

63-
cell_size = (cell_size_x + cell_size_y) / 2
123+
# dask + numpy case
124+
elif isinstance(agg.data, da.Array):
125+
out = _run_dask_numpy(agg.data, cellsize)
64126

65-
matrix_D = kernel_D(raster_values)
66-
matrix_E = kernel_E(raster_values)
127+
# dask + cupy case
128+
elif has_cuda() and isinstance(agg.data, da.Array) and is_cupy_backed(agg):
129+
out = _run_dask_cupy(agg.data, cellsize)
67130

68-
curvature_values = _curvature(matrix_D, matrix_E) / (cell_size * cell_size)
69-
result = xr.DataArray(curvature_values,
70-
coords=raster.coords,
71-
dims=raster.dims,
72-
attrs=raster.attrs)
131+
else:
132+
raise TypeError('Unsupported Array Type: {}'.format(type(agg.data)))
73133

74-
return result
134+
return xr.DataArray(out,
135+
name=name,
136+
coords=agg.coords,
137+
dims=agg.dims,
138+
attrs=agg.attrs)

xrspatial/slope.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,39 @@ class cupy(object):
2525
from xrspatial.utils import ngjit
2626
from xrspatial.utils import is_cupy_backed
2727

28+
2829
@ngjit
2930
def _cpu(data, cellsize_x, cellsize_y):
3031
out = np.zeros_like(data)
3132
out[:] = np.nan
3233
rows, cols = data.shape
33-
for y in range(1, rows-1):
34-
for x in range(1, cols-1):
35-
a = data[y+1, x-1]
36-
b = data[y+1, x]
37-
c = data[y+1, x+1]
38-
d = data[y, x-1]
39-
f = data[y, x+1]
40-
g = data[y-1, x-1]
41-
h = data[y-1, x]
42-
i = data[y-1, x+1]
34+
for y in range(1, rows - 1):
35+
for x in range(1, cols - 1):
36+
a = data[y + 1, x - 1]
37+
b = data[y + 1, x]
38+
c = data[y + 1, x + 1]
39+
d = data[y, x - 1]
40+
f = data[y, x + 1]
41+
g = data[y - 1, x - 1]
42+
h = data[y - 1, x]
43+
i = data[y - 1, x + 1]
4344
dz_dx = ((c + 2 * f + i) - (a + 2 * d + g)) / (8 * cellsize_x)
4445
dz_dy = ((g + 2 * h + i) - (a + 2 * b + c)) / (8 * cellsize_y)
4546
p = (dz_dx * dz_dx + dz_dy * dz_dy) ** .5
4647
out[y, x] = np.arctan(p) * 57.29578
4748
return out
4849

4950

50-
def _run_numpy(data:np.ndarray,
51-
cellsize_x:Union[int, float],
52-
cellsize_y:Union[int, float]) -> np.ndarray:
51+
def _run_numpy(data: np.ndarray,
52+
cellsize_x: Union[int, float],
53+
cellsize_y: Union[int, float]) -> np.ndarray:
5354
out = _cpu(data, cellsize_x, cellsize_y)
5455
return out
5556

5657

57-
def _run_dask_numpy(data:da.Array,
58-
cellsize_x:Union[int, float],
59-
cellsize_y:Union[int, float]) -> da.Array:
58+
def _run_dask_numpy(data: da.Array,
59+
cellsize_x: Union[int, float],
60+
cellsize_y: Union[int, float]) -> da.Array:
6061
_func = partial(_cpu,
6162
cellsize_x=cellsize_x,
6263
cellsize_y=cellsize_y)
@@ -90,17 +91,16 @@ def _run_gpu(arr, cellsize_x_arr, cellsize_y_arr, out):
9091
i, j = cuda.grid(2)
9192
di = 1
9293
dj = 1
93-
if (i-di >= 0 and i+di < out.shape[0] and
94-
j-dj >= 0 and j+dj < out.shape[1]):
95-
out[i, j] = _gpu(arr[i-di:i+di+1, j-dj:j+dj+1],
94+
if (i - di >= 0 and i + di < out.shape[0] and
95+
j - dj >= 0 and j + dj < out.shape[1]):
96+
out[i, j] = _gpu(arr[i - di:i + di + 1, j - dj:j + dj + 1],
9697
cellsize_x_arr,
9798
cellsize_y_arr)
9899

99100

100101
def _run_cupy(data: cupy.ndarray,
101102
cellsize_x: Union[int, float],
102103
cellsize_y: Union[int, float]) -> cupy.ndarray:
103-
104104
cellsize_x_arr = cupy.array([float(cellsize_x)], dtype='f4')
105105
cellsize_y_arr = cupy.array([float(cellsize_y)], dtype='f4')
106106

@@ -115,10 +115,9 @@ def _run_cupy(data: cupy.ndarray,
115115
return out
116116

117117

118-
def _run_dask_cupy(data:da.Array,
119-
cellsize_x:Union[int, float],
120-
cellsize_y:Union[int, float]) -> da.Array:
121-
118+
def _run_dask_cupy(data: da.Array,
119+
cellsize_x: Union[int, float],
120+
cellsize_y: Union[int, float]) -> da.Array:
122121
msg = 'Upstream bug in dask prevents cupy backed arrays'
123122
raise NotImplementedError(msg)
124123

@@ -134,7 +133,7 @@ def _run_dask_cupy(data:da.Array,
134133
return out
135134

136135

137-
def slope(agg:xr.DataArray, name: str='slope') -> xr.DataArray:
136+
def slope(agg: xr.DataArray, name: str = 'slope') -> xr.DataArray:
138137
"""Returns slope of input aggregate in degrees.
139138
140139
Parameters
@@ -168,7 +167,7 @@ def slope(agg:xr.DataArray, name: str='slope') -> xr.DataArray:
168167
# dask + cupy case
169168
elif has_cuda() and isinstance(agg.data, da.Array) and is_cupy_backed(agg):
170169
out = _run_dask_cupy(agg.data, cellsize_x, cellsize_y)
171-
170+
172171
# dask + numpy case
173172
elif isinstance(agg.data, da.Array):
174173
out = _run_dask_numpy(agg.data, cellsize_x, cellsize_y)
@@ -180,4 +179,4 @@ def slope(agg:xr.DataArray, name: str='slope') -> xr.DataArray:
180179
name=name,
181180
coords=agg.coords,
182181
dims=agg.dims,
183-
attrs=agg.attrs)
182+
attrs=agg.attrs)

0 commit comments

Comments
 (0)