Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/sweep-metadata-state.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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-<hash>, _kdtree_chunk_fn-<hash>, asarray-<hash>) 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.
Expand Down
1 change: 1 addition & 0 deletions xrspatial/perlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 43 additions & 0 deletions xrspatial/tests/test_perlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading