1616
1717from xrspatial .utils import ArrayTypeFunctionMapping
1818from xrspatial .utils import Z_UNITS
19+ from xrspatial .utils import _boundary_to_dask
1920from xrspatial .utils import _extract_latlon_coords
21+ from xrspatial .utils import _pad_array
22+ from xrspatial .utils import _validate_boundary
2023from xrspatial .utils import cuda_args
2124from xrspatial .utils import ngjit
2225from xrspatial .dataset_support import supports_dataset
@@ -54,7 +57,7 @@ class cupy(object):
5457# =====================================================================
5558
5659@ngjit
57- def _run_numpy (data : np .ndarray ):
60+ def _cpu (data : np .ndarray ):
5861 data = data .astype (np .float32 )
5962 out = np .zeros_like (data , dtype = np .float32 )
6063 out [:] = np .nan
@@ -90,6 +93,14 @@ def _run_numpy(data: np.ndarray):
9093 return out
9194
9295
96+ def _run_numpy (data : np .ndarray , boundary : str = 'nan' ) -> np .ndarray :
97+ if boundary == 'nan' :
98+ return _cpu (data )
99+ padded = _pad_array (data , 1 , boundary )
100+ result = _cpu (padded )
101+ return result [1 :- 1 , 1 :- 1 ]
102+
103+
93104@cuda .jit (device = True )
94105def _gpu (arr ):
95106
@@ -136,7 +147,12 @@ def _run_gpu(arr, out):
136147 out [i , j ] = _gpu (arr [i - di :i + di + 1 , j - dj :j + dj + 1 ])
137148
138149
139- def _run_cupy (data : cupy .ndarray ) -> cupy .ndarray :
150+ def _run_cupy (data : cupy .ndarray , boundary : str = 'nan' ) -> cupy .ndarray :
151+ if boundary != 'nan' :
152+ padded = _pad_array (data , 1 , boundary )
153+ result = _run_cupy (padded )
154+ return result [1 :- 1 , 1 :- 1 ]
155+
140156 data = data .astype (cupy .float32 )
141157 griddim , blockdim = cuda_args (data .shape )
142158 out = cupy .empty (data .shape , dtype = 'f4' )
@@ -145,22 +161,22 @@ def _run_cupy(data: cupy.ndarray) -> cupy.ndarray:
145161 return out
146162
147163
148- def _run_dask_numpy (data : da .Array ) -> da .Array :
164+ def _run_dask_numpy (data : da .Array , boundary : str = 'nan' ) -> da .Array :
149165 data = data .astype (np .float32 )
150- _func = partial (_run_numpy )
166+ _func = partial (_cpu )
151167 out = data .map_overlap (_func ,
152168 depth = (1 , 1 ),
153- boundary = np . nan ,
169+ boundary = _boundary_to_dask ( boundary ) ,
154170 meta = np .array (()))
155171 return out
156172
157173
158- def _run_dask_cupy (data : da .Array ) -> da .Array :
174+ def _run_dask_cupy (data : da .Array , boundary : str = 'nan' ) -> da .Array :
159175 data = data .astype (cupy .float32 )
160176 _func = partial (_run_cupy )
161177 out = data .map_overlap (_func ,
162178 depth = (1 , 1 ),
163- boundary = cupy . nan ,
179+ boundary = _boundary_to_dask ( boundary , is_cupy = True ) ,
164180 meta = cupy .array (()))
165181 return out
166182
@@ -169,7 +185,14 @@ def _run_dask_cupy(data: da.Array) -> da.Array:
169185# Geodesic backend functions
170186# =====================================================================
171187
172- def _run_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
188+ def _run_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor , boundary = 'nan' ):
189+ if boundary != 'nan' :
190+ data_p = _pad_array (data .astype (np .float64 ), 1 , boundary )
191+ lat_p = _pad_array (lat_2d , 1 , boundary )
192+ lon_p = _pad_array (lon_2d , 1 , boundary )
193+ stacked = np .stack ([data_p , lat_p , lon_p ], axis = 0 )
194+ result = _cpu_geodesic_aspect (stacked , a2 , b2 , z_factor )
195+ return result [1 :- 1 , 1 :- 1 ]
173196 stacked = np .stack ([
174197 data .astype (np .float64 ),
175198 lat_2d ,
@@ -178,7 +201,22 @@ def _run_numpy_geodesic(data, lat_2d, lon_2d, a2, b2, z_factor):
178201 return _cpu_geodesic_aspect (stacked , a2 , b2 , z_factor )
179202
180203
181- def _run_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
204+ def _run_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor , boundary = 'nan' ):
205+ if boundary != 'nan' :
206+ data_p = _pad_array (data .astype (cupy .float64 ), 1 , boundary )
207+ lat_p = _pad_array (cupy .asarray (lat_2d , dtype = cupy .float64 ), 1 , boundary )
208+ lon_p = _pad_array (cupy .asarray (lon_2d , dtype = cupy .float64 ), 1 , boundary )
209+ stacked = cupy .stack ([data_p , lat_p , lon_p ], axis = 0 )
210+ H , W = data_p .shape
211+ out = cupy .full ((H , W ), cupy .nan , dtype = cupy .float32 )
212+ a2_arr = cupy .array ([a2 ], dtype = cupy .float64 )
213+ b2_arr = cupy .array ([b2 ], dtype = cupy .float64 )
214+ zf_arr = cupy .array ([z_factor ], dtype = cupy .float64 )
215+ inv_2r_arr = cupy .array ([INV_2R ], dtype = cupy .float64 )
216+ griddim , blockdim = _geodesic_cuda_dims ((H , W ))
217+ _run_gpu_geodesic_aspect [griddim , blockdim ](stacked , a2_arr , b2_arr , zf_arr , inv_2r_arr , out )
218+ return out [1 :- 1 , 1 :- 1 ]
219+
182220 lat_2d_gpu = cupy .asarray (lat_2d , dtype = cupy .float64 )
183221 lon_2d_gpu = cupy .asarray (lon_2d , dtype = cupy .float64 )
184222 stacked = cupy .stack ([
@@ -227,7 +265,7 @@ def _dask_geodesic_aspect_chunk_cupy(stacked_chunk, a2, b2, z_factor):
227265 return out
228266
229267
230- def _run_dask_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
268+ def _run_dask_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor , boundary = 'nan' ):
231269 lat_dask = da .from_array (lat_2d , chunks = data .chunksize )
232270 lon_dask = da .from_array (lon_2d , chunks = data .chunksize )
233271 stacked = da .stack ([
@@ -237,16 +275,17 @@ def _run_dask_numpy_geodesic(data, lat_2d, lon_2d, a2, b2, z_factor):
237275 ], axis = 0 ).rechunk ({0 : 3 })
238276
239277 _func = partial (_dask_geodesic_aspect_chunk , a2 = a2 , b2 = b2 , z_factor = z_factor )
278+ dask_bnd = _boundary_to_dask (boundary )
240279 out = stacked .map_overlap (
241280 _func ,
242281 depth = (0 , 1 , 1 ),
243- boundary = np .nan ,
282+ boundary = { 0 : np .nan , 1 : dask_bnd , 2 : dask_bnd } ,
244283 meta = np .array ((), dtype = np .float32 ),
245284 )
246285 return out [0 ]
247286
248287
249- def _run_dask_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
288+ def _run_dask_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor , boundary = 'nan' ):
250289 lat_dask = da .from_array (cupy .asarray (lat_2d , dtype = cupy .float64 ),
251290 chunks = data .chunksize )
252291 lon_dask = da .from_array (cupy .asarray (lon_2d , dtype = cupy .float64 ),
@@ -258,10 +297,11 @@ def _run_dask_cupy_geodesic(data, lat_2d, lon_2d, a2, b2, z_factor):
258297 ], axis = 0 ).rechunk ({0 : 3 })
259298
260299 _func = partial (_dask_geodesic_aspect_chunk_cupy , a2 = a2 , b2 = b2 , z_factor = z_factor )
300+ dask_bnd = _boundary_to_dask (boundary , is_cupy = True )
261301 out = stacked .map_overlap (
262302 _func ,
263303 depth = (0 , 1 , 1 ),
264- boundary = cupy .nan ,
304+ boundary = { 0 : cupy .nan , 1 : dask_bnd , 2 : dask_bnd } ,
265305 meta = cupy .array ((), dtype = cupy .float32 ),
266306 )
267307 return out [0 ]
@@ -275,7 +315,8 @@ def _run_dask_cupy_geodesic(data, lat_2d, lon_2d, a2, b2, z_factor):
275315def aspect (agg : xr .DataArray ,
276316 name : Optional [str ] = 'aspect' ,
277317 method : str = 'planar' ,
278- z_unit : str = 'meter' ) -> xr .DataArray :
318+ z_unit : str = 'meter' ,
319+ boundary : str = 'nan' ) -> xr .DataArray :
279320 """
280321 Calculates the aspect value of an elevation aggregate.
281322
@@ -314,6 +355,12 @@ def aspect(agg: xr.DataArray,
314355 Unit of the elevation values. Only used when ``method='geodesic'``.
315356 Accepted values: ``'meter'``, ``'foot'``, ``'kilometer'``, ``'mile'``
316357 (and common aliases).
358+ boundary : str, default='nan'
359+ How to handle edges where the kernel extends beyond the raster.
360+ ``'nan'`` — fill missing neighbours with NaN (default).
361+ ``'nearest'`` — repeat edge values.
362+ ``'reflect'`` — mirror at boundary.
363+ ``'wrap'`` — periodic / toroidal.
317364
318365 Returns
319366 -------
@@ -353,6 +400,7 @@ def aspect(agg: xr.DataArray,
353400 raise ValueError (
354401 f"method must be 'planar' or 'geodesic', got { method !r} "
355402 )
403+ _validate_boundary (boundary )
356404
357405 if method == 'planar' :
358406 mapper = ArrayTypeFunctionMapping (
@@ -361,7 +409,7 @@ def aspect(agg: xr.DataArray,
361409 cupy_func = _run_cupy ,
362410 dask_cupy_func = _run_dask_cupy ,
363411 )
364- out = mapper (agg )(agg .data )
412+ out = mapper (agg )(agg .data , boundary = boundary )
365413
366414 else : # geodesic
367415 if z_unit not in Z_UNITS :
@@ -379,7 +427,7 @@ def aspect(agg: xr.DataArray,
379427 dask_func = _run_dask_numpy_geodesic ,
380428 dask_cupy_func = _run_dask_cupy_geodesic ,
381429 )
382- out = mapper (agg )(agg .data , lat_2d , lon_2d , WGS84_A2 , WGS84_B2 , z_factor )
430+ out = mapper (agg )(agg .data , lat_2d , lon_2d , WGS84_A2 , WGS84_B2 , z_factor , boundary )
383431
384432 return xr .DataArray (out ,
385433 name = name ,
0 commit comments