From ab732d0196691ee0fb6e87468b6b776634a5e801 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Wed, 15 Apr 2020 10:28:02 -0600 Subject: [PATCH 01/20] ENH: Adding __array_ufunc__ capability to MaskedArrays. This enables any ufunc numpy operations that are called on a MaskedArray to use the masked version of that function automatically without needing to resort to np.ma.func() calls. --- numpy/lib/tests/test_function_base.py | 2 +- numpy/ma/core.py | 218 +++++++++++++++++++++----- numpy/ma/extras.py | 17 +- numpy/ma/tests/test_core.py | 4 +- 4 files changed, 194 insertions(+), 47 deletions(-) diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py index ecba35f2d39f..f7572535a0ca 100644 --- a/numpy/lib/tests/test_function_base.py +++ b/numpy/lib/tests/test_function_base.py @@ -786,7 +786,7 @@ def test_subclass(self): mask=[[False, False], [True, False], [False, True], [True, True], [False, False]]) out = diff(x) - assert_array_equal(out.data, [[1], [1], [1], [1], [1]]) + assert_array_equal(out.data, [[1], [4], [6], [8], [1]]) assert_array_equal(out.mask, [[False], [True], [True], [True], [False]]) assert_(type(out) is type(x)) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 90f852e5c66d..9fe56047b10b 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -22,6 +22,7 @@ # pylint: disable-msg=E1002 import builtins import inspect +from numbers import Number import operator import warnings import textwrap @@ -58,10 +59,10 @@ 'frombuffer', 'fromflex', 'fromfunction', 'getdata', 'getmask', 'getmaskarray', 'greater', 'greater_equal', 'harden_mask', 'hypot', 'identity', 'ids', 'indices', 'inner', 'innerproduct', 'isMA', - 'isMaskedArray', 'is_mask', 'is_masked', 'isarray', 'left_shift', - 'less', 'less_equal', 'log', 'log10', 'log2', - 'logical_and', 'logical_not', 'logical_or', 'logical_xor', 'make_mask', - 'make_mask_descr', 'make_mask_none', 'mask_or', 'masked', + 'isMaskedArray', 'is_mask', 'is_masked', 'isarray', 'isfinite', + 'isinf', 'isnan', 'left_shift', 'less', 'less_equal', 'log', 'log10', + 'log2', 'logical_and', 'logical_not', 'logical_or', 'logical_xor', + 'make_mask', 'make_mask_descr', 'make_mask_none', 'mask_or', 'masked', 'masked_array', 'masked_equal', 'masked_greater', 'masked_greater_equal', 'masked_inside', 'masked_invalid', 'masked_less', 'masked_less_equal', 'masked_not_equal', @@ -931,6 +932,12 @@ def __call__(self, a, *args, **kwargs): """ d = getdata(a) + if 'out' in kwargs: + # Need to drop the mask from the output array when being called + kwargs['out'] = getdata(kwargs['out']) + args = [getdata(arg) if isinstance(arg, MaskedArray) else arg + for arg in args] + # Deal with domain if self.domain is not None: # Case 1.1. : Domained function @@ -1054,7 +1061,7 @@ def __call__(self, a, b, *args, **kwargs): masked_result._update_from(b) return masked_result - def reduce(self, target, axis=0, dtype=None): + def reduce(self, target, axis=0, dtype=None, **kwargs): """ Reduce `target` along the given `axis`. @@ -1189,6 +1196,10 @@ def __call__(self, a, b, *args, **kwargs): # Transforms to a (subclass of) MaskedArray masked_result = result.view(get_masked_subclass(a, b)) + # If the original masks were scalar or nomask, don't expand the result + # which comes from the isfinite initialization above + if getmask(a).shape + getmask(b).shape == (): + m = _shrink_mask(m) masked_result._mask = m if isinstance(a, MaskedArray): masked_result._update_from(a) @@ -1215,6 +1226,9 @@ def __call__(self, a, b, *args, **kwargs): ceil = _MaskedUnaryOperation(umath.ceil) around = _MaskedUnaryOperation(np.round_) logical_not = _MaskedUnaryOperation(umath.logical_not) +isinf = _MaskedUnaryOperation(umath.isinf) +isnan = _MaskedUnaryOperation(umath.isnan) +isfinite = _MaskedUnaryOperation(umath.isfinite) # Domained unary ufuncs sqrt = _MaskedUnaryOperation(umath.sqrt, 0.0, @@ -3080,7 +3094,7 @@ def __array_wrap__(self, obj, context=None): func, args, out_i = context # args sometimes contains outputs (gh-10459), which we don't want input_args = args[:func.nin] - m = reduce(mask_or, [getmaskarray(arg) for arg in input_args]) + m = reduce(mask_or, [getmask(arg) for arg in input_args]) # Get the domain mask domain = ufunc_domain.get(func, None) if domain is not None: @@ -3108,7 +3122,6 @@ def __array_wrap__(self, obj, context=None): else: # Don't modify inplace, we risk back-propagation m = (m | d) - # Make sure the mask has the proper size if result is not self and result.shape == () and m: return masked @@ -3118,6 +3131,85 @@ def __array_wrap__(self, obj, context=None): return result + def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): + """ + MaskedArray capability for ufuncs + + Handle masked versions of ufuncs if they are implemented within + the MaskedArray module. If the masked ufunc is not implemented, + this falls back to the standard numpy ndarray ufunc, which we + then call with the ndarray view of the input data. + + """ + # Output can be specified as arguments or as keyword arguments + outputs = kwargs.pop('out', ()) + if not isinstance(outputs, tuple): + outputs = (outputs,) + outputs += inputs[np_ufunc.nin:] + args = inputs[:np_ufunc.nin] + + # Determine what class types we are compatible with and return + # NotImplemented if we don't know how to handle them + for arg in args + outputs: + if not isinstance(arg, (ndarray, np.bool_, Number, list, str)): + return NotImplemented + + # Get the equivalent masked version of the numpy function + # if it is in the module level functions + ma_ufunc = np.ma.__dict__.get(np_ufunc.__name__, np_ufunc) + if ma_ufunc is np_ufunc: + # We didn't have a Masked version of the ufunc, so we need to + # call the ndarray version with the data from the objects and + # prevent infinite recursion. + + # Make ndarray views of the input arguments + args = [getdata(input) if isinstance(input, MaskedArray) + else input for input in args] + else: + # The masked power function doesn't support extra args + if np_ufunc.__name__ in ('power'): + kwargs = {} + + results = getattr(ma_ufunc, method)(*args, **kwargs) + if results is NotImplemented: + return NotImplemented + if method == 'at': + return + if np_ufunc.nout == 1: + results = (results,) + if outputs == (): + outputs = (None,) * np_ufunc.nout + + returns = [] + for i, result in enumerate(results): + output = outputs[i] + + # Reapply the mask + if isinstance(result, ndarray) and result is not masked: + # Need to copy over all of the data and mask from results + # to the original object requested with out + if output is not None: + if isinstance(output, MaskedArray): + output._update_from(result) + if isinstance(result, MaskedArray): + output.data[:] = result._data + output._mask = result._mask + else: + output.data[:] = result + else: + output[:] = result + + result = output + + elif output is not None: + # An out value was requested, but the result is a scalar + output[()] = result + result = output + + returns.append(result) + + return returns[0] if np_ufunc.nout == 1 else returns + def view(self, dtype=None, type=None, fill_value=None): """ Return a view of the MaskedArray data. @@ -3291,7 +3383,7 @@ def _scalar_heuristic(arr, elem): return dout else: # Force dout to MA - dout = dout.view(type(self)) + dout = MaskedArray(dout) # Inherit attributes from self dout._update_from(self) # Check the fill_value @@ -3853,6 +3945,23 @@ def filled(self, fill_value=None): result = self._data return result + def clip(self, a_min, a_max, out=None, **kwargs): + """docstring inherited + np.clip.__doc__ + + TODO: Should we ignore the clip where the data is masked? + It is currently in line with the old numpy version + """ + result = self.data.clip(a_min, a_max, **kwargs).view(MaskedArray) + if out is not None: + # Just copy the data and mask + out.data[:] = getdata(result) + out._mask = self._mask + return out + result._update_from(self) + result._mask = self._mask + return result + def compressed(self): """ Return all the non-masked data as a 1-D array. @@ -3946,10 +4055,15 @@ def compress(self, condition, axis=None, out=None): # values. condition = np.asarray(condition) - _new = _data.compress(condition, axis=axis, out=out).view(type(self)) + _new = _data.compress(condition, axis=axis).view(type(self)) _new._update_from(self) if _mask is not nomask: _new._mask = _mask.compress(condition, axis=axis) + if out is not None: + out._update_from(self) + out.data[:] = _new.data + out._mask = _new.mask + return out return _new def _insert_masked_print(self): @@ -4199,7 +4313,7 @@ def __add__(self, other): """ if self._delegate_binop(other): return NotImplemented - return add(self, other) + return np.add(self, other) def __radd__(self, other): """ @@ -4208,7 +4322,7 @@ def __radd__(self, other): """ # In analogy with __rsub__ and __rdiv__, use original order: # we get here from `other + self`. - return add(other, self) + return np.add(other, self) def __sub__(self, other): """ @@ -4217,20 +4331,20 @@ def __sub__(self, other): """ if self._delegate_binop(other): return NotImplemented - return subtract(self, other) + return np.subtract(self, other) def __rsub__(self, other): """ Subtract self from other, and return a new masked array. """ - return subtract(other, self) + return np.subtract(other, self) def __mul__(self, other): "Multiply self by other, and return a new masked array." if self._delegate_binop(other): return NotImplemented - return multiply(self, other) + return np.multiply(self, other) def __rmul__(self, other): """ @@ -4239,7 +4353,7 @@ def __rmul__(self, other): """ # In analogy with __rsub__ and __rdiv__, use original order: # we get here from `other * self`. - return multiply(other, self) + return np.multiply(other, self) def __div__(self, other): """ @@ -4248,7 +4362,7 @@ def __div__(self, other): """ if self._delegate_binop(other): return NotImplemented - return divide(self, other) + return np.divide(self, other) def __truediv__(self, other): """ @@ -4257,14 +4371,14 @@ def __truediv__(self, other): """ if self._delegate_binop(other): return NotImplemented - return true_divide(self, other) + return np.true_divide(self, other) def __rtruediv__(self, other): """ Divide self into other, and return a new masked array. """ - return true_divide(other, self) + return np.true_divide(other, self) def __floordiv__(self, other): """ @@ -4273,14 +4387,14 @@ def __floordiv__(self, other): """ if self._delegate_binop(other): return NotImplemented - return floor_divide(self, other) + return np.floor_divide(self, other) def __rfloordiv__(self, other): """ Divide self into other, and return a new masked array. """ - return floor_divide(other, self) + return np.floor_divide(other, self) def __pow__(self, other): """ @@ -5057,8 +5171,8 @@ def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): #!!!: implement out + test! m = self._mask if m is nomask: - result = super().trace(offset=offset, axis1=axis1, axis2=axis2, - out=out) + result = self.view(np.ndarray).trace(offset=offset, axis1=axis1, + axis2=axis2, out=out) return result.astype(dtype) else: D = self.diagonal(offset=offset, axis1=axis1, axis2=axis2) @@ -5158,7 +5272,9 @@ def sum(self, axis=None, dtype=None, out=None, keepdims=np._NoValue): result = masked return result # Explicit output - result = self.filled(0).sum(axis, dtype=dtype, out=out, **kwargs) + + self.filled(0).sum(axis, dtype=dtype, out=out.view(np.ndarray), + **kwargs) if isinstance(out, MaskedArray): outmask = getmask(out) if outmask is nomask: @@ -5308,7 +5424,10 @@ def mean(self, axis=None, dtype=None, out=None, keepdims=np._NoValue): """ kwargs = {} if keepdims is np._NoValue else {'keepdims': keepdims} if self._mask is nomask: - result = super().mean(axis=axis, dtype=dtype, **kwargs)[()] + result = self.view(np.ndarray).mean(axis=axis, + dtype=dtype, **kwargs) + if isinstance(result, np.ndarray): + result = MaskedArray(result, mask=False) else: is_float16_result = False if dtype is None: @@ -5391,9 +5510,12 @@ def var(self, axis=None, dtype=None, out=None, ddof=0, # Easy case: nomask, business as usual if self._mask is nomask: - ret = super().var(axis=axis, dtype=dtype, out=out, ddof=ddof, - **kwargs)[()] + ret = self.view(np.ndarray).var(axis=axis, dtype=dtype, + ddof=ddof, **kwargs) + if isinstance(ret, np.ndarray): + ret = MaskedArray(ret, mask=False) if out is not None: + out.flat = ret if isinstance(out, MaskedArray): out.__setmask__(nomask) return out @@ -5451,12 +5573,10 @@ def std(self, axis=None, dtype=None, out=None, ddof=0, numpy.std : Equivalent function """ kwargs = {} if keepdims is np._NoValue else {'keepdims': keepdims} - dvar = self.var(axis, dtype, out, ddof, **kwargs) if dvar is not masked: if out is not None: - np.power(out, 0.5, out=out, casting='unsafe') - return out + return np.power(out, 0.5, out=out, casting='unsafe') dvar = sqrt(dvar) return dvar @@ -5471,6 +5591,10 @@ def round(self, decimals=0, out=None): numpy.ndarray.round : corresponding function for ndarrays numpy.around : equivalent function """ + stored_out = None + if isinstance(out, MaskedArray): + stored_out = out + out = getdata(out) result = self._data.round(decimals=decimals, out=out).view(type(self)) if result.ndim > 0: result._mask = self._mask @@ -5481,7 +5605,9 @@ def round(self, decimals=0, out=None): # No explicit output: we're done if out is None: return result - if isinstance(out, MaskedArray): + if stored_out is not None: + # We got in a masked array originally, so we need to return one + out = stored_out out.__setmask__(self._mask) return out @@ -6032,13 +6158,13 @@ def partition(self, *args, **kwargs): warnings.warn("Warning: 'partition' will ignore the 'mask' " f"of the {self.__class__.__name__}.", stacklevel=2) - return super().partition(*args, **kwargs) + return self.view(np.ndarray).partition(*args, **kwargs) def argpartition(self, *args, **kwargs): warnings.warn("Warning: 'argpartition' will ignore the 'mask' " f"of the {self.__class__.__name__}.", stacklevel=2) - return super().argpartition(*args, **kwargs) + return self.view(np.ndarray).argpartition(*args, **kwargs) def take(self, indices, axis=None, out=None, mode='raise'): """ @@ -6722,7 +6848,7 @@ def __call__(self, a, b): return where(self.compare(a, b), a, b) - def reduce(self, target, axis=np._NoValue): + def reduce(self, target, axis=np._NoValue, **kwargs): "Reduce target along the given axis." target = narray(target, copy=False, subok=True) m = getmask(target) @@ -6737,12 +6863,10 @@ def reduce(self, target, axis=np._NoValue): axis = None if axis is not np._NoValue: - kwargs = dict(axis=axis) - else: - kwargs = dict() + kwargs['axis'] = axis if m is nomask: - t = self.f.reduce(target, **kwargs) + t = self.f.reduce(target.view(np.ndarray), **kwargs) else: target = target.filled( self.fill_value_func(target)).view(type(target)) @@ -7994,6 +8118,23 @@ def allclose(a, b, masked_equal=True, rtol=1e-5, atol=1e-8): """ x = masked_array(a, copy=False) y = masked_array(b, copy=False) + if masked_equal: + # Apply the combined mask right away to avoid comparisons at the + # masked locations (assumed mask is True) + m = mask_or(getmask(x), getmask(y)) + # Expand scalars to the proper dimension for comparison if needed + if shape(x) != shape(y): + if size(x) == 1: + # scalar a + x = masked_array(np.ones(shape=shape(y))*x, mask=m) + elif size(y) == 1: + # scalar b + y = masked_array(np.ones(shape=shape(x))*y, mask=m) + else: + raise ValueError("Cannot compare arrays of different shapes.") + else: + x = masked_array(a, copy=False, mask=m) + y = masked_array(b, copy=False, mask=m) # make sure y is an inexact type to avoid abs(MIN_INT); will cause # casting of x later. @@ -8006,8 +8147,7 @@ def allclose(a, b, masked_equal=True, rtol=1e-5, atol=1e-8): if y.dtype != dtype: y = masked_array(y, dtype=dtype, copy=False) - m = mask_or(getmask(x), getmask(y)) - xinf = np.isinf(masked_array(x, copy=False, mask=m)).filled(False) + xinf = filled(np.isinf(x), False) # If we have some infs, they should fall at the same place. if not np.all(xinf == filled(np.isinf(y), False)): return False diff --git a/numpy/ma/extras.py b/numpy/ma/extras.py index 41bce0f22328..23980b0c6828 100644 --- a/numpy/ma/extras.py +++ b/numpy/ma/extras.py @@ -588,8 +588,8 @@ def average(a, axis=None, weights=None, returned=False, *, >>> avg, sumweights = np.ma.average(x, axis=0, weights=[1, 2, 3], ... returned=True) >>> avg - masked_array(data=[2.6666666666666665, 3.6666666666666665], - mask=[False, False], + masked_array(data=[2.66666667, 3.66666667], + mask=False, fill_value=1e+20) With ``keepdims=True``, the following result has shape (3, 1). @@ -2038,8 +2038,15 @@ def polyfit(x, y, deg, rcond=None, full=False, w=None, cov=False): not_m = ~m if w is not None: w = w[not_m] - return np.polyfit(x[not_m], y[not_m], deg, rcond, full, w, cov) - else: - return np.polyfit(x, y, deg, rcond, full, w, cov) + x = x[not_m] + y = y[not_m] + + # Only pass the ndarray data + if w is not None: + w = w.view(np.ndarray) + x = x.view(np.ndarray) + y = y.view(np.ndarray) + + return np.polyfit(x, y, deg, rcond, full, w, cov) polyfit.__doc__ = ma.doc_note(np.polyfit.__doc__, polyfit.__doc__) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index bb7869e7ab26..0624628127f7 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -3211,7 +3211,7 @@ def test_compress(self): assert_equal(b.fill_value, 9999) assert_equal(b, a[condition]) - condition = (a < 4.) + condition = (a.data < 4.) b = a.compress(condition) assert_equal(b._data, [1., 2., 3.]) assert_equal(b._mask, [0, 0, 1]) @@ -5410,7 +5410,7 @@ def test_ufunc_with_out_varied(): a = array([ 1, 2, 3], mask=[1, 0, 0]) b = array([10, 20, 30], mask=[1, 0, 0]) out = array([ 0, 0, 0], mask=[0, 0, 1]) - expected = array([11, 22, 33], mask=[1, 0, 0]) + expected = array([1, 22, 33], mask=[1, 0, 0]) out_pos = out.copy() res_pos = np.add(a, b, out_pos) From 63df46060e87ef188d404ee5e8c8f9de31832055 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Tue, 13 Apr 2021 20:06:14 -0600 Subject: [PATCH 02/20] DOC: Adding improvement note for MaskedArray ufunc --- doc/release/upcoming_changes/16022.improvement.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/release/upcoming_changes/16022.improvement.rst diff --git a/doc/release/upcoming_changes/16022.improvement.rst b/doc/release/upcoming_changes/16022.improvement.rst new file mode 100644 index 000000000000..0616163b9937 --- /dev/null +++ b/doc/release/upcoming_changes/16022.improvement.rst @@ -0,0 +1,9 @@ +MaskedArray gains a ``__array_ufunc__`` method to better handle ufuncs +---------------------------------------------------------------------- +The MaskedArray class now has an implementation of `__array_ufunc__` that +handles deferrals to the desired implementations of the ufunc. If a masked +implementation of a ufunc exists, that implementation will take priority. +This means that code called with MaskedArray ma as ``np.ufunc(ma)`` will +behave the same as ``np.ma.ufunc(ma)``. Additionally, adding this helps with +dispatching to subclasses and preserving the proper types when another +implementation should take priority. From a63e97ae753ae75148ea25081590caada0caf462 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 13 Jul 2022 15:55:14 +0100 Subject: [PATCH 03/20] nomask in nomask out --- numpy/ma/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 9fe56047b10b..63d7b6d41eed 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -5427,7 +5427,7 @@ def mean(self, axis=None, dtype=None, out=None, keepdims=np._NoValue): result = self.view(np.ndarray).mean(axis=axis, dtype=dtype, **kwargs) if isinstance(result, np.ndarray): - result = MaskedArray(result, mask=False) + result = MaskedArray(result, mask=nomask) else: is_float16_result = False if dtype is None: @@ -5513,7 +5513,7 @@ def var(self, axis=None, dtype=None, out=None, ddof=0, ret = self.view(np.ndarray).var(axis=axis, dtype=dtype, ddof=ddof, **kwargs) if isinstance(ret, np.ndarray): - ret = MaskedArray(ret, mask=False) + ret = MaskedArray(ret, mask=nomask) if out is not None: out.flat = ret if isinstance(out, MaskedArray): From eca1e3c26d2e6a7e50b075005855c0a9a1bb49a9 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Sat, 16 Jul 2022 15:04:53 +0100 Subject: [PATCH 04/20] BUG: fix ma.minimum.reduce with axis keyword Fixes the problem reported at https://github.com/numpy/numpy/pull/21977#issuecomment-1186082534 The reduce method here effectively calls itself with an unmasked MaskedArray (mask=nomask) and then expects either a MaskedArray or a scalar. This change ensures that an ordinary ndarray is converted to a MaskedArray, following the pattern already used in mean and var in this module. --- numpy/ma/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 63d7b6d41eed..de514cd52cab 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -6867,6 +6867,8 @@ def reduce(self, target, axis=np._NoValue, **kwargs): if m is nomask: t = self.f.reduce(target.view(np.ndarray), **kwargs) + if isinstance(t, ndarray): + t = MaskedArray(t, mask=nomask) else: target = target.filled( self.fill_value_func(target)).view(type(target)) From 997d27db0f8b665a9528cfa67902300a2d1d9869 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Sat, 16 Jul 2022 14:40:27 +0100 Subject: [PATCH 05/20] TST: add a test for ma.minimum.reduce with axis keyword Adapted from the problem reported at https://github.com/numpy/numpy/pull/21977#issuecomment-1186082534 --- numpy/ma/tests/test_core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index 0624628127f7..9d054e133be1 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1238,6 +1238,18 @@ def test_minmax_reduce(self): b = np.maximum.reduce(a) assert_equal(b, 3) + def test_minmax_reduce_axis(self): + # Test np.min/maximum.reduce along an axis for 2D array + import numpy as np + data = [[0, 1, 2, 3, 4, 9], [5, 5, 0, 9, 3, 3]] + mask = [[0, 0, 0, 0, 0, 1], [0, 0, 1, 1, 0, 0]] + a = array(data, mask=mask) + + expected = array([0, 3], mask=False) + result = np.minimum.reduce(a, axis=1) + + assert_array_equal(result, expected) + def test_minmax_funcs_with_output(self): # Tests the min/max functions with explicit outputs mask = np.random.rand(12).round() From bd091a61f531301657e7365f01597e6313b650a6 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 16:34:52 -0600 Subject: [PATCH 06/20] MNT: Remove __add__ and other unnecessary overrides --- numpy/ma/core.py | 90 ------------------------------------------------ 1 file changed, 90 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index de514cd52cab..2906a69bb172 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -4306,96 +4306,6 @@ def __ge__(self, other): def __gt__(self, other): return self._comparison(other, operator.gt) - def __add__(self, other): - """ - Add self to other, and return a new masked array. - - """ - if self._delegate_binop(other): - return NotImplemented - return np.add(self, other) - - def __radd__(self, other): - """ - Add other to self, and return a new masked array. - - """ - # In analogy with __rsub__ and __rdiv__, use original order: - # we get here from `other + self`. - return np.add(other, self) - - def __sub__(self, other): - """ - Subtract other from self, and return a new masked array. - - """ - if self._delegate_binop(other): - return NotImplemented - return np.subtract(self, other) - - def __rsub__(self, other): - """ - Subtract self from other, and return a new masked array. - - """ - return np.subtract(other, self) - - def __mul__(self, other): - "Multiply self by other, and return a new masked array." - if self._delegate_binop(other): - return NotImplemented - return np.multiply(self, other) - - def __rmul__(self, other): - """ - Multiply other by self, and return a new masked array. - - """ - # In analogy with __rsub__ and __rdiv__, use original order: - # we get here from `other * self`. - return np.multiply(other, self) - - def __div__(self, other): - """ - Divide other into self, and return a new masked array. - - """ - if self._delegate_binop(other): - return NotImplemented - return np.divide(self, other) - - def __truediv__(self, other): - """ - Divide other into self, and return a new masked array. - - """ - if self._delegate_binop(other): - return NotImplemented - return np.true_divide(self, other) - - def __rtruediv__(self, other): - """ - Divide self into other, and return a new masked array. - - """ - return np.true_divide(other, self) - - def __floordiv__(self, other): - """ - Divide other into self, and return a new masked array. - - """ - if self._delegate_binop(other): - return NotImplemented - return np.floor_divide(self, other) - - def __rfloordiv__(self, other): - """ - Divide self into other, and return a new masked array. - - """ - return np.floor_divide(other, self) - def __pow__(self, other): """ Raise self to the power other, masking the potential NaNs/Infs From b5b9ad34c8505efe0f15be53521d28ead319b3ef Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 17:25:50 -0600 Subject: [PATCH 07/20] ENH: Add sign ufunc to masked arrays --- numpy/ma/core.py | 3 ++- numpy/ma/tests/test_core.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 2906a69bb172..2f18d6e225f9 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -73,7 +73,7 @@ 'not_equal', 'ones', 'ones_like', 'outer', 'outerproduct', 'power', 'prod', 'product', 'ptp', 'put', 'putmask', 'ravel', 'remainder', 'repeat', 'reshape', 'resize', 'right_shift', 'round', 'round_', - 'set_fill_value', 'shape', 'sin', 'sinh', 'size', 'soften_mask', + 'set_fill_value', 'shape', 'sin', 'sign', 'sinh', 'size', 'soften_mask', 'sometrue', 'sort', 'sqrt', 'squeeze', 'std', 'subtract', 'sum', 'swapaxes', 'take', 'tan', 'tanh', 'trace', 'transpose', 'true_divide', 'var', 'where', 'zeros', 'zeros_like', @@ -1229,6 +1229,7 @@ def __call__(self, a, b, *args, **kwargs): isinf = _MaskedUnaryOperation(umath.isinf) isnan = _MaskedUnaryOperation(umath.isnan) isfinite = _MaskedUnaryOperation(umath.isfinite) +sign = _MaskedUnaryOperation(umath.sign) # Domained unary ufuncs sqrt = _MaskedUnaryOperation(umath.sqrt, 0.0, diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py index 9d054e133be1..bd2d260e0860 100644 --- a/numpy/ma/tests/test_core.py +++ b/numpy/ma/tests/test_core.py @@ -1458,6 +1458,13 @@ def test_noshink_on_creation(self): a = np.ma.masked_values([1., 2.5, 3.1], 1.5, shrink=False) assert_equal(a.mask, [0, 0, 0]) + def test_sign(self): + x = np.ma.MaskedArray([1, 2], mask=[True, False]) + expected = np.ma.MaskedArray([1, 1], mask=[True, False]) + y = np.sign(x) + assert_equal(y, expected) + assert isinstance(y, np.ma.MaskedArray) + def test_mod(self): # Tests mod (x, y, a10, m1, m2, xm, ym, z, zm, xf) = self.d From d0ac064248422b1e45e6f751a2c1b7fa8ed33c46 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 17:37:14 -0600 Subject: [PATCH 08/20] ENH: Remove masked ufunc power restriction Now we are calling np.power() in std() which goes through the ufunc machinery, so we don't want to pass any additional unsafe casting kwargs that aren't allowed within the masked implementation. --- numpy/ma/core.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 2f18d6e225f9..43145d7420fe 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3160,16 +3160,8 @@ def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): ma_ufunc = np.ma.__dict__.get(np_ufunc.__name__, np_ufunc) if ma_ufunc is np_ufunc: # We didn't have a Masked version of the ufunc, so we need to - # call the ndarray version with the data from the objects and - # prevent infinite recursion. - - # Make ndarray views of the input arguments - args = [getdata(input) if isinstance(input, MaskedArray) - else input for input in args] - else: - # The masked power function doesn't support extra args - if np_ufunc.__name__ in ('power'): - kwargs = {} + # call the ndarray version to prevent infinite recursion. + return super().__array_ufunc__(np_ufunc, method, *inputs, **kwargs) results = getattr(ma_ufunc, method)(*args, **kwargs) if results is NotImplemented: @@ -5487,7 +5479,7 @@ def std(self, axis=None, dtype=None, out=None, ddof=0, dvar = self.var(axis, dtype, out, ddof, **kwargs) if dvar is not masked: if out is not None: - return np.power(out, 0.5, out=out, casting='unsafe') + return np.power(out, 0.5, out=out) dvar = sqrt(dvar) return dvar From dd7462022e97778fbc9dd5c73a6358e33342501a Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 17:42:59 -0600 Subject: [PATCH 09/20] ENH: Add masked invert ufunc --- numpy/ma/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 43145d7420fe..e44185353c99 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -58,7 +58,7 @@ 'flatten_structured_array', 'floor', 'floor_divide', 'fmod', 'frombuffer', 'fromflex', 'fromfunction', 'getdata', 'getmask', 'getmaskarray', 'greater', 'greater_equal', 'harden_mask', 'hypot', - 'identity', 'ids', 'indices', 'inner', 'innerproduct', 'isMA', + 'identity', 'ids', 'indices', 'inner', 'innerproduct', 'invert', 'isMA', 'isMaskedArray', 'is_mask', 'is_masked', 'isarray', 'isfinite', 'isinf', 'isnan', 'left_shift', 'less', 'less_equal', 'log', 'log10', 'log2', 'logical_and', 'logical_not', 'logical_or', 'logical_xor', @@ -1229,6 +1229,7 @@ def __call__(self, a, b, *args, **kwargs): isinf = _MaskedUnaryOperation(umath.isinf) isnan = _MaskedUnaryOperation(umath.isnan) isfinite = _MaskedUnaryOperation(umath.isfinite) +invert = _MaskedUnaryOperation(np.invert) sign = _MaskedUnaryOperation(umath.sign) # Domained unary ufuncs From c638cdc005bef2f674ca68206634a0e081bb14f8 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 17:56:43 -0600 Subject: [PATCH 10/20] MNT: Remove some restrictions on masked array ufunc implementations --- numpy/ma/core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index e44185353c99..16e8c844e14a 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3152,9 +3152,12 @@ def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): # Determine what class types we are compatible with and return # NotImplemented if we don't know how to handle them - for arg in args + outputs: - if not isinstance(arg, (ndarray, np.bool_, Number, list, str)): + for arg in args: + if not isinstance(arg, (ndarray, Number, str)): return NotImplemented + for arg in outputs: + if not isinstance(arg, ndarray): + raise NotImplemented # Get the equivalent masked version of the numpy function # if it is in the module level functions From b35c309f4f5a258cb991ebad211169a69620380c Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 23 Jul 2022 18:40:45 -0600 Subject: [PATCH 11/20] FIX: Rearrange handling of ndarray-like ufuncs Move the np-ufunc check to the top of the routine so we immediately go to super() when necessary. Before we were returning NotImplemented if an arg wasn't able to be handled. Update the arg instance check to defer for everything but another class that has implemented __array_ufunc__ --- numpy/ma/core.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 16e8c844e14a..05f3fec36088 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3143,6 +3143,14 @@ def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): then call with the ndarray view of the input data. """ + # Get the equivalent masked version of the numpy function + # if it is in the module level functions + ma_ufunc = np.ma.__dict__.get(np_ufunc.__name__, np_ufunc) + if ma_ufunc is np_ufunc: + # We didn't have a Masked version of the ufunc, so we need to + # call the ndarray version to prevent infinite recursion. + return super().__array_ufunc__(np_ufunc, method, *inputs, **kwargs) + # Output can be specified as arguments or as keyword arguments outputs = kwargs.pop('out', ()) if not isinstance(outputs, tuple): @@ -3153,19 +3161,14 @@ def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): # Determine what class types we are compatible with and return # NotImplemented if we don't know how to handle them for arg in args: - if not isinstance(arg, (ndarray, Number, str)): + if not isinstance(arg, ndarray) and hasattr(arg, "__array_ufunc__"): + # we want to defer to other implementations, unless it is an + # ndarray which MaskedArray will handle here instead return NotImplemented for arg in outputs: if not isinstance(arg, ndarray): - raise NotImplemented - - # Get the equivalent masked version of the numpy function - # if it is in the module level functions - ma_ufunc = np.ma.__dict__.get(np_ufunc.__name__, np_ufunc) - if ma_ufunc is np_ufunc: - # We didn't have a Masked version of the ufunc, so we need to - # call the ndarray version to prevent infinite recursion. - return super().__array_ufunc__(np_ufunc, method, *inputs, **kwargs) + # Output must be an ndarray + return NotImplemented results = getattr(ma_ufunc, method)(*args, **kwargs) if results is NotImplemented: From de22beb51b2821357a4ed0d73907d56fc183eb7e Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 13:20:22 -0600 Subject: [PATCH 12/20] FIX: Pass kwargs through ufunc reduce --- numpy/ma/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 05f3fec36088..e4d9f560046a 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -22,7 +22,6 @@ # pylint: disable-msg=E1002 import builtins import inspect -from numbers import Number import operator import warnings import textwrap @@ -1076,11 +1075,11 @@ def reduce(self, target, axis=0, dtype=None, **kwargs): m.shape = (1,) if m is nomask: - tr = self.f.reduce(t, axis) + tr = self.f.reduce(t, axis, **kwargs) mr = nomask else: - tr = self.f.reduce(t, axis, dtype=dtype) - mr = umath.logical_and.reduce(m, axis) + tr = self.f.reduce(t, axis, dtype=dtype, **kwargs) + mr = umath.logical_and.reduce(m, axis, **kwargs) if not tr.shape: if mr: From 9564f27fd03bcade82cfdd6aa350f394b701b961 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:11:33 -0600 Subject: [PATCH 13/20] MNT: Add power ufunc to masked arrays --- numpy/ma/core.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index e4d9f560046a..0f0de92c1986 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -73,7 +73,7 @@ 'product', 'ptp', 'put', 'putmask', 'ravel', 'remainder', 'repeat', 'reshape', 'resize', 'right_shift', 'round', 'round_', 'set_fill_value', 'shape', 'sin', 'sign', 'sinh', 'size', 'soften_mask', - 'sometrue', 'sort', 'sqrt', 'squeeze', 'std', 'subtract', 'sum', + 'sometrue', 'sort', 'sqrt', 'square', 'squeeze', 'std', 'subtract', 'sum', 'swapaxes', 'take', 'tan', 'tanh', 'trace', 'transpose', 'true_divide', 'var', 'where', 'zeros', 'zeros_like', ] @@ -1230,6 +1230,7 @@ def __call__(self, a, b, *args, **kwargs): isfinite = _MaskedUnaryOperation(umath.isfinite) invert = _MaskedUnaryOperation(np.invert) sign = _MaskedUnaryOperation(umath.sign) +square = _MaskedUnaryOperation(umath.square) # Domained unary ufuncs sqrt = _MaskedUnaryOperation(umath.sqrt, 0.0, @@ -4305,22 +4306,6 @@ def __ge__(self, other): def __gt__(self, other): return self._comparison(other, operator.gt) - def __pow__(self, other): - """ - Raise self to the power other, masking the potential NaNs/Infs - - """ - if self._delegate_binop(other): - return NotImplemented - return power(self, other) - - def __rpow__(self, other): - """ - Raise other to the power self, masking the potential NaNs/Infs - - """ - return power(other, self) - def __iadd__(self, other): """ Add other to self in-place. From a7ba76fb9c8b51c26fa8052ee35f1beccb6f39c1 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:18:20 -0600 Subject: [PATCH 14/20] FIX: add back getmaskarray to __array_wrap__ of masked arrays --- numpy/ma/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 0f0de92c1986..eb8e96b9362a 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3096,7 +3096,7 @@ def __array_wrap__(self, obj, context=None): func, args, out_i = context # args sometimes contains outputs (gh-10459), which we don't want input_args = args[:func.nin] - m = reduce(mask_or, [getmask(arg) for arg in input_args]) + m = reduce(mask_or, [getmaskarray(arg) for arg in input_args]) # Get the domain mask domain = ufunc_domain.get(func, None) if domain is not None: @@ -3124,6 +3124,7 @@ def __array_wrap__(self, obj, context=None): else: # Don't modify inplace, we risk back-propagation m = (m | d) + # Make sure the mask has the proper size if result is not self and result.shape == () and m: return masked From fa6c56f67c0e4725fc6966c5f094633394a27e43 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:21:00 -0600 Subject: [PATCH 15/20] FIX: view should return the type(self) rather than MaskedArray This allows for subclasses to be handled correctly --- numpy/ma/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index eb8e96b9362a..4c7853978760 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3384,7 +3384,7 @@ def _scalar_heuristic(arr, elem): return dout else: # Force dout to MA - dout = MaskedArray(dout) + dout = dout.view(type(self)) # Inherit attributes from self dout._update_from(self) # Check the fill_value From cbfd86f80a4d42d822132b4e45740d8cca5698e5 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:26:42 -0600 Subject: [PATCH 16/20] FIX: Revert sum and trace overrides from masked arrays --- numpy/ma/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 4c7853978760..521eb185bd75 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -5066,8 +5066,8 @@ def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): #!!!: implement out + test! m = self._mask if m is nomask: - result = self.view(np.ndarray).trace(offset=offset, axis1=axis1, - axis2=axis2, out=out) + result = super().trace(offset=offset, axis1=axis1, axis2=axis2, + out=out) return result.astype(dtype) else: D = self.diagonal(offset=offset, axis1=axis1, axis2=axis2) @@ -5168,8 +5168,7 @@ def sum(self, axis=None, dtype=None, out=None, keepdims=np._NoValue): return result # Explicit output - self.filled(0).sum(axis, dtype=dtype, out=out.view(np.ndarray), - **kwargs) + self.filled(0).sum(axis, dtype=dtype, out=out, **kwargs) if isinstance(out, MaskedArray): outmask = getmask(out) if outmask is nomask: From 4a58583f46760f548cb4f27521ff79f548c35681 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:47:18 -0600 Subject: [PATCH 17/20] ENH: Add rint and update round method --- numpy/ma/core.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 521eb185bd75..faa4c6f54e67 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -71,7 +71,7 @@ 'mod', 'multiply', 'mvoid', 'ndim', 'negative', 'nomask', 'nonzero', 'not_equal', 'ones', 'ones_like', 'outer', 'outerproduct', 'power', 'prod', 'product', 'ptp', 'put', 'putmask', 'ravel', 'remainder', - 'repeat', 'reshape', 'resize', 'right_shift', 'round', 'round_', + 'repeat', 'reshape', 'resize', 'right_shift', 'rint', 'round', 'round_', 'set_fill_value', 'shape', 'sin', 'sign', 'sinh', 'size', 'soften_mask', 'sometrue', 'sort', 'sqrt', 'square', 'squeeze', 'std', 'subtract', 'sum', 'swapaxes', 'take', 'tan', 'tanh', 'trace', 'transpose', 'true_divide', @@ -1229,6 +1229,7 @@ def __call__(self, a, b, *args, **kwargs): isnan = _MaskedUnaryOperation(umath.isnan) isfinite = _MaskedUnaryOperation(umath.isfinite) invert = _MaskedUnaryOperation(np.invert) +rint = _MaskedUnaryOperation(umath.rint) sign = _MaskedUnaryOperation(umath.sign) square = _MaskedUnaryOperation(umath.square) @@ -5485,10 +5486,6 @@ def round(self, decimals=0, out=None): numpy.ndarray.round : corresponding function for ndarrays numpy.around : equivalent function """ - stored_out = None - if isinstance(out, MaskedArray): - stored_out = out - out = getdata(out) result = self._data.round(decimals=decimals, out=out).view(type(self)) if result.ndim > 0: result._mask = self._mask @@ -5499,9 +5496,7 @@ def round(self, decimals=0, out=None): # No explicit output: we're done if out is None: return result - if stored_out is not None: - # We got in a masked array originally, so we need to return one - out = stored_out + if isinstance(out, MaskedArray): out.__setmask__(self._mask) return out From 2acf5302f209352653c5a79679e47b4c8ebd5f9c Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 14:57:08 -0600 Subject: [PATCH 18/20] FIX: Change back partition/argpartition --- numpy/ma/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index faa4c6f54e67..2de0c3471ad1 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -6047,13 +6047,13 @@ def partition(self, *args, **kwargs): warnings.warn("Warning: 'partition' will ignore the 'mask' " f"of the {self.__class__.__name__}.", stacklevel=2) - return self.view(np.ndarray).partition(*args, **kwargs) + return super().partition(*args, **kwargs) def argpartition(self, *args, **kwargs): warnings.warn("Warning: 'argpartition' will ignore the 'mask' " f"of the {self.__class__.__name__}.", stacklevel=2) - return self.view(np.ndarray).argpartition(*args, **kwargs) + return super().argpartition(*args, **kwargs) def take(self, indices, axis=None, out=None, mode='raise'): """ From e6e80e6f7e27f4fdc4fa29d9803128b540c122c2 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 24 Jul 2022 15:12:53 -0600 Subject: [PATCH 19/20] FIX: Remove compress updates --- numpy/ma/core.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/numpy/ma/core.py b/numpy/ma/core.py index 2de0c3471ad1..b87b627f2574 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -1075,7 +1075,7 @@ def reduce(self, target, axis=0, dtype=None, **kwargs): m.shape = (1,) if m is nomask: - tr = self.f.reduce(t, axis, **kwargs) + tr = self.f.reduce(t.view(np.ndarray), axis, dtype=dtype, **kwargs) mr = nomask else: tr = self.f.reduce(t, axis, dtype=dtype, **kwargs) @@ -4057,15 +4057,10 @@ def compress(self, condition, axis=None, out=None): # values. condition = np.asarray(condition) - _new = _data.compress(condition, axis=axis).view(type(self)) + _new = _data.compress(condition, axis=axis, out=out).view(type(self)) _new._update_from(self) if _mask is not nomask: _new._mask = _mask.compress(condition, axis=axis) - if out is not None: - out._update_from(self) - out.data[:] = _new.data - out._mask = _new.mask - return out return _new def _insert_masked_print(self): From 366dfc36a25e7c08dbffbf00388ebbff4f601a93 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 1 Jan 2023 20:31:03 -0700 Subject: [PATCH 20/20] MAINT: Remove unused delegate_binop code This is handled in the C code now within the ufunc machinery. --- ...22.improvement.rst => 22914.improvement.rst} | 0 numpy/core/src/common/binop_override.h | 6 ------ numpy/ma/core.py | 17 ++--------------- 3 files changed, 2 insertions(+), 21 deletions(-) rename doc/release/upcoming_changes/{16022.improvement.rst => 22914.improvement.rst} (100%) diff --git a/doc/release/upcoming_changes/16022.improvement.rst b/doc/release/upcoming_changes/22914.improvement.rst similarity index 100% rename from doc/release/upcoming_changes/16022.improvement.rst rename to doc/release/upcoming_changes/22914.improvement.rst diff --git a/numpy/core/src/common/binop_override.h b/numpy/core/src/common/binop_override.h index ec3d046796ab..7a2170074cbc 100644 --- a/numpy/core/src/common/binop_override.h +++ b/numpy/core/src/common/binop_override.h @@ -105,12 +105,6 @@ binop_should_defer(PyObject *self, PyObject *other, int inplace) * logic for forward binop implementations. */ - /* - * NB: there's another copy of this code in - * numpy.ma.core.MaskedArray._delegate_binop - * which should possibly be updated when this is. - */ - PyObject *attr; double self_prio, other_prio; int defer; diff --git a/numpy/ma/core.py b/numpy/ma/core.py index b87b627f2574..8489dbd0d96e 100644 --- a/numpy/ma/core.py +++ b/numpy/ma/core.py @@ -3163,7 +3163,8 @@ def __array_ufunc__(self, np_ufunc, method, *inputs, **kwargs): # Determine what class types we are compatible with and return # NotImplemented if we don't know how to handle them for arg in args: - if not isinstance(arg, ndarray) and hasattr(arg, "__array_ufunc__"): + if (not isinstance(arg, ndarray) + and hasattr(arg, "__array_ufunc__")): # we want to defer to other implementations, unless it is an # ndarray which MaskedArray will handle here instead return NotImplemented @@ -4181,20 +4182,6 @@ def __repr__(self): ) return prefix + result + ')' - def _delegate_binop(self, other): - # This emulates the logic in - # private/binop_override.h:forward_binop_should_defer - if isinstance(other, type(self)): - return False - array_ufunc = getattr(other, "__array_ufunc__", False) - if array_ufunc is False: - other_priority = getattr(other, "__array_priority__", -1000000) - return self.__array_priority__ < other_priority - else: - # If array_ufunc is not None, it will be called inside the ufunc; - # None explicitly tells us to not call the ufunc, i.e., defer. - return array_ufunc is None - def _comparison(self, other, compare): """Compare self with other using operator.eq or operator.ne.