Description
apply() and focal_stats() cast float64 input down to float32 on every backend. convolve_2d() already preserves the input floating dtype (via _promote_float) and has a test asserting this, but the focal functions don't, so a float64 raster silently comes back as float32.
The downcast happens in a few places in xrspatial/focal.py:
_apply_numpy casts with data.astype(np.float32) (~line 462)
_apply_dask_numpy casts with data.astype(np.float32) (~line 500)
_apply_cupy casts with data.astype(cupy.float32) (~line 513)
_focal_stats_func_cupy allocates the output as dtype='f4' (~line 985)
_focal_stats_cupy and _focal_stats_dask_cupy cast with data.astype(cupy.float32) (~lines 1041, 1092)
Expected behavior
apply() and focal_stats() should preserve the input floating dtype across all four backends (numpy, cupy, dask+numpy, dask+cupy): float64 in, float64 out, matching convolve_2d(). Integer input should still promote to at least float32.
Why it matters
Silent precision loss is a correctness footgun for downstream numerical work, and it's inconsistent: convolve_2d already preserves float64 and has a test for it. Focal should behave the same way.
Description
apply()andfocal_stats()cast float64 input down to float32 on every backend.convolve_2d()already preserves the input floating dtype (via_promote_float) and has a test asserting this, but the focal functions don't, so a float64 raster silently comes back as float32.The downcast happens in a few places in
xrspatial/focal.py:_apply_numpycasts withdata.astype(np.float32)(~line 462)_apply_dask_numpycasts withdata.astype(np.float32)(~line 500)_apply_cupycasts withdata.astype(cupy.float32)(~line 513)_focal_stats_func_cupyallocates the output asdtype='f4'(~line 985)_focal_stats_cupyand_focal_stats_dask_cupycast withdata.astype(cupy.float32)(~lines 1041, 1092)Expected behavior
apply()andfocal_stats()should preserve the input floating dtype across all four backends (numpy, cupy, dask+numpy, dask+cupy): float64 in, float64 out, matchingconvolve_2d(). Integer input should still promote to at least float32.Why it matters
Silent precision loss is a correctness footgun for downstream numerical work, and it's inconsistent:
convolve_2dalready preserves float64 and has a test for it. Focal should behave the same way.