Fixes math bugs by creating UxSupportsArithmetic class (shared parent of UxDataset and UxDataArray) - #1767
Fixes math bugs by creating UxSupportsArithmetic class (shared parent of UxDataset and UxDataArray)#1767Sevans711 wants to merge 6 commits into
Conversation
ASV BenchmarkingBenchmark Comparison ResultsBenchmarks that have improved:
Benchmarks that have stayed the same:
|
| f"A.uxgrid != B.uxgrid during binary operation {f.__name__!r}, " | ||
| f"with type(A)={type(self).__name__}, type(B)={type(other).__name__}." | ||
| ) | ||
| return super()._binary_op(other, f, reflexive=reflexive, **kw_super) |
There was a problem hiding this comment.
xarray routes += through _inplace_binary_op, which this class doesn't override, so the grid check never runs for in-place ops. On same-shape arrays with different grids a + b raises GridsMismatchError but c += b succeeds silently. Could you pull the check into a helper, call it from both, and add a += test?
def _raise_if_grids_incompatible(self, other, f):
if isinstance(other, UxSupportsArithmetic):
if (
(self.uxgrid.n_face > 1)
and (other.uxgrid.n_face > 1)
and (self.uxgrid != other.uxgrid)
):
raise GridsMismatchError(
f"A.uxgrid != B.uxgrid during binary operation {f.__name__!r}, "
f"with type(A)={type(self).__name__}, type(B)={type(other).__name__}."
)
def _binary_op(self, other, f, reflexive=False, **kw_super):
self._raise_if_grids_incompatible(other, f)
return super()._binary_op(other, f, reflexive=reflexive, **kw_super)
def _inplace_binary_op(self, other, f):
self._raise_if_grids_incompatible(other, f)
return super()._inplace_binary_op(other, f)There was a problem hiding this comment.
Done! The test is named test_inplace_binary_ops_check_grid_compatibility.
Also slightly improved the GridsMismatchError message here to include uxgrid.sizes information for self and other.
| if isinstance(obj, xr.DataArray): | ||
| result[i] = UxDataArray(obj, uxgrid=grid_ref) | ||
| elif isinstance(obj, xr.Dataset): | ||
| result[i] = UxDataset(obj, uxgrid=grid_ref) |
There was a problem hiding this comment.
This drops source_datasets, so a ufunc on a UxDataset resets provenance to None, while dataset.py:143, :211 and :254 all forward it. obj here is a plain xr.Dataset so it has none to carry, which makes it worth deciding whether it should come from self or from whichever input supplied grid_ref.
There was a problem hiding this comment.
I would prefer to keep source_datasets support out of this PR if possible. Does that sound reasonable to you?
Reasoning: source_datasets isn't really fully supported throughout uxarray, for example even something as basic as obj.isel(n_face=slice(None)) also resets it to None. There are also no tests in the test suite which actually check whether source_datasets was properly maintained after any operations. Also, it is much easier to add a feature later than to remove support for it.
I also asked @erogluorhan a few weeks ago about source_datasets, and his impression was that it is a very low priority, we shouldn't worry about touching it or adding more support for it right now, but we could reassess upon receiving any user feedback about it.
| the returned object is a UxDataArray with same uxgrid as the input. | ||
| """ | ||
| da = super().astype(dtype, **kw_super) | ||
| return type(self)(da, uxgrid=self._uxgrid) |
There was a problem hiding this comment.
Same dropped source_datasets here, and the docstring above says "returns a UxDataArray" where it means UxDataset.
| return type(self)(da, uxgrid=self._uxgrid) | |
| return type(self)(da, uxgrid=self._uxgrid, source_datasets=self.source_datasets) |
There was a problem hiding this comment.
Good catch with the docstring - fixed!
Did not add source_datasets support yet (see reasoning above).
and fix tiny typo in docstring of UxDataset.astype(), and slightly improve the GridsMismatchError when the grid compatibility check fails.
Closes #1685, Closes #1695, Closes #1718
Bonus: closes #1737
Overview
This PR introduces the UxSupportsArithmetic class as a shared parent class for both UxDataset and UxDataArray. This class partially serves a similar purpose as xarray's SupportsArithmetic class (defining
__array_ufunc__) but also serves a broader purpose too (override_binary_op()to also compare grids, and override math dunder methods like__add__). This is the majority of the solution for #1685, #1695, and #1718.It also solves #1737 (because it was simple enough to fix and I wanted to use
.astype()in the test suite here).Mentioning also one thing I was surprised by: grids with n_face==1 needed to be handled specially in this PR to avoid considering them for grid comparisons, because doing so would break simple use-cases like
arr.isel(n_face=0) > arr.isel(n_face=7).Bonus details:
Mapping changes to solved issues:
__array_ufunc__.__add__, as well as overriding_binary_op()UxDataArray to check one particular case. It isn't possible to fully solve for cases combining DataArrays and Datasets, at least not without tinkering with xarray directly, as noted in the test suite._binary_op(), and by comparing uxgrids of inputs during__array_ufunc__, whenever multiple inputs have a uxgrid.This PR provides more comprehensive solutions than what the issue examples cover, but it otherwise just solves the original issues (no expansions of scope). In particular:
I doubt this PR would affect performance, but I added the run-benchmark label just in case. (Benchmarks look fine --> removing the label)
PR Checklist
General
Testing & Benchmarking
Documentation and Examples
docs/api.rst; internal (private) function names start with an underscore (_)AI Disclosure
AI Usage: Lots of chatting with Claude to settle on the overall design and to speed up debugging. Also, used GitHub Copilot's inline code suggestions.