Fix isel() bugs: missing indexers' coords; UxDataset.isel() silently skips slicing / fails if grid dim coords exist - #1759
Conversation
|
@Sevans711 Notebooks for current main (before) and this branch (after): 1) A boolean mask with coords crashes
|
|
@dylannelson Thank you for reviewing and for pointing out these bugs! I believe I was able to fix them all. Notes below:
|
dylannelson
left a comment
There was a problem hiding this comment.
1 and 3 looks fixed and 2 may still be a bit of a concern. The error seems like it can still come up even when the time dimensions match.
I'll leave it up to you though if you want to leave this be because I'm not certain if this is some unrealistic user behavior, or if this could maybe be a separate issue. Or maybe the error could be updated to be more broad, like suggesting users to remove time if it's not needed (?)
This doesn't seem unrealistic to me; uxarray should either provide correct results or a much less confusing error message. Looking into it further, I decided to go with a much less confusing error message. Leaving it as a NotImplementedError for now, because maybe it could be implemented in the future. The Also fixed: now raise DimensionError when uxarray object has a scalar coordinate which indexer has as a 1D coordinate, instead of silently overwriting the uxarray object's coordinate. (This matches xarray's behavior.) |
There was a problem hiding this comment.
Four crashes:
1
import uxarray as ux
import numpy as np
uxds = ux.tutorial.open_dataset("quad-hexagon")
uxds = uxds.assign_coords(node_id=("n_node", np.arange(uxds.uxgrid.n_node)))
sub = uxds.isel(n_face=[0, 1])
print(list(sub.coords)) # [] (main: ['node_id'])
sub.node_id # AttributeError2
import xarray as xr
mask = xr.DataArray([True, False, True, False], dims="n_face",
coords={"lab": ("n_face", [10, 20, 30, 40])})
uxds.isel(n_face=mask) # ValueError: conflicting sizes3
hits = np.where(uxds.uxgrid.edge_lon > 1e9)[0] # empty
uxds.isel(n_edge=xr.DataArray(hits, dims="selected")) # IndexError4
This one seems to be wrong on main as well.
uxds = ux.tutorial.open_dataset("quad-hexagon").assign_coords(n_face=[0, 10, 20, 30])
picks = xr.DataArray([0, 20], dims="station", coords={"station": ["A", "B"]})
by_index = uxds.isel(n_face=xr.DataArray([0, 2], dims="station",
coords={"station": ["A", "B"]}))
print("isel keeps indexer coords:", list(by_index.coords)) # ['n_face', 'station']
by_label = uxds.sel(n_face=picks)
print("sel keeps indexer coords:", list(by_label.coords)) # ['n_face'] <-- 'station' gone
by_label.sel(station="A") # ValueError: no 'station'(also ensure sel() does a consistency check with indexer's coords, if both contain coords along grid dim.)
|
@cmdupuis3 Thank you for reviewing and finding these bugs! It looks like your examples (1) and (2) were actually already solved by the commits I pushed just a few hours before your comment. Though, it doesn't hurt to add these to the test suite, so I added them into Example (3) was definitely still a bug; I failed to account for size 0 indexers in the Example (4) was also still a bug; looking into I believe all the points are addressed now, please let me know if you find anything else! |
|
@Sevans711 1 and 4 seem to be resolved. I'm still getting crashes on variants of 2 and 3 though. import uxarray as ux
ds_lab = ux.tutorial.open_dataset("quad-hexagon").assign_coords(n_face=[0, 10, 20, 30])
ds_lab.t2m.isel(n_face=ds_lab.t2m > 297.6)
# IndexError: dimension coordinate 'n_face' conflicts between indexed and indexing objectsimport numpy as np
import xarray as xr
import uxarray as ux
ds = ux.tutorial.open_dataset("quad-hexagon")
empty = xr.DataArray(np.array([], dtype=int), dims="selected",
coords={"selected": np.array([], dtype=int)})
print(ds.isel(n_edge=empty).sizes) # no crash, but gains a stray 'selected' dim of size 0
ds.t2m.isel(n_node=empty) # CoordinateValidationError |
I believe these are fixed now! The issue with 2 is because boolean indexing changes the shape of the indexer's coords to be compared with the result, which I wasn't accounting for. Now there is a The issue with 3 was some subtle bugs occurring with 0D indexers. I checked the implementation you suggested separately and it worked well; adopted it here, now using Please continue to let me know if you find anything else! I appreciate being able to fix all of these in one place instead of needing to find and fix them over many PRs. |
cmdupuis3
left a comment
There was a problem hiding this comment.
Looks good now, no more crashes that I see here.





Closes #1712;
Closes #1713 / Closes #1714
Overview
Mapping code changes to original issues:
isel()with xr.DataArray indexer along grid dimension fails to include indexer's coordinates in result #1712 was to define_assign_grid_dim_indexer_coords_if_appropriatein coords.py, and utilize it during UxDataArray.isel() and UxDataset.isel(). Also updated isel() and sel() docstrings accordingly.UxDataset.isel()silently fails to slice when providing grid dim not in the dataset #1713 was incredibly simple, just needed to check if any grid dim is in da.dims when looping through data arrays in the dataset, inUxDataset._slice_dataset_from_grid. While touching this method though, I decided to clean it up further than this (see relevant "expansion of scope" details below).UxDataset._slice_dataset_from_grid. I fully deleted those lines instead of "trying to fix them" because I don't think they made sense to have in the first place. See point (2) below for more details.In all cases, also updated test_indexing.py accordingly. For 1713 and 1714, it was sufficient to just uncomment some existing lines which now serve as regression tests for these issues. For 1712, it also included building new tests inside
test_indexing_by_dataarray(), which led to discovering #1758.Expansion of scope: updated
UxDataset._slice_dataset_from_gridin a few different ways to increase consistency with UxDataArray indexing methods:UxDataset._slice_from_grid(to match naming conventions ofUxDataArray._slice_from_grid)UxDataset.isel()#1352), and they were not being covered by any example within the test suite (removing them does not cause any existing tests to fail).self.data_varswhengrid_dim in da.dims and not hasattr(da, "_slice_from_grid"). This branch would never be reached. (Right now, all data_vars are UxDataArrays. Eventually, maybe only data_vars with a grid dimension will be UxDataArrays. Either way, there should never be a data_var which both has a grid dim and is an xr.DataArray.)grid_dimorgrid_indexerargs; these were only being used in the now-removed parts of the code from points (2) and (3) above.UxDataArray._slice_from_gridreturns a UxDataArray, not an xr.DataArray.)Tiny expansions of scope:
Updated UxDataArray._slice_from_grid()to return an object of type(self), instead of UxDataArray. This has no extra cost, and makes uxarray slightly more object-oriented friendly. Before this change, on subclasses likeclass MyCoolArray(UxDataArray), isel() would return a UxDataArray instance if indexing by exactly 1 grid dim (e.g. isel(n_face=7)), but an instance of the subclass (MyCoolArray) if indexing by 0 grid dims (e.g. isel(time=0)) or if additionally indexing by something other than a grid dim (e.g. isel(time=0, n_face=7)). After this change it will always return an instance of the subclass (MyCoolArray) regardless of which dimensions get indexed.UxDataArray.isel()code (i.e. calling the resultresultinstead ofdaorxarr, and updating the value ofresultinside theif indexers:...block instead of returning from directly inside that block) to emphasize that the implementation is nearly identical toUxDataset.isel(). Maybe some day the full implementation, or at least a majority of it, could be moved to a shared parent class (something likeUxDataContainer?), as a way to help avoid any more unexpected behavior differences between UxDataArray.isel() and UxDataset.isel().Tiny expansions of scope unrelated to original issue:
import uxarrayfrom top of dataset.py and dataarray.py files. Pretty sure it doesn't belong there; importing a full package from within itself is not standard practice. Removing this made it necessary to make a few other tiny updates:type(self)instead ofuxarray.UxDataset, and to useUxDataArrayinstead ofuxarray.UxDataArray.type(self)instead ofuxarray.UxDataArray.from uxarray.core.dataset import UxDatasetinstead of usinguxarray.core.dataset.UxDataset.PR Checklist
General
Testing & Benchmarking
Documentation and Examples
docs/api.rst; internal (private) function names start with an underscore (_)AI Disclosure
AI Usage: GitHub Copilot's inline code suggestions, plus a small discussion with Claude for debugging/understanding syntax of super().