numpy icon indicating copy to clipboard operation
numpy copied to clipboard

BUG: MaskedArray does not seem to respect ufunc dispatch hierarchy

Open jthielen opened this issue 6 years ago • 4 comments

When using a custom array container that implements __array_ufunc__ and is meant to be able to wrap MaskedArrays, non-commutativity occurs in binary operations with MaskedArrays. Despite what the below comment mentions:

https://github.com/numpy/numpy/blob/v1.17.3/numpy/ma/core.py#L3960-L3972

it does not properly defer, as seen in the example below. I am unfortunately not well-acquainted with the MaskedArray internals, so I don't know what would be a good way forward for a fix.

xref https://github.com/hgrecco/pint/issues/633

Reproducing code example:

import numpy as np
import numpy.lib.mixins


class WrappedArray(numpy.lib.mixins.NDArrayOperatorsMixin):
    __array_priority__ = 20

    def __init__(self, array, **attrs):
        self._array = array
        self.attrs = attrs

    def __repr__(self):
        return f"{self.__class__.__name__}(\n{self._array}\n{self.attrs}\n)"

    def __array__(self):
        return np.asarray(self._array)

    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
        if method == '__call__':
            inputs = [arg._array if isinstance(arg, self.__class__) else arg
                      for arg in inputs]
            return self.__class__(ufunc(*inputs, **kwargs), **self.attrs)
        else:
            return NotImplemented


# Show basic wrap
w = WrappedArray(np.arange(3), test=1)
print(w)
print()

# Wrapping MaskedArrays works fine
wm = WrappedArray(np.ma.masked_array([1, 3, 5], mask=[False, True, False]),
                  test=2)
print(wm)
print()

# Operations with ndarrays work fine
a = np.array([2, 0, 1])
print(a * w)
print()

# Operations with masked arrays are not commutative
m = np.ma.masked_array([2, 0, 1], mask=[False, True, False])

# Good
print(w * m)
print()

# Bad
print(m * w)

Output:

WrappedArray(
[0 1 2]
{'test': 1}
)

WrappedArray(
[1 -- 5]
{'test': 2}
)

WrappedArray(
[0 0 2]
{'test': 1}
)

WrappedArray(
[0 -- 2]
{'test': 1}
)

[0 -- 2]

Numpy/Python version information:

1.17.3 3.6.7 | packaged by conda-forge | (default, Nov 6 2019, 16:19:42) [GCC 7.3.0]

jthielen avatar Dec 30 '19 04:12 jthielen

ping @mhvk who may have some thoughts here.

I suspect the right fix is to define a __array_ufunc__ method on MaskedArray, and rewrite the arithmetic special methods from:

   def add(self, other):
        if self._delegate_binop(other):
            return NotImplemented
        return add(self, other)

to

   def add(self, other):
        if self._delegate_binop(other):
            return NotImplemented
        return np.add(self, other)

Currently MaskedArray overrides ufuncs via __array_finalize__, which is quite indirect. There is an __array_ufunc__ method defined, but only via inheritance from the base np.ndarray class.

shoyer avatar Dec 30 '19 23:12 shoyer

Yes, MaskedArray should gain a proper __array_ufunc__... Right now, you also have to use the "old" system of overrides, and write a custom __array_wrap__. I think this may be do-able, but obviously less clear if it is worth the effort.

mhvk avatar Jan 03 '20 16:01 mhvk

This issue should be re-opened since #16022 was reverted.

dopplershift avatar Jul 11 '22 14:07 dopplershift

Reopening, since the PR to use __array_ufunc__ was reverted.

mattip avatar Jul 20 '22 07:07 mattip