diff --git a/.claude/sweep-test-coverage-state.csv b/.claude/sweep-test-coverage-state.csv index 9bc639431..077bd8e4a 100644 --- a/.claude/sweep-test-coverage-state.csv +++ b/.claude/sweep-test-coverage-state.csv @@ -11,7 +11,7 @@ interpolate_spline,2026-06-04,,HIGH,1;3;5,scope=spline-only; cupy+dask_cupy spli mcda,2026-06-10,3149,HIGH,1;2;5,"Pass 1 (2026-06-10, deep-sweep test-coverage): test_mcda.py had 175 tests, all numpy or dask+numpy -- zero cupy/dask+cupy coverage despite explicit cupy branches in standardize._get_xp and combine._sort_descending (Cat 1 HIGH). Filed #3149, added ~70 tests: cross-backend parity for standardize (7 methods) x cupy/dask+numpy/dask+cupy, combine (wlc/wpm/fuzzy and-or-sum-product-gamma/owa) x 3 backends, constrain, boolean_overlay, sensitivity OAT+MC on GPU backends; metadata preservation (attrs/coords/dims/name) for every stage (Cat 5 MEDIUM); wpm all-NaN criterion + Inf propagation through wlc/fuzzy-and (Cat 2 MEDIUM). All RUN on a CUDA host: 233 passed, 11 xfailed. Probing surfaced real source bugs already filed by sibling sweeps as #3146 (owa raises on ALL dask backends -- _sort_descending calls nonexistent da.sort; owa cupy mixes numpy order weights into cupy stack; piecewise standardize broken on cupy + dask+cupy and categorical on dask+cupy via np.asarray on cupy chunks; monte_carlo sensitivity reads .values on cupy data) and #3147 (constrain drops attrs when masks applied) -- those paths pinned with strict xfail markers to flip on fix; constrain cupy/dask+cupy xfail(strict=False) on the known cupy 13.6 + xarray xr.where dependency incompat, not an mcda bug. Source untouched (test-only PR). LOW (documented, not fixed): name= output parameter untested across combine functions; empty (0-row) raster untested -- elementwise ops, judged low value. weights.py (ahp/rank) is pure-numpy metadata, backend matrix N/A, already well covered." polygon_clip,2026-06-10,3197,MEDIUM,1;2;3;5,"deep-sweep test-coverage 2026-06-10 on a CUDA host. Existing file covered numpy well + one parity test per dask/cupy/dask+cupy backend. Filed #3197 (test-only) and added 13 tests: Cat1 GPU param/NaN coverage (cupy + dask+cupy each get custom nodata, all_touched=True, and NaN-input preservation vs numpy; previously only crop=False inner polygon ran on GPU); Cat2 Inf/-Inf preserved, all-NaN input -> all-NaN, int32 + sentinel nodata=-1; Cat3 Nx1 + 1xN strip rasters; Cat5 coords preserved (crop=False) + crop coords are a contiguous subset of input coords (crop=True). All 13 RAN+PASSED on GPU (6 GPU tests not skipped); full file 36 passed 0 skipped. LOW (documented, NOT fixed): rasterize_kw forwarding never tested; non-square cellsize never tested. SOURCE NOTE (out of scope, not filed): clip_polygon docstring says 'named y and x dims' but rasterize() hard-requires literal y/x, so lat/lon-dim rasters raise -- dim-name preservation (Cat5) is therefore unsupported by contract, not a test gap. SOURCE NOTE 2: polygon_clip.py:216 still passes rasterize(use_cuda=True) on dask+cupy (renamed to gpu= in #3089); harmless deprecation alias today, candidate for an api-consistency follow-up." polygonize,2026-06-12,3299,MEDIUM,1,"Pass 4 (2026-06-12): added test_polygonize_mask_chunk_mismatch_3299.py (25 tests, all passing on a CUDA host incl. dask+cupy). Closes Cat 1 MEDIUM: the _polygonize_dask mask-rechunk branch (mask_data.chunks != dask_data.chunks -> rechunk) was never exercised; every prior dask masked test used mask chunks identical to the raster's. Mismatched layouts pinned against same-backend aligned-mask reference: (6,7) same-grid-shape misalignment (the silent-corruption layout for int rasters), single-chunk (15,18), more-blocks (4,5); int+float rasters, connectivity 4/8, dask+numpy and dask+cupy; plus exact-geometry single-masked-pixel hole anchor. Mutation (delete the rechunk guard) flips all 25 red; clean md5 restore. Full polygonize suite 486 passed / 16 skipped. Test-only; source untouched. Issue #3299. Audit re-confirmed Cat 2/3/4 closed by passes 1-3 and post-2026-05-29 changes (#2913 float-mask fix flipped prior xfails, #3041 has issue-2677 test file, #2673/#2817 covered by batch-invariance and heap tests); Cat 5 N/A (no DataArray output; CRS/transform propagation already tested). | Pass 3 (2026-05-29): added test_polygonize_mask_dtype_coverage_2026_05_29.py (41 passed, 8 xfailed on a CUDA host). Closes Cat 4 MEDIUM parameter-coverage gap: mask= is documented to accept bool/integer/float values but every prior test passed only a bool mask. Integer masks (int32/int64) now pinned against the same-backend bool-mask output on all four backends x both raster dtypes x connectivity 4/8; float-mask-on-integer-raster also pinned. Each backend is compared to its OWN bool reference to isolate mask-dtype from the unrelated numpy-vs-dask hole-vs-single-ring representation difference. Mutation (drop the not-mask[ij] exclusion in _calculate_regions) flips 11 tests red incl. the pixel-exclusion sanity anchor; clean md5 restore. Surfaced source bug #2623: a float-dtype mask on a float-dtype raster raises TypeError at polygonize.py:918 (mask & nan_mask; bitwise_and undefined for float&bool; cupy/dask route floats through _polygonize_numpy so they crash too; int masks coerce fine). 8 float-mask cases marked xfail(strict, raises=TypeError) referencing #2623. Test-only; source untouched. | Pass 2 (2026-05-27): added test_polygonize_atol_rtol_backend_coverage_2026_05_27.py with 15 tests, all passing on a CUDA host. Closes Cat 4 MEDIUM parameter-coverage gap on atol/rtol forwarding through the cupy and dask+cupy backends. atol/rtol were exposed by #2173 / #2194 and thread through _polygonize_cupy (polygonize.py:808) and _polygonize_dask (polygonize.py:1719); the dask path further plumbs them into dask.delayed(_polygonize_chunk)(...) at lines 1748-1754 and into _bucket_key_for_value for cross-chunk merge bucketing at lines 1757-1758. Pre-existing tests covered non-default atol/rtol only on numpy and dask+numpy. The cupy and dask+cupy dispatchers were untested -- a regression dropping the kwargs there would silently change the float polygon count and would not be caught. Same dispatcher-silently-drops-kwarg pattern fixed by #1561 / #1605 / #1685 / #1810 / #1974 on adjacent GeoTIFF surfaces. 15 tests: cupy strict-equality + default-tolerance pin on _REPRO_2173, dask+cupy strict-equality single-chunk + multi-chunk (engages cross-chunk merge bucket) + default-tolerance multi-chunk pin, cupy intermediate-atol small/large pair, dask+cupy intermediate-atol single/multi-chunk small + single-chunk large, cupy integer atol-ignored matrix, dask+cupy integer atol-ignored single-chunk + multi-chunk, cupy rtol-only large/small matrix. Mutation against _polygonize_cupy float branch (drop atol/rtol kwargs in the _polygonize_numpy forward call at polygonize.py:823-825) flips 3 of 5 cupy tests red; mutation against dask.delayed(_polygonize_chunk)(...) at polygonize.py:1748-1754 (drop atol, rtol args) flips 2 of 6 dask+cupy tests red. Confirmed clean restore via md5sum. Source untouched. Filed issue #2537 (test-only). Cat 4 MEDIUM (parameter coverage on cupy + dask+cupy atol/rtol forwarding). Pass 1 (2026-05-19): added test_polygonize_coverage_2026_05_19.py with 58 tests, all passing on a CUDA host. Closes Cat 3 HIGH 1x1 / Nx1 single-column geometric gaps (Nx1 exercises the nx==1 padding path at polygonize.py:565 and the cupy nx==1 numpy-fallback at polygonize.py:671), Cat 3 MEDIUM 1xN single-row and all-equal-value rasters on all four backends. Closes Cat 2 HIGH NaN parity for cupy + dask+cupy (numpy/dask were already covered by test_polygonize_nan_pixels_excluded*), Cat 2 MEDIUM all-NaN raster on all four backends, Cat 2 HIGH +/-Inf pins on all four backends. Filed source-bug issue #2155: numpy/dask/dask+cupy backends silently absorb Inf cells into adjacent finite polygons because _is_close reduces abs(inf-inf) to nan; cupy backend handles Inf correctly. Pins lock the asymmetric behaviour so the fix is visible. Closes Cat 1 MEDIUM simplify_tolerance + mask= parity gaps on dask+cupy backend (numpy/cupy/dask were already covered). Closes Cat 4 MEDIUM column_name non-default value across geopandas/spatialpandas/geojson return types and Cat 4 MEDIUM validation error paths (bad connectivity, bad transform length, mask shape mismatch, mask underlying-type mismatch). Cat 5 N/A: polygonize returns lists/dataframes, not a DataArray with attrs to propagate." -proximity,2026-06-09,2692;3139,MEDIUM,1;2;3,"Pass 3 (2026-06-09, deep-sweep test-coverage): module grew since Pass 2 (#2807 metric validation, #2812 GREAT_CIRCLE brute force, #2850/#2851 input validation, #2854/#2908 halo fixes, tie-break routing) and each landed with its own tests; Pass 2's stale LOW (invalid distance_metric fallback) is FIXED and tested (#2807). Found 3 MEDIUM gaps, filed #3139, added 40 tests (all RUN and PASS on a CUDA host; full file 450 passed): (1) Cat 2 integer-dtype raster untested on any backend -- bounded dask pads int arrays with boundary=np.nan which casts to INT_MIN phantom targets, only neutralized because the coordinate-grid pads are real NaNs; pinned int32 x 3 funcs x 4 backends x bounded/unbounded vs float64 numpy baseline + explicit target_values; (2) Cat 1 bounded dask+cupy (_process_dask_cupy) only ever ran EUCLIDEAN; pinned MANHATTAN+GREAT_CIRCLE x 3 funcs with a routing spy; mutation (pad=0) flips all 6 red, clean md5 restore; (3) Cat 3 empty 0-row/0-col raster unpinned; fails fast with IndexError, pinned raises. All behaviors verified correct before tests were added -- no source bug, source untouched. LOW (documented, not fixed): -inf pixel input never tested (+inf is; isfinite is symmetric). || Pass 2 (2026-06-02): added 18 tests to test_proximity.py closing the two MEDIUM gaps Pass 1 left open, all RUN and passing on a CUDA host across numpy/cupy/dask+numpy/dask+cupy (15 cross-backend + 3 error-path). Source untouched. Cat 4 MEDIUM (error path): _process raises ValueError when raster.dims != (y, x) (proximity.py:1043) but no test exercised the swapped x/y guard; test_wrong_dim_order_raises pins it for proximity/allocation/direction. Cat 2 MEDIUM (all-NaN input): Pass 1 noted all-NaN/all-zero on eager numpy+cupy was unpinned; test_all_nan_raster_all_nan_output pins an all-NaN 6x6 raster -> all-NaN float32 output on all four backends x three functions. Remaining LOW (documented): invalid distance_metric string silently falls back to EUCLIDEAN (proximity.py:1049-1051). || PREVIOUS: Pass 1 (2026-05-29): added 65 tests to test_proximity.py closing three coverage gaps, all RUN and passing on a CUDA host (numpy/cupy/dask+numpy/dask+cupy). Issue #2692, PR opened. Source untouched. Cat 3 HIGH: degenerate raster shapes (1x1 single pixel, Nx1 column strip, 1xN row strip) had zero coverage for proximity/allocation/direction on any backend; they stress the line-sweep kernel boundaries (_process_proximity_line) and the GPU brute-force kernel grid sizing (_proximity_cuda_kernel via cuda_args). Pinned all three shapes x three functions x four backends against hand-checked expected values; mutation of a pinned direction expectation confirms teeth. Cat 1/4 HIGH: allocation and direction only ran EUCLIDEAN across backends; MANHATTAN and GREAT_CIRCLE were cross-backend-tested for proximity only. Pinned both metrics x two functions x four backends against the numpy baseline (all match). Cat 5 MEDIUM: no test set non-empty res/crs attrs so the attrs-preservation assertion in general_output_checks compared two empty dicts. proximity reads attrs['res'] via get_dataarray_resolution for bounded-dask chunk padding, so added attrs round-trip tests on four backends plus a bounded-dask test where a res attr matching the coordinate spacing must equal the numpy baseline. A res attr that lies about the spacing mis-sizes the map_overlap depth; source fragility, not a test gap, left for a separate accuracy issue. Cat 2 (NaN/Inf input) already covered by the shared test_raster fixture (embeds np.inf and np.nan, runs on four backends). Remaining LOW: all-NaN / all-zero input on eager numpy+cupy not directly pinned." +proximity,2026-06-18,2692;3139,MEDIUM,1;4,"Pass 4 (2026-06-18, deep-sweep test-coverage): 1 MEDIUM, 1 LOW. MEDIUM (Cat 4/Cat 1): all three public funcs are @supports_dataset and document Dataset-in/Dataset-out, but no test ever passed a Dataset; the shared decorator is covered generically in test_dataset_support.py which never lists proximity/allocation/direction, so per-variable _process dispatch + attrs/coords round-trip + result.name=None reset were unpinned. Added test_dataset_input_processes_each_variable (3 funcs x 4 backends, 12 tests); numpy+dask+cupy+dask+cupy all RUN and PASS on this CUDA host, expected built from numpy baseline to avoid implicit cupy host conversion. Verified working first -- no source bug, source untouched. Full file 528 passed. LOW (documented, not fixed): public exports euclidean_distance / manhattan_distance have no direct unit test (only great_circle_distance does); both are trivial pure fns exercised indirectly through proximity. || Pass 3 (2026-06-09, deep-sweep test-coverage): module grew since Pass 2 (#2807 metric validation, #2812 GREAT_CIRCLE brute force, #2850/#2851 input validation, #2854/#2908 halo fixes, tie-break routing) and each landed with its own tests; Pass 2's stale LOW (invalid distance_metric fallback) is FIXED and tested (#2807). Found 3 MEDIUM gaps, filed #3139, added 40 tests (all RUN and PASS on a CUDA host; full file 450 passed): (1) Cat 2 integer-dtype raster untested on any backend -- bounded dask pads int arrays with boundary=np.nan which casts to INT_MIN phantom targets, only neutralized because the coordinate-grid pads are real NaNs; pinned int32 x 3 funcs x 4 backends x bounded/unbounded vs float64 numpy baseline + explicit target_values; (2) Cat 1 bounded dask+cupy (_process_dask_cupy) only ever ran EUCLIDEAN; pinned MANHATTAN+GREAT_CIRCLE x 3 funcs with a routing spy; mutation (pad=0) flips all 6 red, clean md5 restore; (3) Cat 3 empty 0-row/0-col raster unpinned; fails fast with IndexError, pinned raises. All behaviors verified correct before tests were added -- no source bug, source untouched. LOW (documented, not fixed): -inf pixel input never tested (+inf is; isfinite is symmetric). || Pass 2 (2026-06-02): added 18 tests to test_proximity.py closing the two MEDIUM gaps Pass 1 left open, all RUN and passing on a CUDA host across numpy/cupy/dask+numpy/dask+cupy (15 cross-backend + 3 error-path). Source untouched. Cat 4 MEDIUM (error path): _process raises ValueError when raster.dims != (y, x) (proximity.py:1043) but no test exercised the swapped x/y guard; test_wrong_dim_order_raises pins it for proximity/allocation/direction. Cat 2 MEDIUM (all-NaN input): Pass 1 noted all-NaN/all-zero on eager numpy+cupy was unpinned; test_all_nan_raster_all_nan_output pins an all-NaN 6x6 raster -> all-NaN float32 output on all four backends x three functions. Remaining LOW (documented): invalid distance_metric string silently falls back to EUCLIDEAN (proximity.py:1049-1051). || PREVIOUS: Pass 1 (2026-05-29): added 65 tests to test_proximity.py closing three coverage gaps, all RUN and passing on a CUDA host (numpy/cupy/dask+numpy/dask+cupy). Issue #2692, PR opened. Source untouched. Cat 3 HIGH: degenerate raster shapes (1x1 single pixel, Nx1 column strip, 1xN row strip) had zero coverage for proximity/allocation/direction on any backend; they stress the line-sweep kernel boundaries (_process_proximity_line) and the GPU brute-force kernel grid sizing (_proximity_cuda_kernel via cuda_args). Pinned all three shapes x three functions x four backends against hand-checked expected values; mutation of a pinned direction expectation confirms teeth. Cat 1/4 HIGH: allocation and direction only ran EUCLIDEAN across backends; MANHATTAN and GREAT_CIRCLE were cross-backend-tested for proximity only. Pinned both metrics x two functions x four backends against the numpy baseline (all match). Cat 5 MEDIUM: no test set non-empty res/crs attrs so the attrs-preservation assertion in general_output_checks compared two empty dicts. proximity reads attrs['res'] via get_dataarray_resolution for bounded-dask chunk padding, so added attrs round-trip tests on four backends plus a bounded-dask test where a res attr matching the coordinate spacing must equal the numpy baseline. A res attr that lies about the spacing mis-sizes the map_overlap depth; source fragility, not a test gap, left for a separate accuracy issue. Cat 2 (NaN/Inf input) already covered by the shared test_raster fixture (embeds np.inf and np.nan, runs on four backends). Remaining LOW: all-NaN / all-zero input on eager numpy+cupy not directly pinned." rasterize,2026-06-18,2614;3102;3105;3296;3383,HIGH,4,"Pass 7 (2026-06-18, deep-sweep test-coverage): #3383 found a Cat 4 (error-path) gap -- rasterize() input-validation guards had no tests. Added 4 test classes (18 tests, all RAN+PASSED on a CUDA host; full module 233 passed/2 skipped): TestFillRepresentableGuard (NaN/out-of-range int + non-False bool fill rejected #2504/#3054, + valid int/bool/float-NaN negatives), TestBurnValueSafeIntegerGuard (|burn|>2**53-1 into int rejected #3056, + boundary 2**53-1 and float-dtype exemption), TestNonFiniteBurnValueGuard (NaN/inf burn into int/bool rejected #3085, + merge='count' exemption and float-dtype negatives), TestNonFiniteGeometryCoordsDropped (NaN/inf geometry coords dropped with UserWarning #3295, list+gdf paths). All guards run pre-dispatch on the CPU path so no GPU needed. Test-only PR, no source change." reproject,2026-06-09,2618;3050;3100;3101;3141,MEDIUM,1,"CI follow-up same day: first CI run of the threaded streaming branch hard-crashed macos-arm64 py3.14 (SIGABRT in numba call_cfunc, two ThreadPoolExecutor threads concurrently inside try_numba_transform/tmerc_inverse) -- the projection kernels are @njit(parallel=True) and numba's workqueue threading layer aborts on concurrent entry; filed source bug #3141. Test fix: threaded parity test now uses transform_precision=0 (per-thread pyproj Transformer, no numba), the NaN multi-tile test and 3-D xfail forced serial (max_memory=1) so the numba fast path stays covered without concurrent entry. windows-3.14 failure was fail-fast collateral (its suite fully passed). Pass 2026-06-09 (deep-sweep test-coverage): delta re-sweep one day after the 2026-06-08 pass; module modified today by #3077 (datum-probe warning silencing) and #3081 (merge output-size guard backend-aware) -- both landed WITH their own tests (TestDatumProbeNoProjWarning; TestSecurityGuards merge-guard trio incl. the monkeypatched in-memory raise), so the delta added no gap; the guard branching is is_dask-only, so cupy eager shares the tested numpy branch (no per-backend guard test needed). Found one MEDIUM Cat 1 gap every prior pass missed: the 5th dispatch branch of reproject() -- the streaming fallback (_reproject_streaming / _process_tile_batch / _parse_max_memory, taken when source >512MB and dask is not importable) -- had zero coverage anywhere; _parse_max_memory only runs on that branch so the existing max_memory kwarg tests never reached it. Filed #3101, added test_reproject_streaming_3101.py (15 tests: parity vs in-memory numpy for threaded / serial(max_memory=1) / single-tile / nearest+NaN, plus 10 _parse_max_memory unit cases). Probe surfaced source bug #3100: streaming assembly allocates a 2-D output buffer but 3-D sources yield (h,w,b) tiles -> ValueError broadcast in both assembly loops; pinned with strict xfail, source fix left to #3100 (test-only PR, source untouched). CPU-only path so no GPU tests needed (CUDA host; file ran 14 passed + 1 xfailed). LOW carried (documented, not fixed): reproject(name=) / merge(name=) override values untested (only merge name fallback covered); non-square-cellsize successful anisotropic run; dask.bag distributed branch of _reproject_streaming still unexercised (needs a live distributed client). || PREVIOUS: Pass 2026-06-08 (deep-sweep test-coverage): #3050 closes the one live gap found this pass. reproject()'s dask+cupy backend was parity-tested only with resampling='cubic' (TestCupyPyprojFallbackParity::test_projected_to_projected_dask_cupy_match); nearest/bilinear were covered on numpy (end-to-end) and eager cupy (parametrized test_projected_to_projected_numpy_cupy_match) but never on the dask+cupy chunk-assembly path. Parametrized that test over ['nearest','bilinear','cubic']; all 3 RUN+PASS on a CUDA host. Cat 4 MEDIUM (resampling-mode parameter coverage on the dask+cupy backend). Test-only, source untouched. Re-confirmed _merge.merge() has NO genuine cupy/dask+cupy backend (_merge_inmemory/_merge_dask use _merge_arrays_numpy + raster.values; _merge_arrays_cupy is imported but never dispatched = dead code, not a test gap) matching the prior pass's observation. reproject() otherwise saturated across all 4 backends, NaN/Inf/all-NaN, degenerate shapes, metadata, vertical, bounds_policy, integer nodata. LOW (documented, not filed): dask+cupy resampling-mode parity is the only per-mode-per-backend cell that had been missing. || PREVIOUS: Pass 2026-05-29: reproject already has a deep suite (369 tests in test_reproject.py + coverage/gate files) covering all 4 backends, NaN/Inf/all-NaN/all-Inf, 1x1/2x2, metadata, vertical shift, bounds_policy x backends, integer nodata x backends. Gaps found: Cat 3 HIGH single-row (1xN) and single-col (Nx1) strip rasters never tested (hit size<2 branch of _validate_regular_axis + degenerate resampling axis); Cat 3 MEDIUM constant-value/zero-gradient raster never reprojected. Added TestDegenerateShapeReproject (12 tests): 1xN+Nx1 strips x numpy/dask/cupy/dask+cupy, constant raster numpy value-preservation + cross-backend parity. All 12 executed and passed on a CUDA host. Test-only, no source change (#2618). LOW (documented only): _merge._merge_arrays_cupy imported but never called by merge() (host-bounces via _merge_arrays_numpy) - dead-code source observation not a test gap; non-square cellsize reproject only covered via resolution-tuple validation errors not a successful anisotropic run." resample,2026-05-29,2547;2615,HIGH,1;2;3;5,"Pass 2 (2026-05-29): added test_resample_cupy_agg_fallback_2615.py (6 tests, all passing on CUDA host). Closes Cat 1 MEDIUM backend-coverage gap: the cupy eager aggregate CPU fallback for average/min/max at a NON-integer downsample factor (_run_cupy fy==int(fy) branch in resample.py ~L957-973) was never exercised; existing TestCuPyParity used 12x12 scale 0.5 (integer factor 2 -> GPU reshape path) and only median/mode hit the host fallback. New tests use 10x10 scale 0.3 (factor 3.33) for average/min/max parity vs numpy plus a NaN-masked variant. Issue #2615. Module is otherwise very thoroughly covered (test_resample.py + 3 supplementary files); no remaining HIGH gaps found. Pass 1 (2026-05-27): added test_resample_coverage_2026_05_27.py with 70 tests (68 passing, 2 skipped). Closes Cat 3 HIGH Nx1 single-column gap across numpy/cupy/dask+numpy/dask+cupy x 8 methods (nearest/bilinear/cubic/average/min/max/median/mode) plus Nx1 upsample-nearest parity and Nx1 cross-backend aggregate parity. Closes Cat 2 MEDIUM NaN-parity gap on cupy and dask+cupy (existing TestCuPyParity/TestDaskCuPyParity used random data without NaN; the weight-mask gate and spline-prepad had no GPU NaN coverage). Closes Cat 3 MEDIUM all-equal-value raster across 8 methods (downsample) and 3 interp methods (upsample) plus a constant-with-NaN aggregate variant. Closes Cat 5 MEDIUM non-default dim-name propagation: lat/lon, latitude/longitude, and (channel, lat, lon) 3D round-trip without being renamed to y/x; per-dim attrs (units) preserved. Closes Cat 3 MEDIUM empty-raster behaviour pin: 0-row and 0-col rasters raise (currently IndexError) -- contract covered. Filed source-bug issue #2547: cubic on dask backends fails for Nx1 / arrays smaller than depth=16; the 2 skipped tests in this file gate on that fix landing. Source untouched." diff --git a/xrspatial/tests/test_proximity.py b/xrspatial/tests/test_proximity.py index c364e8939..b56e9ade3 100644 --- a/xrspatial/tests/test_proximity.py +++ b/xrspatial/tests/test_proximity.py @@ -2501,6 +2501,69 @@ def test_proximity_numpy_no_scipy_fallback_is_exact(): np.testing.assert_allclose(result.data, expected, rtol=1e-5, atol=1e-7) +# --------------------------------------------------------------------------- +# @supports_dataset: proximity / allocation / direction accept an xr.Dataset +# and process each data variable independently (test-only; no source change). +# +# All three functions are decorated with @supports_dataset and their +# docstrings document the Dataset-in / Dataset-out contract, but every other +# test in this file passes a DataArray. The shared decorator is exercised +# generically in test_dataset_support.py, but that file never lists +# proximity/allocation/direction, so the way these functions interact with it +# (per-variable _process dispatch, attrs/coords round-trip, and the +# result.name = None reset each function applies) is otherwise unpinned. +# Behaviour was verified correct on numpy and dask before these were added. +# --------------------------------------------------------------------------- + +def _dataset_two_vars(backend): + """Two-variable Dataset on the requested backend, shared lat/lon coords.""" + data_a = np.zeros((6, 6), dtype=np.float64) + data_a[1, 1] = 1.0 + data_a[4, 4] = 2.0 + data_b = np.zeros((6, 6), dtype=np.float64) + data_b[0, 0] = 3.0 + data_b[5, 2] = 4.0 + + da_a = _backend_raster(data_a, backend) + da_b = _backend_raster(data_b, backend) + ds = xr.Dataset({'a': da_a, 'b': da_b}, attrs={'source': 'test'}) + return ds + + +@pytest.mark.parametrize("backend", ['numpy', 'dask+numpy', 'cupy', 'dask+cupy']) +@pytest.mark.parametrize("func", [proximity, allocation, direction]) +def test_dataset_input_processes_each_variable(backend, func): + if has_cuda_and_cupy() is False and 'cupy' in backend: + pytest.skip("Requires CUDA and CuPy") + if 'dask' in backend and da is None: + pytest.skip("dask not available") + + # Build per-variable expected results from the numpy backend so the + # comparison array stays a plain ndarray (a cupy expected would force an + # implicit host conversion in general_output_checks). + numpy_ds = _dataset_two_vars('numpy') + expected = {name: func(numpy_ds[name], x='lon', y='lat').data + for name in ('a', 'b')} + + ds = _dataset_two_vars(backend) + result = func(ds, x='lon', y='lat') + + # Dataset in -> Dataset out, same variables, attrs preserved. + assert isinstance(result, xr.Dataset) + assert set(result.data_vars) == {'a', 'b'} + assert result.attrs == ds.attrs + + # Each variable must equal the standalone DataArray call on that variable. + for name in ('a', 'b'): + general_output_checks(ds[name], result[name], expected[name], + verify_dtype=True) + # Coordinates round-trip on every variable. + np.testing.assert_array_equal( + result[name]['lon'].data, ds[name]['lon'].data) + np.testing.assert_array_equal( + result[name]['lat'].data, ds[name]['lat'].data) + + # --------------------------------------------------------------------------- # Issue #3389: the max_distance comparison must use the same precision on the # CPU brute-force and the CUDA kernel. _distance casts to float32 before the