Skip to content

Stop surface_distance outputs adopting the dask graph token as .name (#3708) - #3716

Open
brendancol wants to merge 1 commit into
mainfrom
deep-sweep-metadata-surface_distance-2026-08-16
Open

Stop surface_distance outputs adopting the dask graph token as .name (#3708)#3716
brendancol wants to merge 1 commit into
mainfrom
deep-sweep-metadata-surface_distance-2026-08-16

Conversation

@brendancol

Copy link
Copy Markdown
Contributor

Fixes #3708.

surface_distance, surface_allocation and surface_direction built their result with xr.DataArray(result_data, coords=..., dims=..., attrs=...) and left name unset. DataArray.__init__ then falls back to getattr(data, "name", None), which on a dask array is the graph key, so the dask backends returned a hash-suffixed name while numpy and cupy returned None.

Three different tokens leaked depending on the route taken:

backend max_distance route .name before
numpy / cupy any eager None
dask+numpy finite map_overlap _trim-<hash>
dask+numpy inf iterative tile Dijkstra xrspatial.surface_distance-<hash>
dask+cupy finite map_overlap _trim-<hash>
dask+cupy inf CPU iterative, re-uploaded asarray-<hash>

The clearest user-facing symptom is .to_dataset():

numpy      .to_dataset(): ValueError: unable to convert unnamed DataArray to a Dataset without providing an explicit name
dask+numpy .to_dataset(): ['_trim-fe8156384a1e55190d2c7cd7cd7c2567']

The numpy branch asks the user for a name. The dask branch silently produces a variable named after a hash that moves when chunking or max_distance changes, so a saved dataset is not stable across runs.

Change

The three public functions now go through a shared _wrap_result() helper that resets .name to None after construction. That matches the proximity / allocation / direction trio this module mirrors (xrspatial/proximity.py:1757, :1919, :2086). Recurring bug class: slope #2837, aspect #2841, focal #2733, viewshed #2743, zonal #2611, cost_distance #3344, pathfinding #3652.

Nothing else about the returned object changes.

Tests

test_output_name_consistent_across_backends covers 4 backends x 3 functions x bounded/unbounded max_distance, so both dask routes are exercised. The 12 dask cases fail on the parent commit and pass here.

test_output_preserves_attrs_coords_dims is a regression guard for the rest of the metadata contract the audit found already correct: res / crs / transform / nodatavals, extra scalar coords, coord values, dim names, and the float32 output dtype.

Full suite: 73 passed (was 37). CUDA is available on the dev box, so the cupy and dask+cupy parametrisations ran for real rather than skipping.

Audit scope

Found by /deep-sweep --only-hydro (metadata sweep). Everything else in this module's metadata handling checked out across all four backends and both dask routes, including custom dim names via x= / y=. .name was the only divergence. The state row in .claude/sweep-metadata-state.csv records two LOW items left unfixed: attrs are copied verbatim so a user-supplied nodatavals=(-9999,) survives onto output that uses NaN as its sentinel, and a units attr carries onto surface_direction output measured in degrees. Both follow the library-wide attrs=raster.attrs convention rather than being specific to this module.

…3708)

surface_distance, surface_allocation and surface_direction built their
result with xr.DataArray(..., coords=, dims=, attrs=) and left name
unset. DataArray.__init__ then falls back to getattr(data, "name"),
which on a dask array is the graph key, so the dask backends returned
'_trim-<hash>' (bounded map_overlap route),
'xrspatial.surface_*-<hash>' (unbounded iterative route) or
'asarray-<hash>' (dask+cupy unbounded) while numpy and cupy returned
None.

The divergence is user-visible through .to_dataset(): the numpy result
raises "unable to convert unnamed DataArray", the dask result silently
creates a variable named after the hash, and the hash moves when
chunking or max_distance changes.

Route the three functions through a shared _wrap_result() helper that
resets .name to None after construction, matching the
proximity/allocation/direction trio this module mirrors. Same bug class
as cost_distance #3344 and pathfinding #3652.

Tests cover .name parity over 4 backends x 3 functions x
bounded/unbounded max_distance, plus an attrs/coords/dims/dtype
preservation guard. The 12 dask cases fail without the fix.

Also records the metadata sweep result for this module in
.claude/sweep-metadata-state.csv.
@brendancol

Copy link
Copy Markdown
Contributor Author

Verification run on the dev box (CUDA available, so cupy and dask+cupy executed rather than skipping):

  • pytest xrspatial/tests/test_surface_distance.py — 73 passed, 4.9s (37 before this branch).
  • Non-vacuous check: stashed the surface_distance.py change and re-ran the new tests — 12 failed, 24 passed. The 12 are exactly the dask+numpy and dask+cupy cases across the 3 functions and both max_distance routes. With the fix restored, all 73 pass.
  • flake8 xrspatial/surface_distance.py — clean. The one F841 flake8 reports in test_surface_distance.py:234 (sd assigned but never used in test_allocation_consistency) is pre-existing on the parent commit and untouched here.
  • Direct probe of the returned objects on all four backends confirms attrs (res, crs, transform, nodatavals, _FillValue), coord values and dtypes, the extra scalar spatial_ref coord, dim names (including custom lat/lon via x=/y=), and the float32 dtype are unchanged by this patch.

The diff is three call sites plus one helper; no behavioral change to the distance, allocation or direction values themselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

surface_distance/allocation/direction leak the dask graph token as output .name

1 participant