diff --git a/.claude/sweep-metadata-state.csv b/.claude/sweep-metadata-state.csv index 56696a262..35d2545e2 100644 --- a/.claude/sweep-metadata-state.csv +++ b/.claude/sweep-metadata-state.csv @@ -8,6 +8,7 @@ geotiff,2026-06-09,3116,HIGH,2;3,"Re-audited 2026-06-09 (agent-ae89ff94a64e3ee8f interpolate,2026-06-12,3288,MEDIUM,5,kriging K_inv-None fallback was numpy-backed on all backends and misnamed the variance raster; fixed via #3288. All 4 backends verified end-to-end on GPU host. LOW (documented only): template nodatavals/_FillValue copied verbatim while fill_value is the actual output sentinel; tests codify attrs==template.attrs mcda,2026-06-10,3147,HIGH,1,"constrain() dropped all attrs (res/crs/nodatavals) whenever exclude non-empty (xr.where takes attrs from scalar fill); fixed via attrs restore, tests for numpy/dask/dask+cupy. All other mcda funcs keep attrs/coords/dims on all 4 backends. Out-of-scope crashes noted for backend-parity: owa broken on cupy (numpy order-weights x cupy) and on dask (da.sort does not exist); sensitivity monte_carlo crashes on cupy/dask+cupy (.values on cupy); xr.where compute on cupy/dask+cupy hits known cupy13.6/xarray2025.12 incompat." multispectral,2026-06-20,3429,MEDIUM,2;3,"true_color() hardcoded y/x dims + dropped extra coords; fixed PR #3434 (all 4 backends verified, CUDA available)" +perlin,2026-06-23,,MEDIUM,2,perlin() dropped input x/y coords (kept attrs incl crs); fixed by passing coords=agg.coords; verified numpy/cupy/dask/dask+cupy; issues disabled on fork so no issue number; PR pending polygonize,2026-06-12,3293,MEDIUM,1,"Audited 2026-06-12 (agent-a86d90abea41b04cf worktree, branch deep-sweep-metadata-polygonize-2026-06-12). CUDA available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live for int, float+NaN, and no-georef rasters. polygonize returns vector output (numpy/awkward/geopandas/spatialpandas/geojson), not a DataArray, so Cats 2-4 reinterpreted as transform/CRS/value-dtype propagation. Transform auto-detect (attrs['transform'] -> rio.transform() -> x/y coords, #2536/#2607) and CRS resolution run in public polygonize() before dispatch, so all 4 backends emit identical columns, bounds, and CRS (verified live). Column value dtype follows input dtype on every backend. NEW MEDIUM finding #3293 (Cat 1): _detect_raster_crs ignored the _xrspatial_no_georef marker that _detect_raster_transform honours, so a geotiff-reader crs_only raster (attrs carry both crs and the marker; metadata_to_attrs writes crs independent of has_georef) produced a GeoDataFrame claiming EPSG:#### over pixel-space geometries -- the #2536 metadata-lies-about-the-data mismatch through the marker channel. contour.py imports the same helper and inherits the fix. Fix: early return None in _detect_raster_crs on the marker + docstring note; 2 new tests in TestPolygonizeCRSPropagation. polygonize+contour suites 274 passed; all 9 auxiliary polygonize test files 303 passed. rotated-read path unaffected (reader drops CRS there). No CRITICAL/HIGH/LOW findings." proximity,2026-05-29,2723,MEDIUM,4;5,"Audited 2026-05-29 (agent-a61dbadc2452a2003 worktree, branch deep-sweep-metadata-proximity-2026-05-29). CUDA+cupy available; all 4 backends (numpy/cupy/dask+numpy/dask+cupy) run live end-to-end for proximity/allocation/direction, both bounded (finite max_distance) and unbounded. Cat 1 (attrs res/crs/transform/nodatavals/_FillValue), Cat 2 (coords + coord dtype), and Cat 3 (dims) all preserved and identical across the 4 backends -- public funcs wrap with xr.DataArray(coords=raster.coords, dims=raster.dims, attrs=raster.attrs). NEW MEDIUM finding #2723 (Cat 4 + Cat 5): (a) bounded dask+numpy path (_process_dask -> da.map_overlap with meta=np.array(())) declared output dtype float64 while the chunk fn returns float32 and numpy/cupy/dask+cupy + the unbounded KDTree path all declare float32; docstrings show dtype=float32. Fix: meta=np.array((), dtype=np.float32). (b) dask backends leaked an internal dask op name (_trim-, _kdtree_chunk_fn-, asarray-) into result.name while numpy/cupy return None. Fix: assign result.name=None after construction in all 3 public funcs (xarray ignores a name=None kwarg for named dask arrays, so the reset must happen post-construction). Same .name-leak class as zonal #2611. PR #2728 off child branch deep-sweep-metadata-proximity-2026-05-29-01. New parametrized regression test test_output_metadata_consistent_across_backends asserts declared dtype float32 + name None across all 4 backends x 3 funcs x bounded/unbounded; full test_proximity.py suite 93 passed. No other CRITICAL/HIGH/MEDIUM/LOW findings." rasterize,2026-06-09,3087,MEDIUM,1,GeoDataFrame .crs dropped on no-like path (Cat 1); fixed via #3087 emitting attrs crs/crs_wkt when output has no CRS. like-path attrs/coords/dims/nodata verified live on all 4 backends (CUDA available); Cats 2-5 clean. diff --git a/xrspatial/perlin.py b/xrspatial/perlin.py index fe08cff8c..3782fa43b 100644 --- a/xrspatial/perlin.py +++ b/xrspatial/perlin.py @@ -351,6 +351,7 @@ def perlin(agg: xr.DataArray, out = mapper(agg)(agg.data, freq, seed) result = xr.DataArray(out, dims=agg.dims, + coords=agg.coords, attrs=agg.attrs, name=name) return result diff --git a/xrspatial/tests/test_perlin.py b/xrspatial/tests/test_perlin.py index 8942e6456..cd5b46f4d 100644 --- a/xrspatial/tests/test_perlin.py +++ b/xrspatial/tests/test_perlin.py @@ -108,6 +108,49 @@ def test_perlin_rejects_integer_dtype(dtype): perlin(raster) +def test_perlin_preserves_coords(): + # Regression: perlin() preserved attrs but dropped the input's x/y + # coords, yielding a DataArray that kept its `crs` attr with no + # coordinates. The output must carry the input coords through. + H, W = 10, 12 + data = np.zeros((H, W), dtype=np.float32) + raster = xr.DataArray( + data, + dims=['y', 'x'], + coords={'y': np.linspace(10.0, 20.0, H), + 'x': np.linspace(100.0, 200.0, W)}, + attrs={'crs': 'EPSG:4326', 'res': (1.0, 1.0)}, + ) + result = perlin(raster) + general_output_checks(raster, result) + assert list(result.coords) == ['y', 'x'] + np.testing.assert_array_equal(result['y'].data, raster['y'].data) + np.testing.assert_array_equal(result['x'].data, raster['x'].data) + assert result.attrs == raster.attrs + + +@dask_array_available +def test_perlin_preserves_coords_dask(): + # The result wrapping is a single shared path, but the coord-bearing + # assertion above only exercises numpy. Lock in the dask backend too. + import dask.array as da + H, W = 10, 12 + data = np.zeros((H, W), dtype=np.float32) + raster = xr.DataArray( + da.from_array(data, chunks=(5, 6)), + dims=['y', 'x'], + coords={'y': np.linspace(10.0, 20.0, H), + 'x': np.linspace(100.0, 200.0, W)}, + attrs={'crs': 'EPSG:4326', 'res': (1.0, 1.0)}, + ) + result = perlin(raster) + general_output_checks(raster, result) + assert list(result.coords) == ['y', 'x'] + np.testing.assert_array_equal(result['y'].data, raster['y'].data) + np.testing.assert_array_equal(result['x'].data, raster['x'].data) + assert result.attrs == raster.attrs + + def test_perlin_float64_input(): # float64 should still work (not just float32). data = np.zeros((20, 20), dtype=np.float64)