| |
| """Tests suite for MaskedArray & subclassing. |
| |
| :author: Pierre Gerard-Marchant |
| :contact: pierregm_at_uga_dot_edu |
| :version: $Id: test_subclassing.py 3473 2007-10-29 15:18:13Z jarrod.millman $ |
| |
| """ |
| import numpy as np |
| from numpy.lib.mixins import NDArrayOperatorsMixin |
| from numpy.testing import assert_, assert_raises |
| from numpy.ma.testutils import assert_equal |
| from numpy.ma.core import ( |
| array, arange, masked, MaskedArray, masked_array, log, add, hypot, |
| divide, asarray, asanyarray, nomask |
| ) |
| |
|
|
| def assert_startswith(a, b): |
| |
| assert_equal(a[:len(b)], b) |
|
|
| class SubArray(np.ndarray): |
| |
| |
| def __new__(cls,arr,info={}): |
| x = np.asanyarray(arr).view(cls) |
| x.info = info.copy() |
| return x |
|
|
| def __array_finalize__(self, obj): |
| super().__array_finalize__(obj) |
| self.info = getattr(obj, 'info', {}).copy() |
| return |
|
|
| def __add__(self, other): |
| result = super().__add__(other) |
| result.info['added'] = result.info.get('added', 0) + 1 |
| return result |
|
|
| def __iadd__(self, other): |
| result = super().__iadd__(other) |
| result.info['iadded'] = result.info.get('iadded', 0) + 1 |
| return result |
|
|
|
|
| subarray = SubArray |
|
|
|
|
| class SubMaskedArray(MaskedArray): |
| """Pure subclass of MaskedArray, keeping some info on subclass.""" |
| def __new__(cls, info=None, **kwargs): |
| obj = super().__new__(cls, **kwargs) |
| obj._optinfo['info'] = info |
| return obj |
|
|
|
|
| class MSubArray(SubArray, MaskedArray): |
|
|
| def __new__(cls, data, info={}, mask=nomask): |
| subarr = SubArray(data, info) |
| _data = MaskedArray.__new__(cls, data=subarr, mask=mask) |
| _data.info = subarr.info |
| return _data |
|
|
| @property |
| def _series(self): |
| _view = self.view(MaskedArray) |
| _view._sharedmask = False |
| return _view |
|
|
| msubarray = MSubArray |
|
|
|
|
| |
| |
| |
| |
| |
| class CSAIterator: |
| """ |
| Flat iterator object that uses its own setter/getter |
| (works around ndarray.flat not propagating subclass setters/getters |
| see https://github.com/numpy/numpy/issues/4564) |
| roughly following MaskedIterator |
| """ |
| def __init__(self, a): |
| self._original = a |
| self._dataiter = a.view(np.ndarray).flat |
|
|
| def __iter__(self): |
| return self |
|
|
| def __getitem__(self, indx): |
| out = self._dataiter.__getitem__(indx) |
| if not isinstance(out, np.ndarray): |
| out = out.__array__() |
| out = out.view(type(self._original)) |
| return out |
|
|
| def __setitem__(self, index, value): |
| self._dataiter[index] = self._original._validate_input(value) |
|
|
| def __next__(self): |
| return next(self._dataiter).__array__().view(type(self._original)) |
|
|
|
|
| class ComplicatedSubArray(SubArray): |
|
|
| def __str__(self): |
| return f'myprefix {self.view(SubArray)} mypostfix' |
|
|
| def __repr__(self): |
| |
| return f'<{self.__class__.__name__} {self}>' |
|
|
| def _validate_input(self, value): |
| if not isinstance(value, ComplicatedSubArray): |
| raise ValueError("Can only set to MySubArray values") |
| return value |
|
|
| def __setitem__(self, item, value): |
| |
| |
| super().__setitem__(item, self._validate_input(value)) |
|
|
| def __getitem__(self, item): |
| |
| value = super().__getitem__(item) |
| if not isinstance(value, np.ndarray): |
| value = value.__array__().view(ComplicatedSubArray) |
| return value |
|
|
| @property |
| def flat(self): |
| return CSAIterator(self) |
|
|
| @flat.setter |
| def flat(self, value): |
| y = self.ravel() |
| y[:] = value |
|
|
| def __array_wrap__(self, obj, context=None, return_scalar=False): |
| obj = super().__array_wrap__(obj, context, return_scalar) |
| if context is not None and context[0] is np.multiply: |
| obj.info['multiplied'] = obj.info.get('multiplied', 0) + 1 |
|
|
| return obj |
|
|
|
|
| class WrappedArray(NDArrayOperatorsMixin): |
| """ |
| Wrapping a MaskedArray rather than subclassing to test that |
| ufunc deferrals are commutative. |
| See: https://github.com/numpy/numpy/issues/15200) |
| """ |
| __slots__ = ('_array', 'attrs') |
| __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, dtype=None, copy=None): |
| 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 |
|
|
|
|
| class TestSubclassing: |
| |
|
|
| def setup_method(self): |
| x = np.arange(5, dtype='float') |
| mx = msubarray(x, mask=[0, 1, 0, 0, 0]) |
| self.data = (x, mx) |
|
|
| def test_data_subclassing(self): |
| |
| x = np.arange(5) |
| m = [0, 0, 1, 0, 0] |
| xsub = SubArray(x) |
| xmsub = masked_array(xsub, mask=m) |
| assert_(isinstance(xmsub, MaskedArray)) |
| assert_equal(xmsub._data, xsub) |
| assert_(isinstance(xmsub._data, SubArray)) |
|
|
| def test_maskedarray_subclassing(self): |
| |
| (x, mx) = self.data |
| assert_(isinstance(mx._data, subarray)) |
|
|
| def test_masked_unary_operations(self): |
| |
| (x, mx) = self.data |
| with np.errstate(divide='ignore'): |
| assert_(isinstance(log(mx), msubarray)) |
| assert_equal(log(x), np.log(x)) |
|
|
| def test_masked_binary_operations(self): |
| |
| (x, mx) = self.data |
| |
| assert_(isinstance(add(mx, mx), msubarray)) |
| assert_(isinstance(add(mx, x), msubarray)) |
| |
| assert_equal(add(mx, x), mx+x) |
| assert_(isinstance(add(mx, mx)._data, subarray)) |
| assert_(isinstance(add.outer(mx, mx), msubarray)) |
| assert_(isinstance(hypot(mx, mx), msubarray)) |
| assert_(isinstance(hypot(mx, x), msubarray)) |
|
|
| def test_masked_binary_operations2(self): |
| |
| (x, mx) = self.data |
| xmx = masked_array(mx.data.__array__(), mask=mx.mask) |
| assert_(isinstance(divide(mx, mx), msubarray)) |
| assert_(isinstance(divide(mx, x), msubarray)) |
| assert_equal(divide(mx, mx), divide(xmx, xmx)) |
|
|
| def test_attributepropagation(self): |
| x = array(arange(5), mask=[0]+[1]*4) |
| my = masked_array(subarray(x)) |
| ym = msubarray(x) |
| |
| z = (my+1) |
| assert_(isinstance(z, MaskedArray)) |
| assert_(not isinstance(z, MSubArray)) |
| assert_(isinstance(z._data, SubArray)) |
| assert_equal(z._data.info, {}) |
| |
| z = (ym+1) |
| assert_(isinstance(z, MaskedArray)) |
| assert_(isinstance(z, MSubArray)) |
| assert_(isinstance(z._data, SubArray)) |
| assert_(z._data.info['added'] > 0) |
| |
| ym += 1 |
| assert_(isinstance(ym, MaskedArray)) |
| assert_(isinstance(ym, MSubArray)) |
| assert_(isinstance(ym._data, SubArray)) |
| assert_(ym._data.info['iadded'] > 0) |
| |
| ym._set_mask([1, 0, 0, 0, 1]) |
| assert_equal(ym._mask, [1, 0, 0, 0, 1]) |
| ym._series._set_mask([0, 0, 0, 0, 1]) |
| assert_equal(ym._mask, [0, 0, 0, 0, 1]) |
| |
| xsub = subarray(x, info={'name':'x'}) |
| mxsub = masked_array(xsub) |
| assert_(hasattr(mxsub, 'info')) |
| assert_equal(mxsub.info, xsub.info) |
|
|
| def test_subclasspreservation(self): |
| |
| x = np.arange(5) |
| m = [0, 0, 1, 0, 0] |
| xinfo = list(zip(x, m)) |
| xsub = MSubArray(x, mask=m, info={'xsub':xinfo}) |
| |
| mxsub = masked_array(xsub, subok=False) |
| assert_(not isinstance(mxsub, MSubArray)) |
| assert_(isinstance(mxsub, MaskedArray)) |
| assert_equal(mxsub._mask, m) |
| |
| mxsub = asarray(xsub) |
| assert_(not isinstance(mxsub, MSubArray)) |
| assert_(isinstance(mxsub, MaskedArray)) |
| assert_equal(mxsub._mask, m) |
| |
| mxsub = masked_array(xsub, subok=True) |
| assert_(isinstance(mxsub, MSubArray)) |
| assert_equal(mxsub.info, xsub.info) |
| assert_equal(mxsub._mask, xsub._mask) |
| |
| mxsub = asanyarray(xsub) |
| assert_(isinstance(mxsub, MSubArray)) |
| assert_equal(mxsub.info, xsub.info) |
| assert_equal(mxsub._mask, m) |
|
|
| def test_subclass_items(self): |
| """test that getter and setter go via baseclass""" |
| x = np.arange(5) |
| xcsub = ComplicatedSubArray(x) |
| mxcsub = masked_array(xcsub, mask=[True, False, True, False, False]) |
| |
| |
| assert_(isinstance(xcsub[1], ComplicatedSubArray)) |
| assert_(isinstance(xcsub[1,...], ComplicatedSubArray)) |
| assert_(isinstance(xcsub[1:4], ComplicatedSubArray)) |
|
|
| |
| assert_(isinstance(mxcsub[1], ComplicatedSubArray)) |
| assert_(isinstance(mxcsub[1,...].data, ComplicatedSubArray)) |
| assert_(mxcsub[0] is masked) |
| assert_(isinstance(mxcsub[0,...].data, ComplicatedSubArray)) |
| assert_(isinstance(mxcsub[1:4].data, ComplicatedSubArray)) |
|
|
| |
| assert_(isinstance(mxcsub.flat[1].data, ComplicatedSubArray)) |
| assert_(mxcsub.flat[0] is masked) |
| assert_(isinstance(mxcsub.flat[1:4].base, ComplicatedSubArray)) |
|
|
| |
| |
| assert_raises(ValueError, xcsub.__setitem__, 1, x[4]) |
| |
| assert_raises(ValueError, mxcsub.__setitem__, 1, x[4]) |
| assert_raises(ValueError, mxcsub.__setitem__, slice(1, 4), x[1:4]) |
| mxcsub[1] = xcsub[4] |
| mxcsub[1:4] = xcsub[1:4] |
| |
| assert_raises(ValueError, mxcsub.flat.__setitem__, 1, x[4]) |
| assert_raises(ValueError, mxcsub.flat.__setitem__, slice(1, 4), x[1:4]) |
| mxcsub.flat[1] = xcsub[4] |
| mxcsub.flat[1:4] = xcsub[1:4] |
|
|
| def test_subclass_nomask_items(self): |
| x = np.arange(5) |
| xcsub = ComplicatedSubArray(x) |
| mxcsub_nomask = masked_array(xcsub) |
|
|
| assert_(isinstance(mxcsub_nomask[1,...].data, ComplicatedSubArray)) |
| assert_(isinstance(mxcsub_nomask[0,...].data, ComplicatedSubArray)) |
|
|
| assert_(isinstance(mxcsub_nomask[1], ComplicatedSubArray)) |
| assert_(isinstance(mxcsub_nomask[0], ComplicatedSubArray)) |
|
|
| def test_subclass_repr(self): |
| """test that repr uses the name of the subclass |
| and 'array' for np.ndarray""" |
| x = np.arange(5) |
| mx = masked_array(x, mask=[True, False, True, False, False]) |
| assert_startswith(repr(mx), 'masked_array') |
| xsub = SubArray(x) |
| mxsub = masked_array(xsub, mask=[True, False, True, False, False]) |
| assert_startswith(repr(mxsub), |
| f'masked_{SubArray.__name__}(data=[--, 1, --, 3, 4]') |
|
|
| def test_subclass_str(self): |
| """test str with subclass that has overridden str, setitem""" |
| |
| x = np.arange(5) |
| xsub = SubArray(x) |
| mxsub = masked_array(xsub, mask=[True, False, True, False, False]) |
| assert_equal(str(mxsub), '[-- 1 -- 3 4]') |
|
|
| xcsub = ComplicatedSubArray(x) |
| assert_raises(ValueError, xcsub.__setitem__, 0, |
| np.ma.core.masked_print_option) |
| mxcsub = masked_array(xcsub, mask=[True, False, True, False, False]) |
| assert_equal(str(mxcsub), 'myprefix [-- 1 -- 3 4] mypostfix') |
|
|
| def test_pure_subclass_info_preservation(self): |
| |
| |
| arr1 = SubMaskedArray('test', data=[1,2,3,4,5,6]) |
| arr2 = SubMaskedArray(data=[0,1,2,3,4,5]) |
| diff1 = np.subtract(arr1, arr2) |
| assert_('info' in diff1._optinfo) |
| assert_(diff1._optinfo['info'] == 'test') |
| diff2 = arr1 - arr2 |
| assert_('info' in diff2._optinfo) |
| assert_(diff2._optinfo['info'] == 'test') |
|
|
|
|
| class ArrayNoInheritance: |
| """Quantity-like class that does not inherit from ndarray""" |
| def __init__(self, data, units): |
| self.magnitude = data |
| self.units = units |
|
|
| def __getattr__(self, attr): |
| return getattr(self.magnitude, attr) |
|
|
|
|
| def test_array_no_inheritance(): |
| data_masked = np.ma.array([1, 2, 3], mask=[True, False, True]) |
| data_masked_units = ArrayNoInheritance(data_masked, 'meters') |
|
|
| |
| new_array = np.ma.array(data_masked_units) |
| assert_equal(data_masked.data, new_array.data) |
| assert_equal(data_masked.mask, new_array.mask) |
| |
| data_masked.mask = [True, False, False] |
| assert_equal(data_masked.mask, new_array.mask) |
| assert_(new_array.sharedmask) |
|
|
| |
| new_array = np.ma.array(data_masked_units, copy=True) |
| assert_equal(data_masked.data, new_array.data) |
| assert_equal(data_masked.mask, new_array.mask) |
| |
| data_masked.mask = [True, False, True] |
| assert_equal([True, False, False], new_array.mask) |
| assert_(not new_array.sharedmask) |
|
|
| |
| new_array = np.ma.array(data_masked_units, keep_mask=False) |
| assert_equal(data_masked.data, new_array.data) |
| |
| assert_equal(data_masked.mask, [True, False, True]) |
| |
| assert_(not new_array.mask) |
| assert_(not new_array.sharedmask) |
|
|
|
|
| class TestClassWrapping: |
| |
|
|
| def setup_method(self): |
| m = np.ma.masked_array([1, 3, 5], mask=[False, True, False]) |
| wm = WrappedArray(m) |
| self.data = (m, wm) |
|
|
| def test_masked_unary_operations(self): |
| |
| (m, wm) = self.data |
| with np.errstate(divide='ignore'): |
| assert_(isinstance(np.log(wm), WrappedArray)) |
|
|
| def test_masked_binary_operations(self): |
| |
| (m, wm) = self.data |
| |
| assert_(isinstance(np.add(wm, wm), WrappedArray)) |
| assert_(isinstance(np.add(m, wm), WrappedArray)) |
| assert_(isinstance(np.add(wm, m), WrappedArray)) |
| |
| assert_equal(np.add(m, wm), m + wm) |
| assert_(isinstance(np.hypot(m, wm), WrappedArray)) |
| assert_(isinstance(np.hypot(wm, m), WrappedArray)) |
| |
| assert_(isinstance(np.divide(wm, m), WrappedArray)) |
| assert_(isinstance(np.divide(m, wm), WrappedArray)) |
| assert_equal(np.divide(wm, m) * m, np.divide(m, m) * wm) |
| |
| m2 = np.stack([m, m]) |
| assert_(isinstance(np.divide(wm, m2), WrappedArray)) |
| assert_(isinstance(np.divide(m2, wm), WrappedArray)) |
| assert_equal(np.divide(m2, wm), np.divide(wm, m2)) |
|
|
| def test_mixins_have_slots(self): |
| mixin = NDArrayOperatorsMixin() |
| |
| assert_raises(AttributeError, mixin.__setattr__, "not_a_real_attr", 1) |
|
|
| m = np.ma.masked_array([1, 3, 5], mask=[False, True, False]) |
| wm = WrappedArray(m) |
| assert_raises(AttributeError, wm.__setattr__, "not_an_attr", 2) |
|
|