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 4 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)| 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.
| 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) |
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.