From 31b20b0b5c9573ccc2141e04d60d5cd834867b50 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 1 Jul 2026 07:01:45 -0700 Subject: [PATCH 1/2] Return all-NaN surface for empty-valid pycnophylactic input (#3406) pycnophylactic raised an opaque numpy ValueError ("zero-size array to reduction operation fmax") when no pixel was valid for smoothing -- the zones raster was entirely NaN, or no zone id matched the values mapping. The convergence check called np.nanmax on the empty valid slice. Guard the empty-valid case before the smoothing loop and return the all-NaN surface, matching disaggregate's behaviour on the same inputs. Unpin the xfail(strict) coverage and add a cupy-fallback case. --- xrspatial/dasymetric.py | 7 +++++++ xrspatial/tests/test_dasymetric.py | 25 +++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/xrspatial/dasymetric.py b/xrspatial/dasymetric.py index 43f283b21..aa6a54a61 100644 --- a/xrspatial/dasymetric.py +++ b/xrspatial/dasymetric.py @@ -645,6 +645,13 @@ def _pycnophylactic_numpy(zones_arr, values_dict, nodata_zone, # pixels that belong to some zone (valid for smoothing) valid = ~np.isnan(surface) + # no valid pixels (all-NaN zones, or no zone id matched values): the + # smoothing loop has nothing to converge on and np.nanmax over the + # empty valid slice would raise. Return the all-NaN surface, matching + # disaggregate's behaviour on the same inputs. + if not valid.any(): + return surface + for _ in range(max_iterations): # Laplacian smoothing: mean of 4-connected neighbours smoothed = np.full_like(surface, np.nan) diff --git a/xrspatial/tests/test_dasymetric.py b/xrspatial/tests/test_dasymetric.py index 07e9445fa..f5e51acc2 100644 --- a/xrspatial/tests/test_dasymetric.py +++ b/xrspatial/tests/test_dasymetric.py @@ -1072,10 +1072,9 @@ def test_three_class_conservation_only(self): class TestPycnophylacticEmptyValid: """pycnophylactic crashes when no pixel is valid for smoothing (#3406). - disaggregate handles the same inputs gracefully (all-NaN output); these - are xfail(strict) until #3406 makes pycnophylactic agree. When the - source fix lands, the tests start XPASSing and strict mode flips them - red, prompting removal of the marker. + disaggregate handles the same inputs gracefully (all-NaN output); #3406 + makes pycnophylactic agree by returning the all-NaN surface instead of + raising on the empty-valid slice. """ def test_disaggregate_all_nan_zones_is_all_nan(self): @@ -1085,23 +1084,21 @@ def test_disaggregate_all_nan_zones_is_all_nan(self): result = disaggregate(zones, {1: 100.0}, weight) assert np.all(np.isnan(result.values)) - @pytest.mark.xfail( - reason="#3406: pycnophylactic raises ValueError on empty-valid input", - strict=True, - raises=ValueError, - ) def test_pycnophylactic_all_nan_zones(self): zones = create_test_raster(np.full((3, 3), np.nan), backend='numpy') result = pycnophylactic(zones, {1: 100.0}) assert np.all(np.isnan(result.values)) - @pytest.mark.xfail( - reason="#3406: pycnophylactic raises ValueError on empty-valid input", - strict=True, - raises=ValueError, - ) def test_pycnophylactic_no_matching_zone(self): zones = create_test_raster( np.array([[1, 1], [2, 2]], dtype=np.float64), backend='numpy') result = pycnophylactic(zones, {99: 100.0}) assert np.all(np.isnan(result.values)) + + @pytest.mark.skipif(not has_cuda_and_cupy(), + reason="CUDA/CuPy not available") + def test_pycnophylactic_all_nan_zones_cupy(self): + """cupy fallback shares the numpy path; guard it too.""" + zones = create_test_raster(np.full((3, 3), np.nan), backend='cupy') + result = pycnophylactic(zones, {1: 100.0}) + assert np.all(np.isnan(result.data.get())) From 3e94d0915e07ebc205f5ed68d766fbf16989a274 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 1 Jul 2026 07:03:32 -0700 Subject: [PATCH 2/2] Address review nit: fix stale test class docstring (#3406) --- xrspatial/tests/test_dasymetric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xrspatial/tests/test_dasymetric.py b/xrspatial/tests/test_dasymetric.py index f5e51acc2..079b14bd7 100644 --- a/xrspatial/tests/test_dasymetric.py +++ b/xrspatial/tests/test_dasymetric.py @@ -1070,7 +1070,7 @@ def test_three_class_conservation_only(self): # --------------------------------------------------------------------------- class TestPycnophylacticEmptyValid: - """pycnophylactic crashes when no pixel is valid for smoothing (#3406). + """pycnophylactic with no pixel valid for smoothing (#3406). disaggregate handles the same inputs gracefully (all-NaN output); #3406 makes pycnophylactic agree by returning the all-NaN surface instead of