1515from numba import cuda
1616
1717from xrspatial .utils import ArrayTypeFunctionMapping
18+ from xrspatial .utils import Z_UNITS
19+ from xrspatial .utils import _extract_latlon_coords
1820from xrspatial .utils import cuda_args
1921from xrspatial .utils import ngjit
2022
23+
24+ def _geodesic_cuda_dims (shape ):
25+ """Smaller thread block for register-heavy geodesic kernels."""
26+ tpb = (16 , 16 )
27+ bpg = (
28+ (shape [0 ] + tpb [0 ] - 1 ) // tpb [0 ],
29+ (shape [1 ] + tpb [1 ] - 1 ) // tpb [1 ],
30+ )
31+ return bpg , tpb
32+
33+ from xrspatial .geodesic import (
34+ INV_2R ,
35+ WGS84_A2 ,
36+ WGS84_B2 ,
37+ _cpu_geodesic_aspect ,
38+ _run_gpu_geodesic_aspect ,
39+ )
40+
2141# 3rd-party
2242try :
2343 import cupy
@@ -28,6 +48,10 @@ class cupy(object):
2848RADIAN = 180 / np .pi
2949
3050
51+ # =====================================================================
52+ # Planar backend functions (unchanged)
53+ # =====================================================================
54+
3155@ngjit
3256def _run_numpy (data : np .ndarray ):
3357 data = data .astype (np .float32 )
@@ -140,8 +164,116 @@ def _run_dask_cupy(data: da.Array) -> da.Array:
140164 return out
141165
142166
167+ # =====================================================================
168+ # Geodesic backend functions
169+ # =====================================================================
170+
171+ def _run_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
172+ stacked = np .stack ([
173+ data .astype (np .float64 ),
174+ lat_2d ,
175+ lon_2d ,
176+ ], axis = 0 )
177+ return _cpu_geodesic_aspect (stacked , a2 , b2 , z_factor )
178+
179+
180+ def _run_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
181+ lat_2d_gpu = cupy .asarray (lat_2d , dtype = cupy .float64 )
182+ lon_2d_gpu = cupy .asarray (lon_2d , dtype = cupy .float64 )
183+ stacked = cupy .stack ([
184+ data .astype (cupy .float64 ),
185+ lat_2d_gpu ,
186+ lon_2d_gpu ,
187+ ], axis = 0 )
188+
189+ H , W = data .shape
190+ out = cupy .full ((H , W ), cupy .nan , dtype = cupy .float32 )
191+
192+ a2_arr = cupy .array ([a2 ], dtype = cupy .float64 )
193+ b2_arr = cupy .array ([b2 ], dtype = cupy .float64 )
194+ zf_arr = cupy .array ([z_factor ], dtype = cupy .float64 )
195+ inv_2r_arr = cupy .array ([INV_2R ], dtype = cupy .float64 )
196+
197+ griddim , blockdim = _geodesic_cuda_dims ((H , W ))
198+ _run_gpu_geodesic_aspect [griddim , blockdim ](stacked , a2_arr , b2_arr , zf_arr , inv_2r_arr , out )
199+ return out
200+
201+
202+ def _dask_geodesic_aspect_chunk (stacked_chunk , a2 , b2 , z_factor ):
203+ """Returns (3, h, w) to preserve shape for map_overlap."""
204+ result_2d = _cpu_geodesic_aspect (stacked_chunk , a2 , b2 , z_factor )
205+ out = np .empty_like (stacked_chunk , dtype = np .float32 )
206+ out [0 ] = result_2d
207+ out [1 ] = 0.0
208+ out [2 ] = 0.0
209+ return out
210+
211+
212+ def _dask_geodesic_aspect_chunk_cupy (stacked_chunk , a2 , b2 , z_factor ):
213+ H , W = stacked_chunk .shape [1 ], stacked_chunk .shape [2 ]
214+ result_2d = cupy .full ((H , W ), cupy .nan , dtype = cupy .float32 )
215+
216+ a2_arr = cupy .array ([a2 ], dtype = cupy .float64 )
217+ b2_arr = cupy .array ([b2 ], dtype = cupy .float64 )
218+ zf_arr = cupy .array ([z_factor ], dtype = cupy .float64 )
219+ inv_2r_arr = cupy .array ([INV_2R ], dtype = cupy .float64 )
220+
221+ griddim , blockdim = _geodesic_cuda_dims ((H , W ))
222+ _run_gpu_geodesic_aspect [griddim , blockdim ](stacked_chunk , a2_arr , b2_arr , zf_arr , inv_2r_arr , result_2d )
223+
224+ out = cupy .zeros_like (stacked_chunk , dtype = cupy .float32 )
225+ out [0 ] = result_2d
226+ return out
227+
228+
229+ def _run_dask_numpy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
230+ lat_dask = da .from_array (lat_2d , chunks = data .chunksize )
231+ lon_dask = da .from_array (lon_2d , chunks = data .chunksize )
232+ stacked = da .stack ([
233+ data .astype (np .float64 ),
234+ lat_dask ,
235+ lon_dask ,
236+ ], axis = 0 ).rechunk ({0 : 3 })
237+
238+ _func = partial (_dask_geodesic_aspect_chunk , a2 = a2 , b2 = b2 , z_factor = z_factor )
239+ out = stacked .map_overlap (
240+ _func ,
241+ depth = (0 , 1 , 1 ),
242+ boundary = np .nan ,
243+ meta = np .array ((), dtype = np .float32 ),
244+ )
245+ return out [0 ]
246+
247+
248+ def _run_dask_cupy_geodesic (data , lat_2d , lon_2d , a2 , b2 , z_factor ):
249+ lat_dask = da .from_array (cupy .asarray (lat_2d , dtype = cupy .float64 ),
250+ chunks = data .chunksize )
251+ lon_dask = da .from_array (cupy .asarray (lon_2d , dtype = cupy .float64 ),
252+ chunks = data .chunksize )
253+ stacked = da .stack ([
254+ data .astype (cupy .float64 ),
255+ lat_dask ,
256+ lon_dask ,
257+ ], axis = 0 ).rechunk ({0 : 3 })
258+
259+ _func = partial (_dask_geodesic_aspect_chunk_cupy , a2 = a2 , b2 = b2 , z_factor = z_factor )
260+ out = stacked .map_overlap (
261+ _func ,
262+ depth = (0 , 1 , 1 ),
263+ boundary = cupy .nan ,
264+ meta = cupy .array ((), dtype = cupy .float32 ),
265+ )
266+ return out [0 ]
267+
268+
269+ # =====================================================================
270+ # Public API
271+ # =====================================================================
272+
143273def aspect (agg : xr .DataArray ,
144- name : Optional [str ] = 'aspect' ) -> xr .DataArray :
274+ name : Optional [str ] = 'aspect' ,
275+ method : str = 'planar' ,
276+ z_unit : str = 'meter' ) -> xr .DataArray :
145277 """
146278 Calculates the aspect value of an elevation aggregate.
147279
@@ -169,6 +301,15 @@ def aspect(agg: xr.DataArray,
169301 of elevation values.
170302 name : str, default='aspect'
171303 Name of ouput DataArray.
304+ method : str, default='planar'
305+ ``'planar'`` uses the classic Horn algorithm with uniform cell size.
306+ ``'geodesic'`` converts cells to Earth-Centered Earth-Fixed (ECEF)
307+ coordinates and fits a 3D plane, yielding accurate results for
308+ geographic (lat/lon) coordinate systems.
309+ z_unit : str, default='meter'
310+ Unit of the elevation values. Only used when ``method='geodesic'``.
311+ Accepted values: ``'meter'``, ``'foot'``, ``'kilometer'``, ``'mile'``
312+ (and common aliases).
172313
173314 Returns
174315 -------
@@ -198,81 +339,40 @@ def aspect(agg: xr.DataArray,
198339 [1, 5, 0, 5, 5]
199340 ], dtype=np.float32)
200341 >>> raster = xr.DataArray(data, dims=['y', 'x'], name='raster')
201- >>> print(raster)
202- <xarray.DataArray 'raster' (y: 6, x: 5)>
203- array([[1., 1., 1., 1., 1.],
204- [1., 1., 1., 2., 0.],
205- [1., 1., 1., 0., 0.],
206- [4., 4., 9., 2., 4.],
207- [1., 5., 0., 1., 4.],
208- [1., 5., 0., 5., 5.]])
209- Dimensions without coordinates: y, x
210342 >>> aspect_agg = aspect(raster)
211- >>> print(aspect_agg)
212- <xarray.DataArray 'aspect' (y: 6, x: 5)>
213- array([[ nan, nan , nan , nan , nan],
214- [ nan, -1. , 225. , 135. , nan],
215- [ nan, 343.61045967, 8.97262661, 33.69006753, nan],
216- [ nan, 307.87498365, 71.56505118, 54.46232221, nan],
217- [ nan, 191.30993247, 144.46232221, 255.96375653, nan],
218- [ nan, nan , nan , nan , nan]])
219- Dimensions without coordinates: y, x
220-
221- Aspect works with Dask with NumPy backed xarray DataArray
222- .. sourcecode:: python
223-
224- >>> import dask.array as da
225- >>> data_da = da.from_array(data, chunks=(3, 3))
226- >>> raster_da = xr.DataArray(data_da, dims=['y', 'x'], name='raster_da')
227- >>> print(raster_da)
228- <xarray.DataArray 'raster' (y: 6, x: 5)>
229- dask.array<array, shape=(6, 5), dtype=int64, chunksize=(3, 3), chunktype=numpy.ndarray>
230- Dimensions without coordinates: y, x
231- >>> aspect_da = aspect(raster_da)
232- >>> print(aspect_da)
233- <xarray.DataArray 'aspect' (y: 6, x: 5)>
234- dask.array<_trim, shape=(6, 5), dtype=float32, chunksize=(3, 3), chunktype=numpy.ndarray>
235- Dimensions without coordinates: y, x
236- >>> print(aspect_da.compute()) # compute the results
237- <xarray.DataArray 'aspect' (y: 6, x: 5)>
238- array([[ nan, nan , nan , nan , nan],
239- [ nan, -1. , 225. , 135. , nan],
240- [ nan, 343.61045967, 8.97262661, 33.69006753, nan],
241- [ nan, 307.87498365, 71.56505118, 54.46232221, nan],
242- [ nan, 191.30993247, 144.46232221, 255.96375653, nan],
243- [ nan, nan , nan , nan , nan]])
244- Dimensions without coordinates: y, x
245-
246- Aspect works with CuPy backed xarray DataArray.
247- Make sure you have a GPU and CuPy installed to run this example.
248- .. sourcecode:: python
249-
250- >>> import cupy
251- >>> data_cupy = cupy.asarray(data)
252- >>> raster_cupy = xr.DataArray(data_cupy, dims=['y', 'x'])
253- >>> aspect_cupy = aspect(raster_cupy)
254- >>> print(type(aspect_cupy.data))
255- <class 'cupy.core.core.ndarray'>
256- >>> print(aspect_cupy)
257- <xarray.DataArray 'aspect' (y: 6, x: 5)>
258- array([[ nan, nan, nan, nan, nan],
259- [ nan, -1., 225., 135., nan],
260- [ nan, 343.61047, 8.972626, 33.690067, nan],
261- [ nan, 307.87497, 71.56505 , 54.462322, nan],
262- [ nan, 191.30994, 144.46233 , 255.96376, nan],
263- [ nan, nan, nan, nan, nan]],
264- dtype=float32)
265- Dimensions without coordinates: y, x
266343 """
267344
268- mapper = ArrayTypeFunctionMapping (
269- numpy_func = _run_numpy ,
270- dask_func = _run_dask_numpy ,
271- cupy_func = _run_cupy ,
272- dask_cupy_func = _run_dask_cupy ,
273- )
274-
275- out = mapper (agg )(agg .data )
345+ if method not in ('planar' , 'geodesic' ):
346+ raise ValueError (
347+ f"method must be 'planar' or 'geodesic', got { method !r} "
348+ )
349+
350+ if method == 'planar' :
351+ mapper = ArrayTypeFunctionMapping (
352+ numpy_func = _run_numpy ,
353+ dask_func = _run_dask_numpy ,
354+ cupy_func = _run_cupy ,
355+ dask_cupy_func = _run_dask_cupy ,
356+ )
357+ out = mapper (agg )(agg .data )
358+
359+ else : # geodesic
360+ if z_unit not in Z_UNITS :
361+ raise ValueError (
362+ f"z_unit must be one of { sorted (set (Z_UNITS .values ()), key = str )} , "
363+ f"got { z_unit !r} "
364+ )
365+ z_factor = Z_UNITS [z_unit ]
366+
367+ lat_2d , lon_2d = _extract_latlon_coords (agg )
368+
369+ mapper = ArrayTypeFunctionMapping (
370+ numpy_func = _run_numpy_geodesic ,
371+ cupy_func = _run_cupy_geodesic ,
372+ dask_func = _run_dask_numpy_geodesic ,
373+ dask_cupy_func = _run_dask_cupy_geodesic ,
374+ )
375+ out = mapper (agg )(agg .data , lat_2d , lon_2d , WGS84_A2 , WGS84_B2 , z_factor )
276376
277377 return xr .DataArray (out ,
278378 name = name ,
0 commit comments