Preserve input attrs in mcda.constrain() (#3147)#3154
Merged
Conversation
xr.where takes attrs from its first value argument (the scalar fill), so any non-empty exclude list stripped res/crs/nodatavals from the constrained suitability surface. Restore the input's attrs on the output and add regression tests for numpy, dask+numpy, and dask+cupy. Also record the mcda metadata sweep result in the sweep state CSV.
brendancol
commented
Jun 10, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
PR Review: Preserve input attrs in mcda.constrain() (#3147)
Blockers (must fix before merge)
None.
Suggestions (should fix, not blocking)
None.
Nits (optional improvements)
-
xrspatial/mcda/constrain.py:30-33: the Returns section could state that the output keeps the input's attrs. The fix establishes that contract; documenting it makes the next regression visible in review rather than in a sweep.
What looks good
- The root cause is identified precisely:
xr.wheretakes attrs from its first value argument, the scalarfill, so any non-emptyexcludestripped attrs. Restoringdict(suitability.attrs)after the loop fixes every mask count and fill value in one place, and the dict copy keeps the output from aliasing the input's attrs (covered bytest_attrs_not_shared_with_input). - Test coverage matches the bug shape: mask present, custom fill, empty exclude list, dask+numpy with value parity against numpy, and dask+cupy asserting attrs on the lazy result. The comment in
test_attrs_kept_dask_cupyexplaining why values are not computed there (pre-existing cupy/xarrayxr.whereincompatibility) is accurate; I reproduced that failure on main without this change. - Hand-rolled fixtures match the existing test_mcda.py style, which does not use general_checks helpers anywhere.
Checklist
- Algorithm matches reference/paper (N/A, metadata-only fix)
- All implemented backends produce consistent results (numpy, dask+numpy verified by value; dask+cupy attrs verified lazily; bare cupy blocked upstream before this code runs)
- NaN handling is correct (unchanged; NaN fill still propagates)
- Edge cases are covered by tests (empty exclude, custom fill, attrs aliasing)
- Dask chunk boundaries handled correctly (no chunk-level code touched)
- No premature materialization or unnecessary copies
- Benchmark exists or is not needed (not needed, no compute change)
- README feature matrix updated (not applicable)
- Docstrings present and accurate (see nit: attrs contract not documented)
The sweep state CSV row riding along is expected for metadata-sweep PRs.
brendancol
commented
Jun 10, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
Follow-up review after 9f-commit addressing the docstring nit:
- The Returns section of
constrainnow states the output keeps the input's dims, coords, and attrs. That closes the only finding from the first pass. - No new code paths were touched; constrain tests still pass (13 passed locally).
Nothing outstanding.
brendancol
added a commit
that referenced
this pull request
Jun 10, 2026
…e-mcda-2026-06-10-01 Conflicts: - .claude/sweep-performance-state.csv: took main's newer geotiff/rasterize/ reproject rows (LF endings) and re-inserted this branch's mcda row. Semantic fixups in xrspatial/tests/test_mcda.py (auto-merged textually): - Removed strict xfail markers that now XPASS: the three owa backend tests fixed by this PR, plus standardize cupy piecewise, standardize dask+cupy categorical (#3159) and constrain attrs (#3154), whose fixes merged before the #3156 xfails landed.
brendancol
added a commit
that referenced
this pull request
Jun 10, 2026
…cda-2026-06-10 Conflicts: .claude/sweep-accuracy-state.csv, xrspatial/mcda/sensitivity.py, xrspatial/mcda/standardize.py, xrspatial/tests/test_mcda.py. sensitivity.py taken from main (#3160 chunk-bounded monte_carlo already handles cupy). standardize.py taken from main (#3159) plus this PR's ascontiguousarray for cupy.interp, which #3159 lacked. test_mcda.py keeps main's #3156 coverage with the strict xfail markers removed for the paths this PR fixes (owa cupy/dask, standardize piecewise/categorical GPU, monte-carlo sensitivity GPU) and for constrain attrs (fixed by #3154); the de-xfail'd monte_carlo assertions are now NaN-aware. Sweep CSV is main's rows plus this PR's mcda row.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3147
constrain()lost all input attrs (res,crs,nodatavals) wheneverexcludewas non-empty:xr.where(mask, fill, result)takes attrs from the scalarfill. Withexclude=[]attrs survived, so behavior also depended on whether any mask was passed.suitability.attrs(as a copy) on the output after the masking loop.TestConstrainAttrscovers attrs survival with a mask, with a custom fill, with an empty exclude list, attrs-dict isolation from the input, and the dask+numpy and dask+cupy paths.Backend coverage: numpy, dask+numpy, and dask+cupy verified end-to-end. On bare cupy,
xr.whereitself raises before the fix is reached (known cupy 13.6 / xarray 2025.12 incompatibility, tracked separately); the dask+cupy test asserts attrs on the lazy result for the same reason.Test plan:
pytest xrspatial/tests/test_mcda.py(181 passed, includes 6 new attrs tests)Found by the metadata propagation sweep. The sweep state CSV update for the mcda module rides along in this PR.