From 25f09100e46ea2645afb6c31b46f4b135b289582 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 6 Nov 2018 22:48:58 +0100 Subject: [PATCH 01/47] Support NDArray indexing with None and Ellipsis --- python/mxnet/ndarray/ndarray.py | 85 +++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 0b7dca4ebba4..9300970c2885 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -415,27 +415,24 @@ def __setitem__(self, key, value): Examples -------- - >>> x = mx.nd.zeros((2,3)) + >>> x = mx.nd.zeros((2, 3)) >>> x[:] = 1 >>> x.asnumpy() array([[ 1., 1., 1.], [ 1., 1., 1.]], dtype=float32) - >>> x.asnumpy() - array([[ 1., 1., 1.], - [ 1., 1., 1.]], dtype=float32) - >>> x[:,1:2] = 2 + >>> x[:, 1:2] = 2 >>> x.asnumpy() array([[ 1., 2., 1.], [ 1., 2., 1.]], dtype=float32) - >>> x[1:2,1:] = 3 + >>> x[1:2, 1:] = 3 >>> x.asnumpy() array([[ 1., 2., 1.], [ 1., 3., 3.]], dtype=float32) - >>> x[1:,0:2] = mx.nd.zeros((1,2)) + >>> x[1:, 0:2] = mx.nd.zeros((1, 2)) >>> x.asnumpy() array([[ 1., 2., 1.], [ 0., 0., 3.]], dtype=float32) - >>> x[1,2] = 4 + >>> x[1, 2] = 4 >>> x.asnumpy() array([[ 1., 2., 1.], [ 0., 0., 4.]], dtype=float32) @@ -448,6 +445,7 @@ def __setitem__(self, key, value): array([[ 6., 5., 5.], [ 6., 0., 4.]], dtype=float32) """ + self, key = _expand_none_and_ellipsis(self, key, drop_none=True) indexing_dispatch_code = _get_indexing_dispatch_code(key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: self._set_nd_basic_indexing(key, value) @@ -471,7 +469,6 @@ def __getitem__(self, key): - If key is a list type, only a list of integers is supported, e.g. key=[1, 2] is supported, while not for key=[[1, 2]]. - - Ellipsis (...) and np.newaxis are not supported. - Boolean array indexing is not supported. Parameters @@ -481,7 +478,7 @@ def __getitem__(self, key): Examples -------- - >>> x = mx.nd.arange(0,6).reshape((2,3)) + >>> x = mx.nd.arange(0, 6).reshape((2, 3)) >>> x.asnumpy() array([[ 0., 1., 2.], [ 3., 4., 5.]], dtype=float32) @@ -510,6 +507,7 @@ def __getitem__(self, key): [[[4 5] [6 7]]] """ + self, key = _expand_none_and_ellipsis(self, key, drop_none=True) indexing_dispatch_code = _get_indexing_dispatch_code(key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: return self._get_nd_basic_indexing(key) @@ -2382,6 +2380,73 @@ def to_dlpack_for_write(self): """ return to_dlpack_for_write(self) + +def _expand_none_and_ellipsis(arr, idcs, drop_none=False): + orig_idcs = idcs + if not isinstance(idcs, tuple): + # NOTE: `arr[idcs]` should be equivalent to `arr[(idcs,)]`, but in + # some cases this is not true currently, e.g., with `slice` objects. + idcs = (idcs,) + + ## Step 1: Expand ellipsis to an appropriate number of `slice(None)`. + + # We need to loop explicitly since tuple functions like `index()` or + # `count()` use `==` internally, which doesn't play well with fancy + # indexing. + ell_idx = None + num_none = 0 + nonell_idcs = [] + for i, idx in enumerate(idcs): + if idx is Ellipsis: + if ell_idx is not None: + raise IndexError( + "Cannot use more than one ellipsis (`...`) " + "for indexing") + else: + ell_idx = i + else: + if idx is None: + num_none += 1 + nonell_idcs.append(idx) + + nonell_idcs = tuple(nonell_idcs) + + # If neither `None` nor `Ellipsis` are present, we have nothing to do and + # just return the input. + if ell_idx is None and num_none == 0: + return arr, orig_idcs + + if ell_idx is None: + # This handles the case of "too few" indices, e.g., `nd.zeros((2, 3))[0]`. + ell_idx = len(arr.shape) + + ell_ndim = len(arr.shape) + num_none - len(nonell_idcs) + new_idcs = nonell_idcs[:ell_idx] + (slice(None),) * ell_ndim + nonell_idcs[ell_idx:] + + # If we decide to drop `None`, we return here and do not reshape. + if drop_none: + slc_idcs = tuple(idx for idx in new_idcs if idx is not None) + return arr, slc_idcs + + ## Step 2: Make new axes where `None` is in the index tuple. + + # We use `reshape()` with entries equal to 1 at new axes. + tmp_shape = [] + ax = 0 + for idx in new_idcs: + if idx is None: + tmp_shape.append(1) + else: + tmp_shape.append(arr.shape[ax]) + ax += 1 + + reshp_arr = arr.reshape(tmp_shape) + + slc_idcs = tuple(slice(None) if idx is None else idx + for idx in nonell_idcs) + return reshp_arr, slc_idcs + + def _get_indexing_dispatch_code(key): """Returns a dispatch code for calling basic or advanced indexing functions.""" if isinstance(key, (NDArray, np.ndarray)): From a23078f0d0efe07224e06fb3e68101cf5fbb2a71 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 6 Nov 2018 23:15:44 +0100 Subject: [PATCH 02/47] Update NDArray.__setitem__ docs with None and Ellipsis --- python/mxnet/ndarray/ndarray.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 9300970c2885..ececa22e3abc 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -403,7 +403,6 @@ def __setitem__(self, key, value): - If key is a list type, only a list of integers is supported, e.g. key=[1, 2] is supported, while not for key=[[1, 2]]. - - Ellipsis (...) and np.newaxis are not supported. - Boolean array indexing is not supported. Parameters From e602b20c32f73c212c23b6cf56b5f625e64dc946 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Wed, 7 Nov 2018 23:46:40 +0100 Subject: [PATCH 03/47] Fix boolean flag in NDArray.__getitem__, add doctests --- python/mxnet/ndarray/ndarray.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index ececa22e3abc..ca5f9de805c5 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -489,6 +489,12 @@ def __getitem__(self, key): array([[ 2., 2., 2.], [ 3., 4., 5.]], dtype=float32) >>> x = mx.nd.arange(0, 8, dtype='int32').reshape((2, 2, 2)) + >>> x[1, :] + [[4 5] + [6 7]] + >>> x[1, ...] # equivalent + [[4 5] + [6 7]] >>> x[[0, 1]] [[[0 1] [2 3]] @@ -505,8 +511,16 @@ def __getitem__(self, key): >>> x[1:, y] [[[4 5] [6 7]]] + >>> x[None, None].shape + (1, 1, 2, 2, 2) + >>> x[None, None, :].shape # equivalent + (1, 1, 2, 2, 2) + >>> x[None, None, ...].shape # equivalent + (1, 1, 2, 2, 2) + >>> x[None, ..., None].shape + (1, 2, 2, 2, 1) """ - self, key = _expand_none_and_ellipsis(self, key, drop_none=True) + self, key = _expand_none_and_ellipsis(self, key, drop_none=False) indexing_dispatch_code = _get_indexing_dispatch_code(key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: return self._get_nd_basic_indexing(key) From b1876e7a1f64cd562b74f14b472b05e050e20cf5 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 8 Nov 2018 00:42:28 +0100 Subject: [PATCH 04/47] Add setitem test for None and Ellipsis --- tests/python/unittest/test_ndarray.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 0f154bd67a1a..9d3b006a7496 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -20,9 +20,8 @@ from distutils.version import LooseVersion import os import pickle as pkl -import unittest from nose.tools import raises -from common import setup_module, with_seed, assertRaises, TemporaryDirectory, teardown +from common import with_seed, assertRaises, TemporaryDirectory from mxnet.test_utils import almost_equal from mxnet.test_utils import assert_almost_equal, assert_exception from mxnet.test_utils import default_context @@ -101,6 +100,26 @@ def test_ndarray_setitem(): x_np[-1] = 1 assert same(x.asnumpy(), x_np) + # Ellipsis + x = mx.nd.zeros(shape) + x[2, ...] = 1 + x_np = np.zeros(shape, dtype=x.dtype) + x_np[2, ...] = 1 + assert same(x.asnumpy(), x_np) + + x = mx.nd.zeros(shape) + x[..., 1] = 1 + x_np = np.zeros(shape, dtype=x.dtype) + x_np[..., 1] = 1 + assert same(x.asnumpy(), x_np) + + # `None` should be ignored + x = mx.nd.zeros(shape) + x[None, 0, None, None, 0, 0, None] = 1 + x_np = np.zeros(shape, dtype=x.dtype) + x_np[None, 0, None, None, 0, 0, None] = 1 + assert same(x.asnumpy(), x_np) + # short all-dim indexing x = mx.nd.zeros(shape) val = mx.nd.ones((3, 2)) From 8da6e001aebbffdb705b604c7e485fda08b825ec Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Fri, 9 Nov 2018 00:45:58 +0100 Subject: [PATCH 05/47] Fix wrong slice used, add cases to test_indexing --- python/mxnet/ndarray/ndarray.py | 12 ++++++++---- tests/python/unittest/test_ndarray.py | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index ca5f9de805c5..9ae312448356 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -821,7 +821,7 @@ def _get_nd_basic_indexing(self, key): for i, slice_i in enumerate(key): if isinstance(slice_i, integer_types): begin.append(slice_i) - end.append(slice_i+1 if slice_i != -1 else self.shape[i]) + end.append(slice_i + 1 if slice_i != -1 else self.shape[i]) step.append(1) elif isinstance(slice_i, py_slice): if slice_i.step == 0: @@ -834,7 +834,7 @@ def _get_nd_basic_indexing(self, key): else: raise ValueError('basic_indexing does not support slicing with ' 'index=%s of type=%s.' % (str(slice_i), str(type(slice_i)))) - kept_axes.extend(range(i+1, len(shape))) + kept_axes.extend(range(i + 1, len(shape))) sliced_nd = op.slice(self, begin, end, step) if len(kept_axes) == len(shape): return sliced_nd @@ -2434,7 +2434,9 @@ def _expand_none_and_ellipsis(arr, idcs, drop_none=False): ell_idx = len(arr.shape) ell_ndim = len(arr.shape) + num_none - len(nonell_idcs) - new_idcs = nonell_idcs[:ell_idx] + (slice(None),) * ell_ndim + nonell_idcs[ell_idx:] + new_idcs = (nonell_idcs[:ell_idx] + + (slice(None),) * ell_ndim + + nonell_idcs[ell_idx:]) # If we decide to drop `None`, we return here and do not reshape. if drop_none: @@ -2455,8 +2457,10 @@ def _expand_none_and_ellipsis(arr, idcs, drop_none=False): reshp_arr = arr.reshape(tmp_shape) + # Now that the `None` axes have size 1, we're done with them and + # replace `None` by `slice(None)` for the final indexing. slc_idcs = tuple(slice(None) if idx is None else idx - for idx in nonell_idcs) + for idx in new_idcs) return reshp_arr, slc_idcs diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 9d3b006a7496..89bce4c8c2b6 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1521,7 +1521,11 @@ def convert(num): (([[[[1]]]], 3, slice(0, 3), 0), False), (([[[[1]]]], [[2], [12]], slice(0, 3), slice(None)), False), (([1, 2], slice(3, 5), [2, 3], [3, 4]), False), - (([1, 2], slice(3, 5), (2, 3), [3, 4]), False)] + (([1, 2], slice(3, 5), (2, 3), [3, 4]), False), + ((1, Ellipsis, -1), False), + ((slice(2), Ellipsis, None, 0), False), + (None, False), + ((1, None, -2, 3, -4), False)] for index in index_list: test_getitem(np_array, index[0], index[1]) test_setitem(np_array, index[0], index[1]) From 153cd35974762d05efebc031de513a02188bcfca Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 15 Nov 2018 00:42:24 +0100 Subject: [PATCH 06/47] Revamp NDArray.__getitem__ and __setitem__ --- python/mxnet/ndarray/ndarray.py | 326 ++++++++++++++------------------ 1 file changed, 138 insertions(+), 188 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 9ae312448356..18ab7a929876 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -444,15 +444,20 @@ def __setitem__(self, key, value): array([[ 6., 5., 5.], [ 6., 0., 4.]], dtype=float32) """ - self, key = _expand_none_and_ellipsis(self, key, drop_none=True) - indexing_dispatch_code = _get_indexing_dispatch_code(key) + key = _expand_index_implicit_axes(key, self.shape) + conv_key, _ = _convert_index_int_to_slice(key) + slc_key = tuple(idx for idx in conv_key if idx is not None) + indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) + + indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: - self._set_nd_basic_indexing(key, value) + self._set_nd_basic_indexing(slc_key, value) elif indexing_dispatch_code == _NDARRAY_ADVANCED_INDEXING: - self._set_nd_advanced_indexing(key, value) + self._set_nd_advanced_indexing(slc_key, value) else: - raise ValueError('Indexing NDArray with index=%s and type=%s is not supported' - % (str(key), str(type(key)))) + raise ValueError( + 'Indexing NDArray with index {} of type {} is not supported' + ''.format(key, type(key))) # pylint: enable=line-too-long # pylint: disable=line-too-long @@ -520,15 +525,28 @@ def __getitem__(self, key): >>> x[None, ..., None].shape (1, 2, 2, 2, 1) """ - self, key = _expand_none_and_ellipsis(self, key, drop_none=False) - indexing_dispatch_code = _get_indexing_dispatch_code(key) + key = _expand_index_implicit_axes(key, self.shape) + conv_key, int_axes = _convert_index_int_to_slice(key) + new_axes = _new_axes_after_indexing(key) + slc_key = tuple(idx for idx in conv_key if idx is not None) + indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) + if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: - return self._get_nd_basic_indexing(key) + sliced = self._get_nd_basic_indexing(slc_key) elif indexing_dispatch_code == _NDARRAY_ADVANCED_INDEXING: - return self._get_nd_advanced_indexing(key) + sliced = self._get_nd_advanced_indexing(key) else: - raise ValueError('Indexing NDArray with index=%s and type=%s is not supported' - % (str(key), str(type(key)))) + raise ValueError( + 'Indexing NDArray with index {} of type {} is not supported' + ''.format(key, type(key))) + + final_shape = [sliced.shape[i] for i in range(sliced.ndim) + if i not in int_axes] + for new_ax in new_axes: + final_shape.insert(new_ax, 1) + + return sliced.reshape(final_shape) + # pylint: enable=line-too-long def _get_index_nd(self, key): @@ -681,7 +699,7 @@ def _prepare_value_nd(self, value, vshape): """Given value and vshape, create an `NDArray` from value with the same context and dtype as the current one and broadcast it to vshape.""" if isinstance(value, numeric_types): - value_nd = full(shape=vshape, val=value, ctx=self.context, dtype=self.dtype) + value_nd = full(vshape, value, ctx=self.context, dtype=self.dtype) elif isinstance(value, NDArray): value_nd = value.as_in_context(self.context) if value_nd.dtype != self.dtype: @@ -690,95 +708,59 @@ def _prepare_value_nd(self, value, vshape): try: value_nd = array(value, ctx=self.context, dtype=self.dtype) except: - raise TypeError('NDArray does not support assignment with non-array-like' - ' object %s of type %s' % (str(value), str(type(value)))) + raise TypeError('NDArray does not support assignment with non-array-like ' + 'object {} of type {}'.format(value, type(value))) + if value_nd.shape != vshape: value_nd = value_nd.broadcast_to(vshape) return value_nd def _set_nd_basic_indexing(self, key, value): - """This function is called by __setitem__ when key is a basic index, i.e. - an integer, or a slice, or a tuple of integers and slices. No restrictions - on the values of slices' steps.""" - shape = self.shape - if isinstance(key, integer_types): - if key < 0: - key += shape[0] - if key < 0 or key >= shape[0]: - if key < 0: - key -= shape[0] - raise IndexError('index %d is out of bounds for axis 0 with size %d' - % (key, shape[0])) - key = py_slice(key, key+1) # key must be >= 0 here - - if isinstance(key, py_slice): - assign_to_self = key.step is None or key.step == 1 - assign_to_self &= key.start is None or key.start == 0 - assign_to_self &= key.stop is None or key.stop == shape[0] - if assign_to_self: # trivial case, assign value to self - if isinstance(value, NDArray): - if value.handle is not self.handle: - if value.shape != shape: - value = value.broadcast_to(shape) - value.copyto(self) - elif isinstance(value, numeric_types): - _internal._full(shape=shape, ctx=self.context, - dtype=self.dtype, value=float(value), out=self) - elif isinstance(value, (np.ndarray, np.generic)): - if isinstance(value, np.generic) or value.shape != shape: - value = np.broadcast_to(value, shape) - self._sync_copyfrom(value) - else: # value might be a list or a tuple - value_nd = self._prepare_value_nd(value, shape) - value_nd.copyto(self) - return - else: # non-trivial case, use _slice_assign or _slice_assign_scalar - key = (key,) - - assert isinstance(key, tuple), "key=%s must be a tuple of slices and integers" % str(key) - - assert len(key) <= len(shape), "Indexing dimensions exceed array dimensions, %d vs %d"\ - % (len(key), len(shape)) - begin = [] - end = [] - steps = [] - oshape = [] # output shape of slice using key - vshape = [] # value shape of data[key] - for i, slice_i in enumerate(key): - dim_size = 1 - if isinstance(slice_i, py_slice): - begin.append(slice_i.start) - end.append(slice_i.stop) - steps.append(slice_i.step) - start, stop, step = _get_index_range(slice_i.start, slice_i.stop, - shape[i], slice_i.step) - dim_size = _get_dim_size(start, stop, step) - vshape.append(dim_size) - elif isinstance(slice_i, integer_types): - begin.append(slice_i) - end.append(slice_i+1 if slice_i != -1 else self.shape[i]) - steps.append(1) + """This function indexes ``self`` with a tuple of ``slice`` objects only.""" + if len(key) != self.ndim: + raise ValueError( + 'length of `key` must be equal to `self.ndim`, but {} != {}.' + 'This is a bug, please report it!' + ''.format(len(key), self.ndim)) + for idx in key: + if not isinstance(idx, py_slice): + raise RuntimeError( + '`key` may only contain `slice` objects in the basic ' + 'implementation, indexing, got object of type {}. ' + 'This is a bug, please report it!' + ''.format(type(idx))) + begin, end, step = _basic_indexing_tuple_to_begin_end_step(key, self.shape) + indexed_shape = tuple((e - b) // s for b, e, s in zip(begin, end, step)) + + if indexed_shape == self.shape: + # Easy case, overwrite whole array. + if isinstance(value, NDArray): + if value.handle is not self.handle: + if value.shape != self.shape: + value = value.broadcast_to(self.shape) + value.copyto(self) + elif isinstance(value, numeric_types): + _internal._full( + shape=self.shape, value=float(value), ctx=self.context, + dtype=self.dtype, out=self + ) + elif isinstance(value, (np.ndarray, np.generic)): + if isinstance(value, np.generic) or value.shape != self.shape: + value = np.broadcast_to(value, self.shape) + self._sync_copyfrom(value) else: - raise ValueError("basic indexing does not support index=%s of type=%s" - % (str(slice_i), str(type(slice_i)))) - oshape.append(dim_size) - - oshape.extend(shape[len(key):]) - vshape.extend(shape[len(key):]) - # if key contains all integers, vshape should be (1,) - if len(vshape) == 0: - vshape.append(1) - oshape = tuple(oshape) - vshape = tuple(vshape) + # Other array-like + value_nd = self._prepare_value_nd(value, self.shape) + value_nd.copyto(self) + + elif isinstance(value, numeric_types): + _internal._slice_assign_scalar( + self, float(value), begin, end, step, out=self + ) - if isinstance(value, numeric_types): - _internal._slice_assign_scalar(self, out=self, begin=begin, end=end, - step=steps, scalar=float(value)) else: - value_nd = self._prepare_value_nd(value, vshape) - if vshape != oshape: - value_nd = value_nd.reshape(oshape) - _internal._slice_assign(self, value_nd, begin, end, steps, out=self) + value_nd = self._prepare_value_nd(value, indexed_shape) + _internal._slice_assign(self, value_nd, begin, end, step, out=self) def _set_nd_advanced_indexing(self, key, value): """This function is called by __setitem__ when key is an advanced index.""" @@ -789,69 +771,18 @@ def _set_nd_advanced_indexing(self, key, value): shape=self.shape, out=self) def _get_nd_basic_indexing(self, key): - """This function is called when key is a slice, or an integer, - or a tuple of slices or integers""" - shape = self.shape - if isinstance(key, integer_types): - if key > shape[0] - 1: - raise IndexError( - 'index {} is out of bounds for axis 0 with size {}'.format( - key, shape[0])) - return self._at(key) - elif isinstance(key, py_slice): - if key.step is not None and key.step != 1: - if key.step == 0: - raise ValueError("slice step cannot be zero") - return op.slice(self, begin=(key.start,), end=(key.stop,), step=(key.step,)) - elif key.start is not None or key.stop is not None: - return self._slice(key.start, key.stop) - else: - return self - - if not isinstance(key, tuple): - raise ValueError('index=%s must be a slice, or an ineger, or a tuple' - ' of slices and integers to use basic indexing, received type=%s' - % (str(key), str(type(key)))) - assert len(key) != 0, 'basic index cannot be an empty tuple' - begin = [] - end = [] - step = [] - kept_axes = [] # axes where slice_i is a slice - i = -1 - for i, slice_i in enumerate(key): - if isinstance(slice_i, integer_types): - begin.append(slice_i) - end.append(slice_i + 1 if slice_i != -1 else self.shape[i]) - step.append(1) - elif isinstance(slice_i, py_slice): - if slice_i.step == 0: - raise ValueError('basic index=%s cannot have slice=%s with step = 0' - % (str(key), str(slice_i))) - begin.append(slice_i.start) - end.append(slice_i.stop) - step.append(slice_i.step) - kept_axes.append(i) - else: - raise ValueError('basic_indexing does not support slicing with ' - 'index=%s of type=%s.' % (str(slice_i), str(type(slice_i)))) - kept_axes.extend(range(i + 1, len(shape))) - sliced_nd = op.slice(self, begin, end, step) - if len(kept_axes) == len(shape): - return sliced_nd - # squeeze sliced_shape to remove the axes indexed by integers - oshape = [] - sliced_shape = sliced_nd.shape - for axis in kept_axes: - oshape.append(sliced_shape[axis]) - # if key is a tuple of integers, still need to keep 1 dim - # while in Numpy, the output will become an value instead of an ndarray - if len(oshape) == 0: - oshape.append(1) - oshape = tuple(oshape) - assert np.prod(oshape) == np.prod(sliced_shape), 'oshape=%s has different size'\ - ' than sliced_shape=%s'\ - % (oshape, sliced_shape) - return sliced_nd.reshape(oshape) + """This function indexes ``self`` with a tuple of ``slice`` objects only.""" + if len(key) == 0: + raise ValueError('basic index cannot be an empty tuple') + for idx in key: + if not isinstance(idx, py_slice): + raise RuntimeError( + '`key` may only contain `slice` objects in the basic ' + 'implementation, indexing, got object of type {}. ' + 'This is a bug, please report it!' + ''.format(type(idx))) + begin, end, step = _basic_indexing_tuple_to_begin_end_step(key, self.shape) + return op.slice(self, begin, end, step) def _get_nd_advanced_indexing(self, key): """Get item when key is a tuple of any objects of the following types: @@ -2394,15 +2325,14 @@ def to_dlpack_for_write(self): return to_dlpack_for_write(self) -def _expand_none_and_ellipsis(arr, idcs, drop_none=False): - orig_idcs = idcs +def _expand_index_implicit_axes(idcs, shape): + """Make implicit axes explicit by adding ``slice(None)``.""" + if not isinstance(idcs, tuple): # NOTE: `arr[idcs]` should be equivalent to `arr[(idcs,)]`, but in # some cases this is not true currently, e.g., with `slice` objects. idcs = (idcs,) - ## Step 1: Expand ellipsis to an appropriate number of `slice(None)`. - # We need to loop explicitly since tuple functions like `index()` or # `count()` use `==` internally, which doesn't play well with fancy # indexing. @@ -2424,44 +2354,64 @@ def _expand_none_and_ellipsis(arr, idcs, drop_none=False): nonell_idcs = tuple(nonell_idcs) - # If neither `None` nor `Ellipsis` are present, we have nothing to do and - # just return the input. - if ell_idx is None and num_none == 0: - return arr, orig_idcs - if ell_idx is None: - # This handles the case of "too few" indices, e.g., `nd.zeros((2, 3))[0]`. - ell_idx = len(arr.shape) + # This handles the case of "too few" indices, e.g., `nd.zeros((2, 3))[0]`, + # where the ellipsis is implicitly after the last entry. + ell_idx = len(shape) + + ell_ndim = len(shape) + num_none - len(nonell_idcs) + expanded_idcs = (nonell_idcs[:ell_idx] + + (slice(None),) * ell_ndim + + nonell_idcs[ell_idx:]) + + return expanded_idcs + +def _convert_index_int_to_slice(idcs): + """Return the converted indexing tuple and the integer axes.""" + int_axes = [] + conv_idcs = [] + for ax, idx in enumerate(idcs): + if isinstance(idx, integer_types): + if idx == -1: + # Avoid slice(-1, 0) + conv_idcs.append(slice(-1, None)) + else: + conv_idcs.append(slice(idx, idx + 1)) + int_axes.append(ax) + else: + conv_idcs.append(idx) - ell_ndim = len(arr.shape) + num_none - len(nonell_idcs) - new_idcs = (nonell_idcs[:ell_idx] + - (slice(None),) * ell_ndim + - nonell_idcs[ell_idx:]) + return tuple(conv_idcs), tuple(int_axes) - # If we decide to drop `None`, we return here and do not reshape. - if drop_none: - slc_idcs = tuple(idx for idx in new_idcs if idx is not None) - return arr, slc_idcs - ## Step 2: Make new axes where `None` is in the index tuple. +def _new_axes_after_indexing(idcs): + """Return indices of new axes after slicing (potentially fancy). - # We use `reshape()` with entries equal to 1 at new axes. - tmp_shape = [] + Integer entries are considered as collapsed axes after indexing, i.e., + the input to this function should be the non-int-to-slice-converted + indices. + """ ax = 0 - for idx in new_idcs: + new_axes = [] + for idx in idcs: if idx is None: - tmp_shape.append(1) + new_axes.append(ax) + ax += 1 + elif hasattr(idx, "ndim"): + ax += idx.ndim + elif isinstance(idx, integer_types): + pass else: - tmp_shape.append(arr.shape[ax]) ax += 1 - reshp_arr = arr.reshape(tmp_shape) + return tuple(new_axes) + - # Now that the `None` axes have size 1, we're done with them and - # replace `None` by `slice(None)` for the final indexing. - slc_idcs = tuple(slice(None) if idx is None else idx - for idx in new_idcs) - return reshp_arr, slc_idcs +def _basic_indexing_tuple_to_begin_end_step(idcs, shape): + """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" + not_none_idcs = [idx for idx in idcs if idx is not None] + sss_list = [slc.indices(n) for slc, n in zip(not_none_idcs, shape)] + return tuple(zip(*sss_list)) def _get_indexing_dispatch_code(key): From 3b9a3b1b0d0bd9b48fb7d70860bc18a64d96c3ce Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 15 Nov 2018 00:43:21 +0100 Subject: [PATCH 07/47] Fix typo in error message of SetSliceOpOutputDimSize --- src/operator/tensor/matrix_op-inl.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/operator/tensor/matrix_op-inl.h b/src/operator/tensor/matrix_op-inl.h index cd98cb020c6b..128fa0e75aa1 100644 --- a/src/operator/tensor/matrix_op-inl.h +++ b/src/operator/tensor/matrix_op-inl.h @@ -734,11 +734,11 @@ inline void SetSliceOpOutputDimSize(const index_t i, const int b, mxnet::TShape* oshape) { if (e != b) { if (s > 0) { - CHECK_LT(b, e) << "slicing with begin=[" << i << "]=" << b << ", end[" << i << "]=" + CHECK_LT(b, e) << "slicing with begin[" << i << "]=" << b << ", end[" << i << "]=" << e << ", and step[" << i << "]=" << s << " is invalid"; (*oshape)[i] = (e - b - 1) / s + 1; } else { - CHECK_LT(e, b) << "slicing with begin=[" << i << "]=" << b << ", end[" << i << "]=" + CHECK_LT(e, b) << "slicing with begin[" << i << "]=" << b << ", end[" << i << "]=" << e << ", and step[" << i << "]=" << s << " is invalid"; (*oshape)[i] = (b - e - 1) / (-s) + 1; } From 6911806587d9701e8eafb297e4cfdb00e6a3072e Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 15 Nov 2018 23:31:35 +0100 Subject: [PATCH 08/47] Fix setting of array with integer indices --- python/mxnet/ndarray/ndarray.py | 109 ++++++++++++++++++-------- tests/python/unittest/test_ndarray.py | 16 ++-- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 18ab7a929876..f9b335ed5e16 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -445,8 +445,7 @@ def __setitem__(self, key, value): [ 6., 0., 4.]], dtype=float32) """ key = _expand_index_implicit_axes(key, self.shape) - conv_key, _ = _convert_index_int_to_slice(key) - slc_key = tuple(idx for idx in conv_key if idx is not None) + slc_key = tuple(idx for idx in key if idx is not None) indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) @@ -695,11 +694,17 @@ def _is_advanced_index(index): indices = broadcasted_indices return op.stack(*indices) - def _prepare_value_nd(self, value, vshape): - """Given value and vshape, create an `NDArray` from value with the same - context and dtype as the current one and broadcast it to vshape.""" + def _prepare_value_nd(self, value, new_axes, bcast_shape): + """Return a broadcast `NDArray` with same context and dtype as ``self``. + + Before broadcasting, ``new_axes`` of length 1 will be added to + ``value``. This is done in contrast to blindly reshaping based on + ``bcast_shape``, since the latter would silently ignore wrongly shaped + ``value`` arrays, e.g. ``nd.zeros((2, 3))[:, :1] = nd.ones(2)``. + """ if isinstance(value, numeric_types): - value_nd = full(vshape, value, ctx=self.context, dtype=self.dtype) + value_nd = full(bcast_shape, value, ctx=self.context, dtype=self.dtype) + new_axes = [] # ignore for scalar elif isinstance(value, NDArray): value_nd = value.as_in_context(self.context) if value_nd.dtype != self.dtype: @@ -711,31 +716,42 @@ def _prepare_value_nd(self, value, vshape): raise TypeError('NDArray does not support assignment with non-array-like ' 'object {} of type {}'.format(value, type(value))) - if value_nd.shape != vshape: - value_nd = value_nd.broadcast_to(vshape) + value_nd = _add_empty_axes(value_nd, new_axes) + if value_nd.shape != bcast_shape: + value_nd = value_nd.broadcast_to(bcast_shape) return value_nd def _set_nd_basic_indexing(self, key, value): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" - if len(key) != self.ndim: - raise ValueError( - 'length of `key` must be equal to `self.ndim`, but {} != {}.' - 'This is a bug, please report it!' - ''.format(len(key), self.ndim)) + if len(key) < self.ndim: + raise RuntimeError( + 'too few indices in basic indexing: expected `ndim` ({}) ' + 'but got {}. This is a bug, please report it!' + ''.format(self.ndim, len(key)) + ) + elif len(key) > self.ndim: + raise IndexError( + 'too many indices ({}) for array with {} dimensions' + ''.format(len(key), self.ndim) + ) for idx in key: - if not isinstance(idx, py_slice): + if not isinstance(idx, (py_slice, integer_types)): raise RuntimeError( - '`key` may only contain `slice` objects in the basic ' - 'implementation, indexing, got object of type {}. ' + '`key` may only contain `slice` or integer objects in the ' + 'basic implementation, got object of type {}. ' 'This is a bug, please report it!' ''.format(type(idx))) begin, end, step = _basic_indexing_tuple_to_begin_end_step(key, self.shape) + int_axes = [ax for ax in range(len(key)) + if isinstance(key[ax], integer_types)] indexed_shape = tuple((e - b) // s for b, e, s in zip(begin, end, step)) if indexed_shape == self.shape: # Easy case, overwrite whole array. if isinstance(value, NDArray): if value.handle is not self.handle: + # Need to do this before `broadcast_to`. + value = _add_empty_axes(value, int_axes) if value.shape != self.shape: value = value.broadcast_to(self.shape) value.copyto(self) @@ -745,12 +761,15 @@ def _set_nd_basic_indexing(self, key, value): dtype=self.dtype, out=self ) elif isinstance(value, (np.ndarray, np.generic)): + value = _add_empty_axes(value, int_axes) if isinstance(value, np.generic) or value.shape != self.shape: value = np.broadcast_to(value, self.shape) self._sync_copyfrom(value) else: # Other array-like - value_nd = self._prepare_value_nd(value, self.shape) + value_nd = self._prepare_value_nd( + value, new_axes=int_axes, bcast_shape=self.shape + ) value_nd.copyto(self) elif isinstance(value, numeric_types): @@ -759,7 +778,9 @@ def _set_nd_basic_indexing(self, key, value): ) else: - value_nd = self._prepare_value_nd(value, indexed_shape) + value_nd = self._prepare_value_nd( + value, new_axes=int_axes, bcast_shape=indexed_shape + ) _internal._slice_assign(self, value_nd, begin, end, step, out=self) def _set_nd_advanced_indexing(self, key, value): @@ -2343,8 +2364,8 @@ def _expand_index_implicit_axes(idcs, shape): if idx is Ellipsis: if ell_idx is not None: raise IndexError( - "Cannot use more than one ellipsis (`...`) " - "for indexing") + 'Cannot use more than one ellipsis (`...`) ' + 'for indexing') else: ell_idx = i else: @@ -2366,17 +2387,23 @@ def _expand_index_implicit_axes(idcs, shape): return expanded_idcs + +def _int_to_slice(idx): + """Return a slice that indexes the same entries as a single int.""" + if idx == -1: + # Avoid slice(-1, 0) + return slice(-1, None) + else: + return slice(idx, idx + 1) + + def _convert_index_int_to_slice(idcs): """Return the converted indexing tuple and the integer axes.""" int_axes = [] conv_idcs = [] for ax, idx in enumerate(idcs): if isinstance(idx, integer_types): - if idx == -1: - # Avoid slice(-1, 0) - conv_idcs.append(slice(-1, None)) - else: - conv_idcs.append(slice(idx, idx + 1)) + conv_idcs.append(_int_to_slice(idx)) int_axes.append(ax) else: conv_idcs.append(idx) @@ -2397,7 +2424,7 @@ def _new_axes_after_indexing(idcs): if idx is None: new_axes.append(ax) ax += 1 - elif hasattr(idx, "ndim"): + elif hasattr(idx, 'ndim'): ax += idx.ndim elif isinstance(idx, integer_types): pass @@ -2407,10 +2434,23 @@ def _new_axes_after_indexing(idcs): return tuple(new_axes) +def _add_empty_axes(arr, axes): + """Reshape array so that it has axes of length 1 at ``axes``.""" + if not axes: + return arr + + shape = list(arr.shape) + for ax in axes: + shape.insert(ax, 1) + return arr.reshape(shape) + + def _basic_indexing_tuple_to_begin_end_step(idcs, shape): """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" - not_none_idcs = [idx for idx in idcs if idx is not None] - sss_list = [slc.indices(n) for slc, n in zip(not_none_idcs, shape)] + idcs = [idx for idx in idcs if idx is not None] + idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) + for idx in idcs] + sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] return tuple(zip(*sss_list)) @@ -2639,9 +2679,16 @@ def array(source_array, ctx=None, dtype=None): source_array = np.array(source_array, dtype=dtype) except: raise TypeError('source_array must be array like object') - arr = empty(source_array.shape, ctx, dtype) - arr[:] = source_array - return arr + + if source_array.shape == (): + # In this case we can't assign, so we need to go through an auxiliary array + arr = empty((1,), ctx, dtype) + arr[:] = source_array + return arr.reshape(()) + else: + arr = empty(source_array.shape, ctx, dtype) + arr[:] = source_array + return arr def moveaxis(tensor, source, destination): diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 89bce4c8c2b6..a855cf24ae2f 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -20,7 +20,7 @@ from distutils.version import LooseVersion import os import pickle as pkl -from nose.tools import raises +from nose.tools import assert_raises, raises from common import with_seed, assertRaises, TemporaryDirectory from mxnet.test_utils import almost_equal from mxnet.test_utils import assert_almost_equal, assert_exception @@ -140,13 +140,13 @@ def test_ndarray_setitem(): x_np[:, -3:-1, -2:-1] = 1 assert same(x.asnumpy(), x_np) - # numpy assignment for empty axis - for trivial_shape in [(), (1,), (1, 1), (1, 1, 1)]: - if trivial_shape == tuple(): - with mx.np_shape(): - x = mx.nd.zeros(trivial_shape) - else: - x = mx.nd.zeros(trivial_shape) + # Scalar array, no assignment allowed + x = mx.nd.zeros(()) + with assert_raises(IndexError): + x[:] = 1 + + for trivial_shape in [(1,), (1, 1), (1, 1, 1)]: + x = mx.nd.zeros(trivial_shape) x[:] = np.ones(trivial_shape) x_np = np.ones(trivial_shape, dtype=x.dtype) assert x.shape == trivial_shape From 378a1de1429c09e1aeb11c0a5539d69f66b88292 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sat, 17 Nov 2018 01:34:41 +0100 Subject: [PATCH 09/47] Fix basic __setitem__ for all test cases --- python/mxnet/ndarray/ndarray.py | 88 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index f9b335ed5e16..2fe31e720a58 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -544,6 +544,10 @@ def __getitem__(self, key): for new_ax in new_axes: final_shape.insert(new_ax, 1) + if int_axes == tuple(range(self.ndim)): + # Override for single element indexing + final_shape = (1,) + return sliced.reshape(final_shape) # pylint: enable=line-too-long @@ -716,7 +720,13 @@ def _prepare_value_nd(self, value, new_axes, bcast_shape): raise TypeError('NDArray does not support assignment with non-array-like ' 'object {} of type {}'.format(value, type(value))) - value_nd = _add_empty_axes(value_nd, new_axes) + # First reshape `value_nd` to a new shape that incorporates existing + # axes, new axes and broadcasting axes in the right way. + tmp_shape = _new_shape_for_bcast( + value_nd.shape, target_ndim=len(bcast_shape), new_axes=new_axes + ) + value_nd = value_nd.reshape(tmp_shape) + if value_nd.shape != bcast_shape: value_nd = value_nd.broadcast_to(bcast_shape) return value_nd @@ -741,30 +751,50 @@ def _set_nd_basic_indexing(self, key, value): 'basic implementation, got object of type {}. ' 'This is a bug, please report it!' ''.format(type(idx))) - begin, end, step = _basic_indexing_tuple_to_begin_end_step(key, self.shape) int_axes = [ax for ax in range(len(key)) if isinstance(key[ax], integer_types)] - indexed_shape = tuple((e - b) // s for b, e, s in zip(begin, end, step)) - - if indexed_shape == self.shape: + begin, end, step = _basic_indexing_tuple_to_begin_end_step( + key, self.shape, keep_none=False + ) + indexed_shape = tuple((abs(e - b) - 1) // abs(s) + 1 + for b, e, s in zip(begin, end, step)) + can_assign_directly = ( + (indexed_shape == self.shape) and all(s > 0 for s in step) + ) + begin, end, step = _basic_indexing_tuple_to_begin_end_step( + key, self.shape, keep_none=True + ) + + if can_assign_directly: # Easy case, overwrite whole array. if isinstance(value, NDArray): if value.handle is not self.handle: # Need to do this before `broadcast_to`. - value = _add_empty_axes(value, int_axes) + tmp_shape = _new_shape_for_bcast( + value.shape, target_ndim=self.ndim, new_axes=int_axes + ) + value = value.reshape(tmp_shape) + if value.shape != self.shape: value = value.broadcast_to(self.shape) value.copyto(self) + elif isinstance(value, numeric_types): _internal._full( shape=self.shape, value=float(value), ctx=self.context, dtype=self.dtype, out=self ) + elif isinstance(value, (np.ndarray, np.generic)): - value = _add_empty_axes(value, int_axes) + tmp_shape = _new_shape_for_bcast( + value.shape, target_ndim=self.ndim, new_axes=int_axes + ) + value = value.reshape(tmp_shape) + if isinstance(value, np.generic) or value.shape != self.shape: value = np.broadcast_to(value, self.shape) self._sync_copyfrom(value) + else: # Other array-like value_nd = self._prepare_value_nd( @@ -802,7 +832,17 @@ def _get_nd_basic_indexing(self, key): 'implementation, indexing, got object of type {}. ' 'This is a bug, please report it!' ''.format(type(idx))) - begin, end, step = _basic_indexing_tuple_to_begin_end_step(key, self.shape) + begin, _, _ = _basic_indexing_tuple_to_begin_end_step( + key, self.shape, keep_none=False + ) + for ax, (b, n) in enumerate(zip(begin, self.shape)): + if b >= n: + raise IndexError( + 'index {} is out of bounds for axis {} with size {}' + ''.format(b, ax, n)) + begin, end, step = _basic_indexing_tuple_to_begin_end_step( + key, self.shape, keep_none=True + ) return op.slice(self, begin, end, step) def _get_nd_advanced_indexing(self, key): @@ -2434,23 +2474,35 @@ def _new_axes_after_indexing(idcs): return tuple(new_axes) -def _add_empty_axes(arr, axes): - """Reshape array so that it has axes of length 1 at ``axes``.""" - if not axes: - return arr +def _new_shape_for_bcast(shape, target_ndim, new_axes): + """Return shape with added axes for broadcasting in ``target_ndim`` dimensions.""" + new_shape = [None] * target_ndim + for new_ax in new_axes: + new_shape[new_ax] = 1 - shape = list(arr.shape) - for ax in axes: - shape.insert(ax, 1) - return arr.reshape(shape) + # Replace `None` from the right with `shape` entries from the right as + # long as possible, thereafter with 1. + ax_s = 1 + for ax in range(1, target_ndim + 1): + if new_shape[-ax] is None: + try: + new_shape[-ax] = shape[-ax_s] + ax_s += 1 + except IndexError: + new_shape[-ax] = 1 + + return new_shape -def _basic_indexing_tuple_to_begin_end_step(idcs, shape): +def _basic_indexing_tuple_to_begin_end_step(idcs, shape, keep_none=True): """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" idcs = [idx for idx in idcs if idx is not None] idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) for idx in idcs] - sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] + if keep_none: + sss_list = [(slc.start, slc.stop, slc.step) for slc in idcs] + else: + sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] return tuple(zip(*sss_list)) From 8dada74dc730e4b71828604f85a9e82a4bbd80e6 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Mon, 26 Nov 2018 21:19:32 +0100 Subject: [PATCH 10/47] WIP: fixing advanced indexing --- python/mxnet/ndarray/ndarray.py | 402 ++++++++++++++++++++------------ 1 file changed, 250 insertions(+), 152 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 2fe31e720a58..d21505dbe164 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -392,17 +392,19 @@ def __setstate__(self, state): else: self.handle = None - # pylint: disable=line-too-long def __setitem__(self, key, value): """x.__setitem__(i, y) <=> x[i]=y - Sets value to self[key]. This functions supports advanced indexing defined in the following reference with - some restrictions. + Sets ``self[key]`` to ``value``. - https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html#combining-advanced-and-basic-indexing + This functions supports advanced indexing as defined in `the NumPy + advanced indexing documentation + `_, + with some restrictions: - - If key is a list type, only a list of integers is supported, e.g. key=[1, 2] is supported, - while not for key=[[1, 2]]. + - If ``key`` is of type ``list``, only a list of integers, e.g. + ``key=[1, 2]``, is supported. Nested lists like ``key=[[1, 2]]`` are + not supported. - Boolean array indexing is not supported. Parameters @@ -444,10 +446,22 @@ def __setitem__(self, key, value): array([[ 6., 5., 5.], [ 6., 0., 4.]], dtype=float32) """ - key = _expand_index_implicit_axes(key, self.shape) + key = _indexing_key_expand_implicit_axes(key, self.shape) slc_key = tuple(idx for idx in key if idx is not None) indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) + if len(slc_key) < self.ndim: + raise RuntimeError( + 'too few indices after normalization: expected `ndim` ({}) ' + 'but got {}. This is a bug, please report it!' + ''.format(self.ndim, len(slc_key)) + ) + elif len(slc_key) > self.ndim: + raise IndexError( + 'too many indices ({}) for array with {} dimensions' + ''.format(len(slc_key), self.ndim) + ) + indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: self._set_nd_basic_indexing(slc_key, value) @@ -456,22 +470,28 @@ def __setitem__(self, key, value): else: raise ValueError( 'Indexing NDArray with index {} of type {} is not supported' - ''.format(key, type(key))) - # pylint: enable=line-too-long + ''.format(key, type(key)) + ) - # pylint: disable=line-too-long def __getitem__(self, key): """x.__getitem__(i) <=> x[i] - Returns a sliced view of this array if the elements fetched are contiguous in memory; - otherwise, returns a newly created NDArray. - This functions supports advanced indexing defined in the following reference with - some restrictions. + Returns the subarray ``self[key]``. + + For basic indexing, i.e., if ``key`` consists only of integers, + ``slice``, ``Ellipsis`` (``...``) and ``None``, a mutable view is + returned that shares memory with this array if the accessed portion is + contiguous in memory. + Otherwise, a newly created ``NDArray`` is returned. - https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html#combining-advanced-and-basic-indexing + This functions supports advanced indexing as defined in `the NumPy + advanced indexing documentation + `_, + with some restrictions: - - If key is a list type, only a list of integers is supported, e.g. key=[1, 2] is supported, - while not for key=[[1, 2]]. + - If ``key`` is of type ``list``, only a list of integers, e.g. + ``key=[1, 2]``, is supported. Nested lists like ``key=[[1, 2]]`` are + not supported. - Boolean array indexing is not supported. Parameters @@ -524,53 +544,49 @@ def __getitem__(self, key): >>> x[None, ..., None].shape (1, 2, 2, 2, 1) """ - key = _expand_index_implicit_axes(key, self.shape) - conv_key, int_axes = _convert_index_int_to_slice(key) - new_axes = _new_axes_after_indexing(key) - slc_key = tuple(idx for idx in conv_key if idx is not None) - indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) + key = _indexing_key_expand_implicit_axes(key, self.shape) + if len(key) == 0: + raise ValueError('indexing key cannot be an empty tuple') + indexing_dispatch_code = _get_indexing_dispatch_code(key) if indexing_dispatch_code == _NDARRAY_BASIC_INDEXING: - sliced = self._get_nd_basic_indexing(slc_key) + return self._get_nd_basic_indexing(key) elif indexing_dispatch_code == _NDARRAY_ADVANCED_INDEXING: - sliced = self._get_nd_advanced_indexing(key) + return self._get_nd_advanced_indexing(key) else: - raise ValueError( - 'Indexing NDArray with index {} of type {} is not supported' - ''.format(key, type(key))) - - final_shape = [sliced.shape[i] for i in range(sliced.ndim) - if i not in int_axes] - for new_ax in new_axes: - final_shape.insert(new_ax, 1) - - if int_axes == tuple(range(self.ndim)): - # Override for single element indexing - final_shape = (1,) + raise RuntimeError - return sliced.reshape(final_shape) + def _get_index_nd(self, key): + """Return an index array for use in `scatter_nd` and `gather_nd`.""" + key_nd = tuple(idx for idx in key if idx is not None) + if len(key_nd) < self.ndim: + raise RuntimeError( + 'too few indices after normalization: expected `ndim` ({}) ' + 'but got {}. This is a bug, please report it!' + ''.format(self.ndim, len(key_nd)) + ) + elif len(key_nd) > self.ndim: + raise IndexError( + 'too many indices ({}) for array with {} dimensions' + ''.format(len(key_nd), self.ndim) + ) + ndim = len(key_nd) - # pylint: enable=line-too-long + conv_key = _advanced_indexing_convert_to_arrays( + key_nd, self.shape, self.context + ) - def _get_index_nd(self, key): - """Returns an index array for use in scatter_nd and gather_nd.""" - def _is_advanced_index(index): - """The definition of advanced index here includes integers as well, while - integers are considered as basic index type when the key contains only - slices and integers.""" - return not isinstance(index, py_slice) + # TODO: continue here - if isinstance(key, (NDArray, np.ndarray, list, integer_types, py_slice)): - key = (key,) + adv_axes = [ax for ax in range(ndim) if _is_advanced_index(key_nd[ax])] + adv_idcs = [conv_key[ax] for ax in adv_axes] + adv_block_shape = _broadcast_shapes(adv_idcs) + adv_block_extra_ndim = len(adv_block_shape) - len(adv_axes) - assert isinstance(key, tuple),\ - 'index=%s must be a NDArray, or np.ndarray, or list, or tuple ' \ - ' type to use advanced indexing, received type=%s' % (str(key), str(type(key))) + bas_axes = [ax for ax in range(ndim) if not _is_advanced_index(key_nd[ax])] + bas_shape = sum((conv_key[ax].shape for ax in bas_axes), ()) - assert len(key) > 0, "Cannot slice with empty indices" shape = self.shape - assert len(shape) >= len(key),\ - "Slicing dimensions exceeds array dimensions, %d vs %d" % (len(key), len(shape)) indices = [] dtype = 'int32' # index data type passed to gather_nd op need_broadcast = (len(key) != 1) @@ -612,22 +628,22 @@ def _is_advanced_index(index): if len(advanced_indices) == 0: raise ValueError('Advanced index tuple must contain at least one of the following types:' ' list, tuple, NDArray, np.ndarray, integer, received index=%s' % key) - # determine the output array's shape by checking whether advanced_indices are all adjacent + # Determine the output array's shape by checking whether advanced_indices are all adjacent # or separated by slices advanced_indices_adjacent = True - for i in range(0, len(advanced_indices)-1): - if advanced_indices[i] + 1 != advanced_indices[i+1]: + for i in range(0, len(advanced_indices) - 1): + if advanced_indices[i] + 1 != advanced_indices[i + 1]: advanced_indices_adjacent = False break - index_bshape_list = [] # index broadcasted shape + index_bshape_list = [] # index broadcast shape if advanced_indices_adjacent: for i in range(0, advanced_indices[0]): index_bshape_list.extend(indices[i].shape) if not need_broadcast and indices[i].shape != advanced_index_bshape: need_broadcast = True index_bshape_list.extend(advanced_index_bshape) - for i in range(advanced_indices[-1]+1, len(indices)): + for i in range(advanced_indices[-1] + 1, len(indices)): if not need_broadcast and indices[i].shape != advanced_index_bshape: need_broadcast = True index_bshape_list.extend(indices[i].shape) @@ -653,7 +669,7 @@ def _is_advanced_index(index): # broadcast the reshaped array to (1, 2, 4, 3); to broadcast key[1], we first # reshape it into (1, 2, 1, 1), then broadcast the reshaped array to (1, 2, 4, 3). if need_broadcast: - broadcasted_indices = [] + broadcast_indices = [] idx_rshape = [1] * len(index_bshape) if advanced_indices_adjacent: advanced_index_bshape_start = advanced_indices[0] # start index of advanced_index_bshape in index_shape @@ -674,7 +690,7 @@ def _is_advanced_index(index): raise ValueError('basic index i=%d cannot be between advanced index i=%d and i=%d' % (i, advanced_indices[0], advanced_indices[-1])) # broadcast current index to the final shape - broadcasted_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) + broadcast_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) # reset idx_rshape to ones for j, _ in enumerate(idx_rshape): idx_rshape[j] = 1 @@ -690,12 +706,12 @@ def _is_advanced_index(index): idx_rshape[basic_index_offset] = indices[i].shape[0] basic_index_offset += 1 # broadcast current index to the final shape - broadcasted_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) + broadcast_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) # reset idx_rshape to ones for j, _ in enumerate(idx_rshape): idx_rshape[j] = 1 - indices = broadcasted_indices + indices = broadcast_indices return op.stack(*indices) def _prepare_value_nd(self, value, new_axes, bcast_shape): @@ -722,7 +738,7 @@ def _prepare_value_nd(self, value, new_axes, bcast_shape): # First reshape `value_nd` to a new shape that incorporates existing # axes, new axes and broadcasting axes in the right way. - tmp_shape = _new_shape_for_bcast( + tmp_shape = _shape_for_bcast( value_nd.shape, target_ndim=len(bcast_shape), new_axes=new_axes ) value_nd = value_nd.reshape(tmp_shape) @@ -733,17 +749,6 @@ def _prepare_value_nd(self, value, new_axes, bcast_shape): def _set_nd_basic_indexing(self, key, value): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" - if len(key) < self.ndim: - raise RuntimeError( - 'too few indices in basic indexing: expected `ndim` ({}) ' - 'but got {}. This is a bug, please report it!' - ''.format(self.ndim, len(key)) - ) - elif len(key) > self.ndim: - raise IndexError( - 'too many indices ({}) for array with {} dimensions' - ''.format(len(key), self.ndim) - ) for idx in key: if not isinstance(idx, (py_slice, integer_types)): raise RuntimeError( @@ -753,15 +758,16 @@ def _set_nd_basic_indexing(self, key, value): ''.format(type(idx))) int_axes = [ax for ax in range(len(key)) if isinstance(key[ax], integer_types)] - begin, end, step = _basic_indexing_tuple_to_begin_end_step( + begin, end, step = _basic_indexing_key_to_begin_end_step( key, self.shape, keep_none=False ) - indexed_shape = tuple((abs(e - b) - 1) // abs(s) + 1 - for b, e, s in zip(begin, end, step)) + indexed_shape = tuple( + _get_dim_size(b, e, s) for b, e, s in zip(begin, end, step) + ) can_assign_directly = ( (indexed_shape == self.shape) and all(s > 0 for s in step) ) - begin, end, step = _basic_indexing_tuple_to_begin_end_step( + begin, end, step = _basic_indexing_key_to_begin_end_step( key, self.shape, keep_none=True ) @@ -770,7 +776,7 @@ def _set_nd_basic_indexing(self, key, value): if isinstance(value, NDArray): if value.handle is not self.handle: # Need to do this before `broadcast_to`. - tmp_shape = _new_shape_for_bcast( + tmp_shape = _shape_for_bcast( value.shape, target_ndim=self.ndim, new_axes=int_axes ) value = value.reshape(tmp_shape) @@ -786,7 +792,7 @@ def _set_nd_basic_indexing(self, key, value): ) elif isinstance(value, (np.ndarray, np.generic)): - tmp_shape = _new_shape_for_bcast( + tmp_shape = _shape_for_bcast( value.shape, target_ndim=self.ndim, new_axes=int_axes ) value = value.reshape(tmp_shape) @@ -823,27 +829,61 @@ def _set_nd_advanced_indexing(self, key, value): def _get_nd_basic_indexing(self, key): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" - if len(key) == 0: - raise ValueError('basic index cannot be an empty tuple') - for idx in key: + key_nd = tuple(idx for idx in key if idx is not None) + if len(key_nd) < self.ndim: + raise RuntimeError( + 'too few indices after normalization: expected `ndim` ({}) ' + 'but got {}. This is a bug, please report it!' + ''.format(self.ndim, len(key_nd)) + ) + elif len(key_nd) > self.ndim: + raise IndexError( + 'too many indices ({}) for array with {} dimensions' + ''.format(len(key_nd), self.ndim) + ) + + none_axes = [ax for ax in range(len(key)) if key[ax] is None] + slc_key, int_axes = _indexing_key_int_to_slice(key_nd) + new_axes = _axes_after_indexing(none_axes, key_nd, self.ndim) + + # Make sure we don't accidentally have advanced indexing or + # unsupported entries. + for idx in slc_key: if not isinstance(idx, py_slice): raise RuntimeError( - '`key` may only contain `slice` objects in the basic ' - 'implementation, indexing, got object of type {}. ' + 'found object of type {} instead of `slice`. ' 'This is a bug, please report it!' ''.format(type(idx))) - begin, _, _ = _basic_indexing_tuple_to_begin_end_step( - key, self.shape, keep_none=False + + # Check index bounds. Only `begin` is relevant, `end` is automatically + # clipped. + begin, _, _ = _basic_indexing_key_to_begin_end_step( + slc_key, self.shape, keep_none=False ) for ax, (b, n) in enumerate(zip(begin, self.shape)): - if b >= n: + if not -(n + 1) <= b <= n: raise IndexError( 'index {} is out of bounds for axis {} with size {}' ''.format(b, ax, n)) - begin, end, step = _basic_indexing_tuple_to_begin_end_step( - key, self.shape, keep_none=True + + # Do the slicing. + begin, end, step = _basic_indexing_key_to_begin_end_step( + slc_key, self.shape, keep_none=True ) - return op.slice(self, begin, end, step) + # TODO: copy-free indexing if possible + sliced = op.slice(self, begin, end, step) + + # Reshape to final shape due to integer and `None` entries in `key`. + final_shape = [sliced.shape[i] for i in range(sliced.ndim) + if i not in int_axes] + for ax in new_axes: + final_shape.insert(ax, 1) + + if final_shape == []: + # Override for single element indexing + final_shape = [1] + + return sliced.reshape(final_shape) def _get_nd_advanced_indexing(self, key): """Get item when key is a tuple of any objects of the following types: @@ -2386,46 +2426,46 @@ def to_dlpack_for_write(self): return to_dlpack_for_write(self) -def _expand_index_implicit_axes(idcs, shape): +def _indexing_key_expand_implicit_axes(key, shape): """Make implicit axes explicit by adding ``slice(None)``.""" - if not isinstance(idcs, tuple): - # NOTE: `arr[idcs]` should be equivalent to `arr[(idcs,)]`, but in + if not isinstance(key, tuple): + # NOTE: `arr[key]` should be equivalent to `arr[(key,)]`, but in # some cases this is not true currently, e.g., with `slice` objects. - idcs = (idcs,) + key = (key,) # We need to loop explicitly since tuple functions like `index()` or # `count()` use `==` internally, which doesn't play well with fancy # indexing. ell_idx = None num_none = 0 - nonell_idcs = [] - for i, idx in enumerate(idcs): + nonell_key = [] + for i, idx in enumerate(key): if idx is Ellipsis: if ell_idx is not None: raise IndexError( - 'Cannot use more than one ellipsis (`...`) ' - 'for indexing') + 'Cannot use more than one ellipsis (`...`) for indexing' + ) else: ell_idx = i else: if idx is None: num_none += 1 - nonell_idcs.append(idx) + nonell_key.append(idx) - nonell_idcs = tuple(nonell_idcs) + nonell_key = tuple(nonell_key) if ell_idx is None: # This handles the case of "too few" indices, e.g., `nd.zeros((2, 3))[0]`, # where the ellipsis is implicitly after the last entry. - ell_idx = len(shape) + ell_idx = len(nonell_key) - ell_ndim = len(shape) + num_none - len(nonell_idcs) - expanded_idcs = (nonell_idcs[:ell_idx] + + ell_ndim = len(shape) + num_none - len(nonell_key) + expanded_key = (nonell_key[:ell_idx] + (slice(None),) * ell_ndim + - nonell_idcs[ell_idx:]) + nonell_key[ell_idx:]) - return expanded_idcs + return expanded_key def _int_to_slice(idx): @@ -2437,7 +2477,7 @@ def _int_to_slice(idx): return slice(idx, idx + 1) -def _convert_index_int_to_slice(idcs): +def _indexing_key_int_to_slice(idcs): """Return the converted indexing tuple and the integer axes.""" int_axes = [] conv_idcs = [] @@ -2451,31 +2491,33 @@ def _convert_index_int_to_slice(idcs): return tuple(conv_idcs), tuple(int_axes) -def _new_axes_after_indexing(idcs): - """Return indices of new axes after slicing (potentially fancy). +def _axes_after_indexing(axes, key, ndim): + """Return indices of ``axes`` after slicing with ``key``. Integer entries are considered as collapsed axes after indexing, i.e., - the input to this function should be the non-int-to-slice-converted - indices. + ``key`` should not be the result of int-to-slice conversion, but the + original indices. + """ + steps = [0] + [getattr(idx, 'ndim', 1) for idx in key] + for _ in range(len(key), ndim): + steps.append(1) + assert len(steps) == 1 + ndim + cum_steps = np.cumsum(steps) + axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] + axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] + axes_after = tuple(cum_steps[axes_in_bounds]) + oob_offsets = [ax - ndim for ax in axes_out_of_bounds] + axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) + return axes_after + + +def _shape_for_bcast(shape, target_ndim, new_axes): + """Return shape with added axes for broadcasting in ``target_ndim`` dimensions. + + The returned shape has fixed ``1`` entries in locations indexed by ``new_axes``, + and the rest is filled from the back with ``shape`` while possible, after + that with ``1``. """ - ax = 0 - new_axes = [] - for idx in idcs: - if idx is None: - new_axes.append(ax) - ax += 1 - elif hasattr(idx, 'ndim'): - ax += idx.ndim - elif isinstance(idx, integer_types): - pass - else: - ax += 1 - - return tuple(new_axes) - - -def _new_shape_for_bcast(shape, target_ndim, new_axes): - """Return shape with added axes for broadcasting in ``target_ndim`` dimensions.""" new_shape = [None] * target_ndim for new_ax in new_axes: new_shape[new_ax] = 1 @@ -2491,10 +2533,10 @@ def _new_shape_for_bcast(shape, target_ndim, new_axes): except IndexError: new_shape[-ax] = 1 - return new_shape + return tuple(new_shape) -def _basic_indexing_tuple_to_begin_end_step(idcs, shape, keep_none=True): +def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" idcs = [idx for idx in idcs if idx is not None] idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) @@ -2506,30 +2548,78 @@ def _basic_indexing_tuple_to_begin_end_step(idcs, shape, keep_none=True): return tuple(zip(*sss_list)) +def _is_advanced_index(idx): + """Return whether ``idx`` is an advanced index (array-like or integer). + + Note that in contrast to basic indexing, integers are considered advanced + indices in the context of advanced indexing as they participate in + broadcasting. + """ + if isinstance(idx, (NDArray, np.ndarray, integer_types, list, tuple)): + return True + elif isinstance(idx, py_slice) or idx is None: + return False + else: + raise RuntimeError('illegal index type {}'.format(type(idx))) + + +def _advanced_indexing_convert_to_arrays(key, shape, ctx): + """Convert all entries in ``key`` to `NDArray` for advanced indexing. + + The ``key`` is assumed to have the same length as ``shape`` and may not + contain any ``None`` entries. + """ + idx_dtype = 'int32' + converted = [] + for idx, n in zip(key, shape): + + if isinstance(idx, NDArray): + if idx.dtype != idx_dtype: + idx = idx.astype(idx_dtype) + idx = idx.as_in_context(ctx) + + elif isinstance(idx, (np.ndarray, list, tuple)): + idx = array(idx, ctx, idx_dtype) + + elif isinstance(idx, integer_types): + idx = array([idx], ctx, idx_dtype) + + elif isinstance(idx, py_slice): + start, stop, step = idx.indices(n) + idx = arange(start, stop, step, ctx=ctx, dtype=idx_dtype) + + else: + raise RuntimeError('illegal index type {}'.format(type(idx))) + + converted.append(idx) + + return tuple(converted) + + def _get_indexing_dispatch_code(key): """Returns a dispatch code for calling basic or advanced indexing functions.""" - if isinstance(key, (NDArray, np.ndarray)): - return _NDARRAY_ADVANCED_INDEXING - elif isinstance(key, list): - # TODO(junwu): Add support for nested lists besides integer list - for i in key: - if not isinstance(i, integer_types): - raise TypeError('Indexing NDArray only supports a list of integers as index' - ' when key is of list type, received element=%s of type=%s' - % (str(i), str(type(i)))) - return _NDARRAY_ADVANCED_INDEXING - elif isinstance(key, (integer_types, py_slice)): - return _NDARRAY_BASIC_INDEXING - elif isinstance(key, tuple): - for idx in key: - if isinstance(idx, (NDArray, np.ndarray, list, tuple)): - return _NDARRAY_ADVANCED_INDEXING - elif not isinstance(idx, (py_slice, integer_types)): - raise ValueError("NDArray does not support slicing with key %s of type %s." - % (str(idx), str(type(idx)))) + assert isinstance(key, tuple) + + for idx in key: + if isinstance(key, list): + for i in key: + if not isinstance(i, integer_types): + raise TypeError( + 'Indexing NDArray only supports a list of integers as index ' + 'when key is of list type, got entry {} of type {}.' + ''.format(i, type(i)) + ) + + if isinstance(idx, (NDArray, np.ndarray, list, tuple)): + return _NDARRAY_ADVANCED_INDEXING + + elif not (isinstance(idx, (py_slice, integer_types)) or idx is None): + raise ValueError( + 'NDArray does not support slicing with key {} of type {}.' + ''.format(idx), type(idx) + ) + return _NDARRAY_BASIC_INDEXING - else: - return _NDARRAY_UNSUPPORTED_INDEXING def _get_index_range(start, stop, length, step=1): @@ -2619,6 +2709,14 @@ def _get_broadcast_shape(shape1, shape2): return tuple(shape) +def _broadcast_shapes(seq): + """Return the broadcast shape of all advanced indices in ``seq``. + + All entries are assumed to have a ``shape`` property. + """ + return reduce(_get_broadcast_shape, [x.shape for x in seq], ()) + + def onehot_encode(indices, out): """One-hot encoding indices into matrix out. From 95cfc82a51d790d222a161ccb54c553cd03133c4 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Mon, 26 Nov 2018 21:19:52 +0100 Subject: [PATCH 11/47] REMOVE: printing in tests --- tests/python/unittest/test_ndarray.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index a855cf24ae2f..0803105ae059 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1333,6 +1333,7 @@ def test_getitem(np_array, index, is_scalar=False): assert same(np_indexed_array, mx_indexed_array), 'Failed with index=%s' % str(index) def test_setitem(np_array, index, is_scalar): + print("index:", index) def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None): if np_value is not None: np_array[np_index] = np_value @@ -1341,7 +1342,13 @@ def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None) else: np_array[np_index] = mx_value mx_array[mx_index] = mx_value - assert same(np_array, mx_array.asnumpy()) + passed = same(np_array, mx_array.asnumpy()) + if not passed: + not_eq = np_array != mx_array.asnumpy() + print(np.where(not_eq)) + print(np_array[not_eq]) + print(mx_array.asnumpy()[not_eq]) + assert False np_index = index if isinstance(index, mx.nd.NDArray): @@ -1361,22 +1368,30 @@ def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None) # test value is a numeric type assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0)) value_nd = [np.random.randint(low=-10000, high=0)] + print("scalar value, represented as", value_nd) assert_same(np_array, np_index, mx_array, index, value_nd, value_nd[0]) else: indexed_array_shape = np_array[np_index].shape + print("shape without broadcasting:", indexed_array_shape) np_indexed_array = np.random.randint(low=-10000, high=0, size=indexed_array_shape) # test value is a numpy array without broadcast + print("fully-shaped rhs, shape:", np_indexed_array.shape) assert_same(np_array, np_index, mx_array, index, np_indexed_array) # test value is an numeric_type + print("scalar rhs") assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0)) if len(indexed_array_shape) > 1: + print("shape without broadcasting:", indexed_array_shape) # test NDArray with broadcast + print("from NDArray") assert_same(np_array, np_index, mx_array, index, mx.nd.random.uniform(low=-10000, high=0, shape=(indexed_array_shape[-1],))) # test numpy array with broadcast + print("from numpy.ndarray") assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0, size=(indexed_array_shape[-1],))) # test list with broadcast + print("from list") assert_same(np_array, np_index, mx_array, index, [np.random.randint(low=-10000, high=0)] * indexed_array_shape[-1]) From 8e748d7cd56c47c8421282fbee8a0badc0bdc8c2 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 13 Dec 2018 23:54:47 +0100 Subject: [PATCH 12/47] Re-implement advanced indexing with None and Ellipsis --- python/mxnet/ndarray/ndarray.py | 331 ++++++++++++++------------------ 1 file changed, 147 insertions(+), 184 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index d21505dbe164..e3207da984c6 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -556,6 +556,13 @@ def __getitem__(self, key): else: raise RuntimeError + @staticmethod + def _drop_slice_none_at_end(key): + key = list(key) + while isinstance(key[-1], py_slice) and key[-1] == slice(None): + key.pop() + return tuple(key) + def _get_index_nd(self, key): """Return an index array for use in `scatter_nd` and `gather_nd`.""" key_nd = tuple(idx for idx in key if idx is not None) @@ -572,147 +579,68 @@ def _get_index_nd(self, key): ) ndim = len(key_nd) - conv_key = _advanced_indexing_convert_to_arrays( - key_nd, self.shape, self.context + # --- Preparation --- # + + # - Make lists for bookkeeping of advanced indices & axes + # - Drop trailing `slice(None)` entries in `key` for efficiency + # - Determine whether the advanced indices are adjacent in `key` + # - Depending on that, make index permutations to move around indices + + adv_axs = [ax for ax, idx in enumerate(key) if _is_advanced_index(idx)] + adv_axs_nd = [ax for ax, idx in enumerate(key_nd) if _is_advanced_index(idx)] + adv_idcs_are_adjacent = bool(np.all(np.diff(adv_axs) == 1)) + nonadv_axs_nd = [ax for ax in range(ndim) if ax not in adv_axs_nd] + adv_idcs_nd = [key_nd[ax] for ax in adv_axs_nd] + idcs_short = self._drop_slice_none_at_end(key_nd) + dropped_axs = list(range(len(idcs_short), ndim)) + + if adv_idcs_are_adjacent: + # The easy case: the advanced block can stay at its position, and no + # permutation needs to be done (identity permutation) + axs_nd_permut = axs_nd_permut_inv = tuple(range(ndim)) + idcs_permut_short = idcs_short + block_axs_nd = adv_axs_nd + else: + # The more complicated case: during broadcasting, we need to use the + # indices in the *permuted* order, where the advanced block is + # at the beginning, while the final index for `gather_nd` is stacked + # in the *original* order, so that the association of index with + # array axis remains the same. + + # This order is used for broadcasting: advanced block at the beginning + idcs_permut_short = ( + adv_idcs_nd + + [key_nd[ax] for ax in range(ndim) + if ax not in adv_axs_nd and ax not in dropped_axs] + ) + block_axs_nd = list(range(len(adv_axs_nd))) + axs_nd_permut = adv_axs_nd + nonadv_axs_nd + axs_nd_permut_inv = list(np.argsort(axs_nd_permut)) + + # --- Conversion, broadcasting and index stacking --- # + + # - Convert all indices in `key` to arrays: integers to 1-element arrays, + # `slice` objects to arrays with explicit indices + # - Reshape arrays for broadcasting according to their position in the + # *permuted* key + # - Broadcast and stack the indices in the *original* order + + shape_nd_permut = tuple(self.shape[ax] for ax in axs_nd_permut) + converted_idcs_short = [ + _advanced_index_to_array(idx, ax_len, self.context) + for idx, ax_len in zip(idcs_permut_short, shape_nd_permut) + ] + bcast_idcs_permut_short = _broadcast_advanced_indices( + converted_idcs_short, block_axes=block_axs_nd ) + # Undo the permutation to restore the original order + bcast_idcs_short = [ + bcast_idcs_permut_short[ax] + for ax in axs_nd_permut_inv + if axs_nd_permut[ax] not in dropped_axs + ] - # TODO: continue here - - adv_axes = [ax for ax in range(ndim) if _is_advanced_index(key_nd[ax])] - adv_idcs = [conv_key[ax] for ax in adv_axes] - adv_block_shape = _broadcast_shapes(adv_idcs) - adv_block_extra_ndim = len(adv_block_shape) - len(adv_axes) - - bas_axes = [ax for ax in range(ndim) if not _is_advanced_index(key_nd[ax])] - bas_shape = sum((conv_key[ax].shape for ax in bas_axes), ()) - - shape = self.shape - indices = [] - dtype = 'int32' # index data type passed to gather_nd op - need_broadcast = (len(key) != 1) - advanced_indices = [] # include list, NDArray, np.ndarray, integer - basic_indices = [] # include only slices - advanced_index_bshape = None # final advanced index shape - for i, idx_i in enumerate(key): - is_advanced_index = True - if isinstance(idx_i, (np.ndarray, list, tuple)): - idx_i = array(idx_i, ctx=self.context, dtype=dtype) - advanced_indices.append(i) - elif isinstance(idx_i, py_slice): - start, stop, step = _get_index_range(idx_i.start, idx_i.stop, shape[i], idx_i.step) - idx_i = arange(start, stop, step, ctx=self.context, dtype=dtype) - basic_indices.append(i) - is_advanced_index = False - elif isinstance(idx_i, integer_types): - start, stop, step = _get_index_range(idx_i, idx_i+1, shape[i], 1) - idx_i = arange(start, stop, step, ctx=self.context, dtype=dtype) - advanced_indices.append(i) - elif isinstance(idx_i, NDArray): - if dtype != idx_i.dtype: - idx_i = idx_i.astype(dtype) - advanced_indices.append(i) - else: - raise IndexError('Indexing NDArray with index=%s of type=%s is not supported' - % (str(key), str(type(key)))) - if is_advanced_index: - if advanced_index_bshape is None: - advanced_index_bshape = idx_i.shape - elif advanced_index_bshape != idx_i.shape: - need_broadcast = True - advanced_index_bshape = _get_broadcast_shape(advanced_index_bshape, idx_i.shape) - indices.append(idx_i) - - # Get final index shape for gather_nd. See the following reference - # for determining the output array shape. - # https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html#combining-advanced-and-basic-indexing # pylint: disable=line-too-long - if len(advanced_indices) == 0: - raise ValueError('Advanced index tuple must contain at least one of the following types:' - ' list, tuple, NDArray, np.ndarray, integer, received index=%s' % key) - # Determine the output array's shape by checking whether advanced_indices are all adjacent - # or separated by slices - advanced_indices_adjacent = True - for i in range(0, len(advanced_indices) - 1): - if advanced_indices[i] + 1 != advanced_indices[i + 1]: - advanced_indices_adjacent = False - break - - index_bshape_list = [] # index broadcast shape - if advanced_indices_adjacent: - for i in range(0, advanced_indices[0]): - index_bshape_list.extend(indices[i].shape) - if not need_broadcast and indices[i].shape != advanced_index_bshape: - need_broadcast = True - index_bshape_list.extend(advanced_index_bshape) - for i in range(advanced_indices[-1] + 1, len(indices)): - if not need_broadcast and indices[i].shape != advanced_index_bshape: - need_broadcast = True - index_bshape_list.extend(indices[i].shape) - else: - index_bshape_list.extend(advanced_index_bshape) - for i in basic_indices: - index_bshape_list.extend(indices[i].shape) - if not need_broadcast and indices[i].shape != advanced_index_bshape: - need_broadcast = True - index_bshape = tuple(index_bshape_list) - - # Need to broadcast all ndarrays in indices to the final shape. - # For example, suppose an array has shape=(5, 6, 7, 8) and - # key=(slice(1, 5), [[1, 2]], slice(2, 5), [1]). - # Since key[1] and key[3] are two advanced indices here and they are - # separated by basic indices key[0] and key[2], the output shape - # is (1, 2, 4, 3), where the first two elements come from the shape - # that key[1] and key[3] should broadcast to, which is (1, 2), and - # the last two elements come from the shape of two basic indices. - # In order to broadcast all basic and advanced indices to the output shape, - # we need to reshape them based on their axis. For example, to broadcast key[0], - # with shape=(4,), we first need to reshape it into (1, 1, 4, 1), and then - # broadcast the reshaped array to (1, 2, 4, 3); to broadcast key[1], we first - # reshape it into (1, 2, 1, 1), then broadcast the reshaped array to (1, 2, 4, 3). - if need_broadcast: - broadcast_indices = [] - idx_rshape = [1] * len(index_bshape) - if advanced_indices_adjacent: - advanced_index_bshape_start = advanced_indices[0] # start index of advanced_index_bshape in index_shape - advanced_index_bshape_stop = advanced_index_bshape_start + len(advanced_index_bshape) - for i, idx in enumerate(key): - if _is_advanced_index(idx): - k = advanced_index_bshape_stop - # find the reshaped shape for indices[i] - for dim_size in indices[i].shape[::-1]: - k -= 1 - idx_rshape[k] = dim_size - else: - if i < advanced_indices[0]: # slice is on the left side of advanced indices - idx_rshape[i] = indices[i].shape[0] - elif i > advanced_indices[-1]: # slice is on the right side of advanced indices - idx_rshape[i-len(key)] = indices[i].shape[0] - else: - raise ValueError('basic index i=%d cannot be between advanced index i=%d and i=%d' - % (i, advanced_indices[0], advanced_indices[-1])) - # broadcast current index to the final shape - broadcast_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) - # reset idx_rshape to ones - for j, _ in enumerate(idx_rshape): - idx_rshape[j] = 1 - else: - basic_index_offset = len(advanced_index_bshape) - for i, idx in enumerate(key): - if _is_advanced_index(idx): - k = len(advanced_index_bshape) - for dim_size in indices[i].shape[::-1]: - k -= 1 - idx_rshape[k] = dim_size - else: - idx_rshape[basic_index_offset] = indices[i].shape[0] - basic_index_offset += 1 - # broadcast current index to the final shape - broadcast_indices.append(indices[i].reshape(tuple(idx_rshape)).broadcast_to(index_bshape)) - # reset idx_rshape to ones - for j, _ in enumerate(idx_rshape): - idx_rshape[j] = 1 - - indices = broadcast_indices - return op.stack(*indices) + return op.stack(*bcast_idcs_short) def _prepare_value_nd(self, value, new_axes, bcast_shape): """Return a broadcast `NDArray` with same context and dtype as ``self``. @@ -827,6 +755,22 @@ def _set_nd_advanced_indexing(self, key, value): _internal._scatter_set_nd(lhs=self, rhs=value_nd, indices=indices, shape=self.shape, out=self) + @staticmethod + def _axes_after_indexing(axes, key, ndim): + """Return indices of ``axes`` after slicing with ``key``. + + Integer entries are considered as collapsed axes after indexing, i.e., + ``key`` should not be the result of int-to-slice conversion, but the + original indices. + """ + cum_steps = np.arange(ndim + 1) + axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] + axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] + axes_after = tuple(cum_steps[axes_in_bounds]) + oob_offsets = [ax - ndim for ax in axes_out_of_bounds] + axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) + return axes_after + def _get_nd_basic_indexing(self, key): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" key_nd = tuple(idx for idx in key if idx is not None) @@ -844,7 +788,7 @@ def _get_nd_basic_indexing(self, key): none_axes = [ax for ax in range(len(key)) if key[ax] is None] slc_key, int_axes = _indexing_key_int_to_slice(key_nd) - new_axes = _axes_after_indexing(none_axes, key_nd, self.ndim) + new_axes = self._axes_after_indexing(none_axes, key_nd, self.ndim) # Make sure we don't accidentally have advanced indexing or # unsupported entries. @@ -2491,26 +2435,6 @@ def _indexing_key_int_to_slice(idcs): return tuple(conv_idcs), tuple(int_axes) -def _axes_after_indexing(axes, key, ndim): - """Return indices of ``axes`` after slicing with ``key``. - - Integer entries are considered as collapsed axes after indexing, i.e., - ``key`` should not be the result of int-to-slice conversion, but the - original indices. - """ - steps = [0] + [getattr(idx, 'ndim', 1) for idx in key] - for _ in range(len(key), ndim): - steps.append(1) - assert len(steps) == 1 + ndim - cum_steps = np.cumsum(steps) - axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] - axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] - axes_after = tuple(cum_steps[axes_in_bounds]) - oob_offsets = [ax - ndim for ax in axes_out_of_bounds] - axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) - return axes_after - - def _shape_for_bcast(shape, target_ndim, new_axes): """Return shape with added axes for broadcasting in ``target_ndim`` dimensions. @@ -2563,37 +2487,76 @@ def _is_advanced_index(idx): raise RuntimeError('illegal index type {}'.format(type(idx))) -def _advanced_indexing_convert_to_arrays(key, shape, ctx): - """Convert all entries in ``key`` to `NDArray` for advanced indexing. +def _advanced_index_to_array(idx, ax_len, ctx): + """Convert ``idx`` to `NDArray` for advanced indexing. - The ``key`` is assumed to have the same length as ``shape`` and may not - contain any ``None`` entries. + The ``ax_len`` is used to convert `slice` objects to integer arrays. """ idx_dtype = 'int32' - converted = [] - for idx, n in zip(key, shape): + if isinstance(idx, NDArray): + if idx.dtype != idx_dtype: + idx = idx.astype(idx_dtype) + return idx.as_in_context(ctx) - if isinstance(idx, NDArray): - if idx.dtype != idx_dtype: - idx = idx.astype(idx_dtype) - idx = idx.as_in_context(ctx) + elif isinstance(idx, (np.ndarray, list, tuple)): + return array(idx, ctx, idx_dtype) - elif isinstance(idx, (np.ndarray, list, tuple)): - idx = array(idx, ctx, idx_dtype) + elif isinstance(idx, integer_types): + return array([idx], ctx, idx_dtype) - elif isinstance(idx, integer_types): - idx = array([idx], ctx, idx_dtype) + elif isinstance(idx, py_slice): + start, stop, step = idx.indices(ax_len) + return arange(start, stop, step, ctx=ctx, dtype=idx_dtype) - elif isinstance(idx, py_slice): - start, stop, step = idx.indices(n) - idx = arange(start, stop, step, ctx=ctx, dtype=idx_dtype) + else: + raise RuntimeError('illegal index type {}'.format(type(idx))) - else: - raise RuntimeError('illegal index type {}'.format(type(idx))) - converted.append(idx) +def _broadcast_advanced_indices(arrays, block_axes): + """Broadcast arrays according to position in the sequence. + + Here, "according to position" means that an array of dimension 1 + (which is the case for all except ``block_axes``) will have shape + ``(1, ..., 1, N, 1, ..., 1)``, where ``N`` is the length, and the + position of ``N`` in the shape is the same as the position of the + array in the ``arrays`` sequence, plus extra dimensions of the + advanced block if it is left of the array. - return tuple(converted) + The arrays at ``block_axes`` are the advanced indices. They are assumed to + be ready for mutual broadcasting to produce the advanced indexing block. + It is further assumed that the numbers in ``block_axes`` are consecutive. + + The return value is a tuple containing the arrays with broadcast shapes. + """ + block_shape = _broadcast_shapes([arrays[ax] for ax in block_axes]) + block_ndim = len(block_shape) + bcast_shape = ( + tuple(arrays[ax].shape[0] for ax in range(block_axes[0])) + + block_shape + + tuple(arrays[ax].shape[0] for ax in range(block_axes[-1] + 1, len(arrays))) + ) + bcast_ndim = len(bcast_shape) + bcast_arrays = [] + ndim_done = 0 + block_started = block_done = False + for ax, idx in enumerate(arrays): + shp = (1,) * ndim_done + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) + bcast_arrays.append(idx.reshape(shp).broadcast_to(bcast_shape)) + + if ax in block_axes and not block_started: + # Keep `ndim_done` constant to use the same shape for broadcasting + # in the block. + block_started = True + elif block_started and not block_done and ax + 1 not in block_axes: + # Done with the block, increment `ndim_done` with the block ndim. + block_done = True + ndim_done += block_ndim + elif ax not in block_axes: + # Outside the block, arrays are assumed to have 1 dimension, + # so `ndim_done` is incremented by 1 after each. + ndim_done += 1 + + return tuple(bcast_arrays) def _get_indexing_dispatch_code(key): @@ -2601,8 +2564,8 @@ def _get_indexing_dispatch_code(key): assert isinstance(key, tuple) for idx in key: - if isinstance(key, list): - for i in key: + if isinstance(idx, list): + for i in idx: if not isinstance(i, integer_types): raise TypeError( 'Indexing NDArray only supports a list of integers as index ' @@ -2619,7 +2582,7 @@ def _get_indexing_dispatch_code(key): ''.format(idx), type(idx) ) - return _NDARRAY_BASIC_INDEXING + return _NDARRAY_BASIC_INDEXING def _get_index_range(start, stop, length, step=1): From c7e38294fef650e768d17c87c9fb2731f8dc445a Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Fri, 14 Dec 2018 00:13:31 +0100 Subject: [PATCH 13/47] Fix lint errors --- python/mxnet/ndarray/ndarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index e3207da984c6..f5e90ec357d6 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -2406,8 +2406,8 @@ def _indexing_key_expand_implicit_axes(key, shape): ell_ndim = len(shape) + num_none - len(nonell_key) expanded_key = (nonell_key[:ell_idx] + - (slice(None),) * ell_ndim + - nonell_key[ell_idx:]) + (slice(None),) * ell_ndim + + nonell_key[ell_idx:]) return expanded_key From 57f26b73372ee1b7416736a9d1ca80751d3e6128 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sun, 23 Dec 2018 20:16:31 +0100 Subject: [PATCH 14/47] WIP: fix basic indexing --- python/mxnet/ndarray/ndarray.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index f5e90ec357d6..0970689f4524 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -749,6 +749,7 @@ def _set_nd_basic_indexing(self, key, value): def _set_nd_advanced_indexing(self, key, value): """This function is called by __setitem__ when key is an advanced index.""" + # FIXME: broken now, fix it! indices = self._get_index_nd(key) vshape = _get_oshape_of_gather_nd_op(self.shape, indices.shape) value_nd = self._prepare_value_nd(value, vshape) @@ -763,7 +764,12 @@ def _axes_after_indexing(axes, key, ndim): ``key`` should not be the result of int-to-slice conversion, but the original indices. """ - cum_steps = np.arange(ndim + 1) + steps = [0] + [0 if isinstance(idx, integer_types) else 1 + for idx in key] + for _ in range(len(key), ndim): + steps.append(1) + assert len(steps) == 1 + ndim + cum_steps = np.cumsum(steps) axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] axes_after = tuple(cum_steps[axes_in_bounds]) From 99bca07d421f419043c6a5a2ede5abcf5b857c74 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 25 Dec 2018 20:37:21 +0100 Subject: [PATCH 15/47] WIP: fix basic indexing --- python/mxnet/ndarray/ndarray.py | 60 +++++++++++++++++---------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 0970689f4524..fe21b54ec510 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -18,9 +18,10 @@ # coding: utf-8 # pylint: disable=too-many-lines, protected-access # pylint: disable=import-error, no-name-in-module, undefined-variable + """NDArray API of MXNet.""" -from __future__ import absolute_import -from __future__ import division + +from __future__ import absolute_import, division try: from __builtin__ import slice as py_slice @@ -757,23 +758,23 @@ def _set_nd_advanced_indexing(self, key, value): shape=self.shape, out=self) @staticmethod - def _axes_after_indexing(axes, key, ndim): - """Return indices of ``axes`` after slicing with ``key``. + def _new_axes_after_basic_indexing(axes, key_nd): + """Return indices of ``axes`` after slicing with ``key_nd``. + + This function is used to calculate the positions where new axes should + end up after indexing, taking into account the removal of axes by + integer indexing. - Integer entries are considered as collapsed axes after indexing, i.e., - ``key`` should not be the result of int-to-slice conversion, but the - original indices. + The ``key_nd`` sequence should contain slices and integers only, no + ``None`` entries. """ steps = [0] + [0 if isinstance(idx, integer_types) else 1 - for idx in key] - for _ in range(len(key), ndim): - steps.append(1) - assert len(steps) == 1 + ndim + for idx in key_nd] cum_steps = np.cumsum(steps) axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] axes_after = tuple(cum_steps[axes_in_bounds]) - oob_offsets = [ax - ndim for ax in axes_out_of_bounds] + oob_offsets = [ax - len(key_nd) for ax in axes_out_of_bounds] axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) return axes_after @@ -794,7 +795,14 @@ def _get_nd_basic_indexing(self, key): none_axes = [ax for ax in range(len(key)) if key[ax] is None] slc_key, int_axes = _indexing_key_int_to_slice(key_nd) - new_axes = self._axes_after_indexing(none_axes, key_nd, self.ndim) + new_axes = self._new_axes_after_basic_indexing(none_axes, key_nd) + + # Check bounds for integer axes + for ax in int_axes: + if not -self.shape[ax] <= key_nd[ax] < self.shape[ax]: + raise IndexError( + 'index {} is out of bounds for axis {} with size {}' + ''.format(key_nd[ax], ax, self.shape[ax])) # Make sure we don't accidentally have advanced indexing or # unsupported entries. @@ -805,21 +813,16 @@ def _get_nd_basic_indexing(self, key): 'This is a bug, please report it!' ''.format(type(idx))) - # Check index bounds. Only `begin` is relevant, `end` is automatically - # clipped. - begin, _, _ = _basic_indexing_key_to_begin_end_step( + # Convert to begin, end and step, and return immediately if the slice + # is non-empty + begin, end, step = _basic_indexing_key_to_begin_end_step( slc_key, self.shape, keep_none=False ) - for ax, (b, n) in enumerate(zip(begin, self.shape)): - if not -(n + 1) <= b <= n: - raise IndexError( - 'index {} is out of bounds for axis {} with size {}' - ''.format(b, ax, n)) + if any( + b >= e and s > 0 or b <= e and s < 0 for b, e, s in zip(begin, end, step) + ): + return array([], self.context, self.dtype) - # Do the slicing. - begin, end, step = _basic_indexing_key_to_begin_end_step( - slc_key, self.shape, keep_none=True - ) # TODO: copy-free indexing if possible sliced = op.slice(self, begin, end, step) @@ -2471,10 +2474,7 @@ def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): idcs = [idx for idx in idcs if idx is not None] idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) for idx in idcs] - if keep_none: - sss_list = [(slc.start, slc.stop, slc.step) for slc in idcs] - else: - sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] + sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] return tuple(zip(*sss_list)) @@ -2804,6 +2804,8 @@ def array(source_array, ctx=None, dtype=None): arr = empty((1,), ctx, dtype) arr[:] = source_array return arr.reshape(()) + elif source_array.size == 0: + return empty((0,), ctx, dtype) else: arr = empty(source_array.shape, ctx, dtype) arr[:] = source_array From f9c048aab7a14b2b39fe45cc99c1316ead5cd6a7 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 25 Dec 2018 20:37:36 +0100 Subject: [PATCH 16/47] TEMP: print statements in tests --- tests/python/unittest/test_ndarray.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 0803105ae059..38476f61c5ef 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1315,13 +1315,10 @@ def test_getitem(np_array, index, is_scalar=False): if isinstance(index, mx.nd.NDArray): np_index = index.asnumpy() if isinstance(index, tuple): - np_index = [] - for idx in index: - if isinstance(idx, mx.nd.NDArray): - np_index.append(idx.asnumpy()) - else: - np_index.append(idx) - np_index = tuple(np_index) + np_index = tuple( + idx.asnumpy() if isinstance(idx, mx.nd.NDArray) else idx + for idx in index + ) np_indexed_array = np_array[np_index] mx_array = mx.nd.array(np_array, dtype=np_array.dtype) @@ -1330,6 +1327,16 @@ def test_getitem(np_array, index, is_scalar=False): mx_indexed_array = mx_indexed_array.asscalar() else: mx_indexed_array = mx_indexed_array.asnumpy() + + passed = same(np_indexed_array, mx_indexed_array) + if not passed: + print("index:", index) + print("shape:", np_array.shape) + print(mx_indexed_array.shape) + #print(np.where(mx_indexed_array != np_indexed_array)) + #print(np_indexed_array[mx_indexed_array != np_indexed_array]) + #print(np_indexed_array[mx_indexed_array != np_indexed_array].shape) + assert same(np_indexed_array, mx_indexed_array), 'Failed with index=%s' % str(index) def test_setitem(np_array, index, is_scalar): From bb57638d9f8f34b831ebb14819f83f80c6ef4bac Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 25 Dec 2018 20:38:04 +0100 Subject: [PATCH 17/47] Fix op.slice with step<0 and end==-1 --- src/operator/tensor/matrix_op-inl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operator/tensor/matrix_op-inl.h b/src/operator/tensor/matrix_op-inl.h index 128fa0e75aa1..00493a629ada 100644 --- a/src/operator/tensor/matrix_op-inl.h +++ b/src/operator/tensor/matrix_op-inl.h @@ -704,7 +704,7 @@ inline void GetIndexRange(const mxnet::TShape& dshape, << " exceeds limit of input dimension[" << i << "]=" << len; // checking upper and lower bounds for end - if (e < 0 && param_end[i].has_value()) { + if (e < 0 && param_end[i].has_value() && !(s < 0 && e == -1)) { // Keep -1 as one-beyond-limits index for negative stride e += len; CHECK_GE(e, 0) << "slicing with end[" << i << "]=" << e - len << " exceeds limit of input dimension[" << i << "]=" << len; From e3d9921f0de7dcf04e34063c942fd0d048f25ba2 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 27 Dec 2018 00:36:00 +0100 Subject: [PATCH 18/47] Implement copy-free general contiguous indexing --- python/mxnet/ndarray/ndarray.py | 519 +++++++++++++++----------- tests/python/unittest/test_ndarray.py | 27 ++ 2 files changed, 325 insertions(+), 221 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index fe21b54ec510..536d855dafd1 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -557,92 +557,6 @@ def __getitem__(self, key): else: raise RuntimeError - @staticmethod - def _drop_slice_none_at_end(key): - key = list(key) - while isinstance(key[-1], py_slice) and key[-1] == slice(None): - key.pop() - return tuple(key) - - def _get_index_nd(self, key): - """Return an index array for use in `scatter_nd` and `gather_nd`.""" - key_nd = tuple(idx for idx in key if idx is not None) - if len(key_nd) < self.ndim: - raise RuntimeError( - 'too few indices after normalization: expected `ndim` ({}) ' - 'but got {}. This is a bug, please report it!' - ''.format(self.ndim, len(key_nd)) - ) - elif len(key_nd) > self.ndim: - raise IndexError( - 'too many indices ({}) for array with {} dimensions' - ''.format(len(key_nd), self.ndim) - ) - ndim = len(key_nd) - - # --- Preparation --- # - - # - Make lists for bookkeeping of advanced indices & axes - # - Drop trailing `slice(None)` entries in `key` for efficiency - # - Determine whether the advanced indices are adjacent in `key` - # - Depending on that, make index permutations to move around indices - - adv_axs = [ax for ax, idx in enumerate(key) if _is_advanced_index(idx)] - adv_axs_nd = [ax for ax, idx in enumerate(key_nd) if _is_advanced_index(idx)] - adv_idcs_are_adjacent = bool(np.all(np.diff(adv_axs) == 1)) - nonadv_axs_nd = [ax for ax in range(ndim) if ax not in adv_axs_nd] - adv_idcs_nd = [key_nd[ax] for ax in adv_axs_nd] - idcs_short = self._drop_slice_none_at_end(key_nd) - dropped_axs = list(range(len(idcs_short), ndim)) - - if adv_idcs_are_adjacent: - # The easy case: the advanced block can stay at its position, and no - # permutation needs to be done (identity permutation) - axs_nd_permut = axs_nd_permut_inv = tuple(range(ndim)) - idcs_permut_short = idcs_short - block_axs_nd = adv_axs_nd - else: - # The more complicated case: during broadcasting, we need to use the - # indices in the *permuted* order, where the advanced block is - # at the beginning, while the final index for `gather_nd` is stacked - # in the *original* order, so that the association of index with - # array axis remains the same. - - # This order is used for broadcasting: advanced block at the beginning - idcs_permut_short = ( - adv_idcs_nd + - [key_nd[ax] for ax in range(ndim) - if ax not in adv_axs_nd and ax not in dropped_axs] - ) - block_axs_nd = list(range(len(adv_axs_nd))) - axs_nd_permut = adv_axs_nd + nonadv_axs_nd - axs_nd_permut_inv = list(np.argsort(axs_nd_permut)) - - # --- Conversion, broadcasting and index stacking --- # - - # - Convert all indices in `key` to arrays: integers to 1-element arrays, - # `slice` objects to arrays with explicit indices - # - Reshape arrays for broadcasting according to their position in the - # *permuted* key - # - Broadcast and stack the indices in the *original* order - - shape_nd_permut = tuple(self.shape[ax] for ax in axs_nd_permut) - converted_idcs_short = [ - _advanced_index_to_array(idx, ax_len, self.context) - for idx, ax_len in zip(idcs_permut_short, shape_nd_permut) - ] - bcast_idcs_permut_short = _broadcast_advanced_indices( - converted_idcs_short, block_axes=block_axs_nd - ) - # Undo the permutation to restore the original order - bcast_idcs_short = [ - bcast_idcs_permut_short[ax] - for ax in axs_nd_permut_inv - if axs_nd_permut[ax] not in dropped_axs - ] - - return op.stack(*bcast_idcs_short) - def _prepare_value_nd(self, value, new_axes, bcast_shape): """Return a broadcast `NDArray` with same context and dtype as ``self``. @@ -676,6 +590,107 @@ def _prepare_value_nd(self, value, new_axes, bcast_shape): value_nd = value_nd.broadcast_to(bcast_shape) return value_nd + @staticmethod + def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): + """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" + idcs = [idx for idx in idcs if idx is not None] + idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) + for idx in idcs] + sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] + return tuple(zip(*sss_list)) + + @staticmethod + def _basic_indexing_key_int_to_slice(idcs): + """Return the converted indexing tuple and the integer axes.""" + int_axes = [] + conv_idcs = [] + for ax, idx in enumerate(idcs): + if isinstance(idx, integer_types): + conv_idcs.append(_int_to_slice(idx)) + int_axes.append(ax) + else: + conv_idcs.append(idx) + + return tuple(conv_idcs), tuple(int_axes) + + @staticmethod + def _new_axes_after_basic_indexing(axes, key_nd): + """Return indices of ``axes`` after slicing with ``key_nd``. + + This function is used to calculate the positions where new axes should + end up after indexing, taking into account the removal of axes by + integer indexing. + + The ``key_nd`` sequence should contain slices and integers only, no + ``None`` entries. + """ + steps = [0] + [0 if isinstance(idx, integer_types) else 1 + for idx in key_nd] + cum_steps = np.cumsum(steps) + axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] + axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] + axes_after = tuple(cum_steps[axes_in_bounds]) + oob_offsets = [ax - len(key_nd) for ax in axes_out_of_bounds] + axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) + return axes_after + + @staticmethod + def _basic_indexing_slice_is_contiguous(slc_key, shape): + """Whether indexing with the given key results in a contiguous array. + + The rule is: From right to left, if in an axis, a slice produces a + proper subset, no later axis can produce a proper subset or use + a step different from 1. + + The ``slc_key`` sequence must have the same length as ``shape`` and + only contain `slice` objects. + """ + assert len(slc_key) == len(shape) + subset = False + for idx, n in zip(reversed(slc_key), reversed(shape)): + start, stop, step = idx.indices(n) + if step > 0: + num = int(np.ceil(max(stop - start, 0) / step)) + else: + num = int(np.ceil(min(stop - start, 0) / step)) + + if num != 1 and (subset or step != 1): + return False + if num != n: + subset = True + + return True + + @staticmethod + def _basic_indexing_sliced_shape(slc_key, shape): + """Return the shape after slicing with the given key.""" + assert len(slc_key) == len(shape) + sliced_shape = [] + for idx, n in zip(slc_key, shape): + start, stop, step = idx.indices(n) + if step > 0: + num = int(np.ceil(max(stop - start, 0) / step)) + else: + num = int(np.ceil(min(stop - start, 0) / step)) + sliced_shape.append(num) + + return tuple(sliced_shape) + + @staticmethod + def _basic_indexing_contiguous_flat_begin_end(slc_key, shape): + """Return the flat indices of begin and end for contiguous slicing.""" + assert len(slc_key) == len(shape) + begin, end, _ = slc_key[0].indices(shape[0]) + flat_begin, flat_end = begin, end - 1 + for idx, n in zip(slc_key[1:], shape[1:]): + flat_begin *= n + flat_end *= n + begin, end, _ = idx.indices(n) + flat_begin += begin + flat_end += end - 1 + + return flat_begin, flat_end + 1 + def _set_nd_basic_indexing(self, key, value): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" for idx in key: @@ -687,7 +702,7 @@ def _set_nd_basic_indexing(self, key, value): ''.format(type(idx))) int_axes = [ax for ax in range(len(key)) if isinstance(key[ax], integer_types)] - begin, end, step = _basic_indexing_key_to_begin_end_step( + begin, end, step = self._basic_indexing_key_to_begin_end_step( key, self.shape, keep_none=False ) indexed_shape = tuple( @@ -696,7 +711,7 @@ def _set_nd_basic_indexing(self, key, value): can_assign_directly = ( (indexed_shape == self.shape) and all(s > 0 for s in step) ) - begin, end, step = _basic_indexing_key_to_begin_end_step( + begin, end, step = self._basic_indexing_key_to_begin_end_step( key, self.shape, keep_none=True ) @@ -748,38 +763,8 @@ def _set_nd_basic_indexing(self, key, value): ) _internal._slice_assign(self, value_nd, begin, end, step, out=self) - def _set_nd_advanced_indexing(self, key, value): - """This function is called by __setitem__ when key is an advanced index.""" - # FIXME: broken now, fix it! - indices = self._get_index_nd(key) - vshape = _get_oshape_of_gather_nd_op(self.shape, indices.shape) - value_nd = self._prepare_value_nd(value, vshape) - _internal._scatter_set_nd(lhs=self, rhs=value_nd, indices=indices, - shape=self.shape, out=self) - - @staticmethod - def _new_axes_after_basic_indexing(axes, key_nd): - """Return indices of ``axes`` after slicing with ``key_nd``. - - This function is used to calculate the positions where new axes should - end up after indexing, taking into account the removal of axes by - integer indexing. - - The ``key_nd`` sequence should contain slices and integers only, no - ``None`` entries. - """ - steps = [0] + [0 if isinstance(idx, integer_types) else 1 - for idx in key_nd] - cum_steps = np.cumsum(steps) - axes_in_bounds = [ax for ax in axes if ax < len(cum_steps)] - axes_out_of_bounds = [ax for ax in axes if ax >= len(cum_steps)] - axes_after = tuple(cum_steps[axes_in_bounds]) - oob_offsets = [ax - len(key_nd) for ax in axes_out_of_bounds] - axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) - return axes_after - def _get_nd_basic_indexing(self, key): - """This function indexes ``self`` with a tuple of ``slice`` objects only.""" + """This function indexes ``self`` with a tuple of `slice` objects only.""" key_nd = tuple(idx for idx in key if idx is not None) if len(key_nd) < self.ndim: raise RuntimeError( @@ -794,7 +779,7 @@ def _get_nd_basic_indexing(self, key): ) none_axes = [ax for ax in range(len(key)) if key[ax] is None] - slc_key, int_axes = _indexing_key_int_to_slice(key_nd) + slc_key, int_axes = self._basic_indexing_key_int_to_slice(key_nd) new_axes = self._new_axes_after_basic_indexing(none_axes, key_nd) # Check bounds for integer axes @@ -814,8 +799,8 @@ def _get_nd_basic_indexing(self, key): ''.format(type(idx))) # Convert to begin, end and step, and return immediately if the slice - # is non-empty - begin, end, step = _basic_indexing_key_to_begin_end_step( + # is empty + begin, end, step = self._basic_indexing_key_to_begin_end_step( slc_key, self.shape, keep_none=False ) if any( @@ -823,8 +808,25 @@ def _get_nd_basic_indexing(self, key): ): return array([], self.context, self.dtype) - # TODO: copy-free indexing if possible - sliced = op.slice(self, begin, end, step) + if self._basic_indexing_slice_is_contiguous(slc_key, self.shape): + # Create a shared-memory view by using low-level flat slicing + flat_begin, flat_end = self._basic_indexing_contiguous_flat_begin_end( + slc_key, self.shape + ) + handle = NDArrayHandle() + flat_self = self.reshape(-1) + check_call( + _LIB.MXNDArraySlice( + flat_self.handle, + mx_uint(flat_begin), + mx_uint(flat_end), + ctypes.byref(handle), + ) + ) + sliced_shape = self._basic_indexing_sliced_shape(slc_key, self.shape) + sliced = NDArray(handle=handle, writable=self.writable).reshape(sliced_shape) + else: + sliced = op.slice(self, begin, end, step) # Reshape to final shape due to integer and `None` entries in `key`. final_shape = [sliced.shape[i] for i in range(sliced.ndim) @@ -838,6 +840,178 @@ def _get_nd_basic_indexing(self, key): return sliced.reshape(final_shape) + @staticmethod + def _advanced_index_to_array(idx, ax_len, ctx): + """Convert ``idx`` to `NDArray` for advanced indexing. + + The ``ax_len`` is used to convert `slice` objects to integer arrays. + """ + idx_dtype = 'int32' + if isinstance(idx, NDArray): + if idx.dtype != idx_dtype: + idx = idx.astype(idx_dtype) + return idx.as_in_context(ctx) + + elif isinstance(idx, (np.ndarray, list, tuple)): + return array(idx, ctx, idx_dtype) + + elif isinstance(idx, integer_types): + return array([idx], ctx, idx_dtype) + + elif isinstance(idx, py_slice): + start, stop, step = idx.indices(ax_len) + return arange(start, stop, step, ctx=ctx, dtype=idx_dtype) + + else: + raise RuntimeError('illegal index type {}'.format(type(idx))) + + def _broadcast_advanced_indices(arrays, block_axes): + """Broadcast arrays according to position in the sequence. + + Here, "according to position" means that an array of dimension 1 + (which is the case for all except ``block_axes``) will have shape + ``(1, ..., 1, N, 1, ..., 1)``, where ``N`` is the length, and the + position of ``N`` in the shape is the same as the position of the + array in the ``arrays`` sequence, plus extra dimensions of the + advanced block if it is left of the array. + + The arrays at ``block_axes`` are the advanced indices. They are assumed to + be ready for mutual broadcasting to produce the advanced indexing block. + It is further assumed that the numbers in ``block_axes`` are consecutive. + + The return value is a tuple containing the arrays with broadcast shapes. + """ + block_shape = _broadcast_shapes([arrays[ax] for ax in block_axes]) + block_ndim = len(block_shape) + bcast_shape = ( + tuple(arrays[ax].shape[0] for ax in range(block_axes[0])) + + block_shape + + tuple(arrays[ax].shape[0] for ax in range(block_axes[-1] + 1, len(arrays))) + ) + bcast_ndim = len(bcast_shape) + bcast_arrays = [] + ndim_done = 0 + block_started = block_done = False + for ax, idx in enumerate(arrays): + shp = (1,) * ndim_done + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) + bcast_arrays.append(idx.reshape(shp).broadcast_to(bcast_shape)) + + if ax in block_axes and not block_started: + # Keep `ndim_done` constant to use the same shape for broadcasting + # in the block. + block_started = True + elif block_started and not block_done and ax + 1 not in block_axes: + # Done with the block, increment `ndim_done` with the block ndim. + block_done = True + ndim_done += block_ndim + elif ax not in block_axes: + # Outside the block, arrays are assumed to have 1 dimension, + # so `ndim_done` is incremented by 1 after each. + ndim_done += 1 + + return tuple(bcast_arrays) + + @staticmethod + def _drop_slice_none_at_end(key): + """Remove ``slice(None)`` at the end of a key. + + This is used for efficiency in advanced indexing, to avoid generating + ``arange(n)`` arrays for these axes. The `gather_nd` and `scatter_nd` + handle implicit full trailing axes automatically. + """ + key = list(key) + while isinstance(key[-1], py_slice) and key[-1] == slice(None): + key.pop() + return tuple(key) + + def _get_index_nd(self, key): + """Return an index array for use in `scatter_nd` and `gather_nd`.""" + key_nd = tuple(idx for idx in key if idx is not None) + if len(key_nd) < self.ndim: + raise RuntimeError( + 'too few indices after normalization: expected `ndim` ({}) ' + 'but got {}. This is a bug, please report it!' + ''.format(self.ndim, len(key_nd)) + ) + elif len(key_nd) > self.ndim: + raise IndexError( + 'too many indices ({}) for array with {} dimensions' + ''.format(len(key_nd), self.ndim) + ) + ndim = len(key_nd) + + # --- Preparation --- # + + # - Make lists for bookkeeping of advanced indices & axes + # - Drop trailing `slice(None)` entries in `key` for efficiency + # - Determine whether the advanced indices are adjacent in `key` + # - Depending on that, make index permutations to move around indices + + adv_axs = [ax for ax, idx in enumerate(key) if _is_advanced_index(idx)] + adv_axs_nd = [ax for ax, idx in enumerate(key_nd) if _is_advanced_index(idx)] + adv_idcs_are_adjacent = bool(np.all(np.diff(adv_axs) == 1)) + nonadv_axs_nd = [ax for ax in range(ndim) if ax not in adv_axs_nd] + adv_idcs_nd = [key_nd[ax] for ax in adv_axs_nd] + idcs_short = self._drop_slice_none_at_end(key_nd) + dropped_axs = list(range(len(idcs_short), ndim)) + + if adv_idcs_are_adjacent: + # The easy case: the advanced block can stay at its position, and no + # permutation needs to be done (identity permutation) + axs_nd_permut = axs_nd_permut_inv = tuple(range(ndim)) + idcs_permut_short = idcs_short + block_axs_nd = adv_axs_nd + else: + # The more complicated case: during broadcasting, we need to use the + # indices in the *permuted* order, where the advanced block is + # at the beginning, while the final index for `gather_nd` is stacked + # in the *original* order, so that the association of index with + # array axis remains the same. + + # This order is used for broadcasting: advanced block at the beginning + idcs_permut_short = ( + adv_idcs_nd + + [key_nd[ax] for ax in range(ndim) + if ax not in adv_axs_nd and ax not in dropped_axs] + ) + block_axs_nd = list(range(len(adv_axs_nd))) + axs_nd_permut = adv_axs_nd + nonadv_axs_nd + axs_nd_permut_inv = list(np.argsort(axs_nd_permut)) + + # --- Conversion, broadcasting and index stacking --- # + + # - Convert all indices in `key` to arrays: integers to 1-element arrays, + # `slice` objects to arrays with explicit indices + # - Reshape arrays for broadcasting according to their position in the + # *permuted* key + # - Broadcast and stack the indices in the *original* order + + shape_nd_permut = tuple(self.shape[ax] for ax in axs_nd_permut) + converted_idcs_short = [ + self._advanced_index_to_array(idx, ax_len, self.context) + for idx, ax_len in zip(idcs_permut_short, shape_nd_permut) + ] + bcast_idcs_permut_short = self._broadcast_advanced_indices( + converted_idcs_short, block_axes=block_axs_nd + ) + # Undo the permutation to restore the original order + bcast_idcs_short = [ + bcast_idcs_permut_short[ax] + for ax in axs_nd_permut_inv + if axs_nd_permut[ax] not in dropped_axs + ] + + return op.stack(*bcast_idcs_short) + + def _set_nd_advanced_indexing(self, key, value): + """This function is called by __setitem__ when key is an advanced index.""" + # FIXME: broken now, fix it! + indices = self._get_index_nd(key) + vshape = _get_oshape_of_gather_nd_op(self.shape, indices.shape) + value_nd = self._prepare_value_nd(value, vshape) + _internal._scatter_set_nd(lhs=self, rhs=value_nd, indices=indices, + shape=self.shape, out=self) + def _get_nd_advanced_indexing(self, key): """Get item when key is a tuple of any objects of the following types: NDArray, np.ndarray, list, tuple, slice, and integer.""" @@ -2383,8 +2557,6 @@ def _indexing_key_expand_implicit_axes(key, shape): """Make implicit axes explicit by adding ``slice(None)``.""" if not isinstance(key, tuple): - # NOTE: `arr[key]` should be equivalent to `arr[(key,)]`, but in - # some cases this is not true currently, e.g., with `slice` objects. key = (key,) # We need to loop explicitly since tuple functions like `index()` or @@ -2430,20 +2602,6 @@ def _int_to_slice(idx): return slice(idx, idx + 1) -def _indexing_key_int_to_slice(idcs): - """Return the converted indexing tuple and the integer axes.""" - int_axes = [] - conv_idcs = [] - for ax, idx in enumerate(idcs): - if isinstance(idx, integer_types): - conv_idcs.append(_int_to_slice(idx)) - int_axes.append(ax) - else: - conv_idcs.append(idx) - - return tuple(conv_idcs), tuple(int_axes) - - def _shape_for_bcast(shape, target_ndim, new_axes): """Return shape with added axes for broadcasting in ``target_ndim`` dimensions. @@ -2469,15 +2627,6 @@ def _shape_for_bcast(shape, target_ndim, new_axes): return tuple(new_shape) -def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): - """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" - idcs = [idx for idx in idcs if idx is not None] - idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) - for idx in idcs] - sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] - return tuple(zip(*sss_list)) - - def _is_advanced_index(idx): """Return whether ``idx`` is an advanced index (array-like or integer). @@ -2493,78 +2642,6 @@ def _is_advanced_index(idx): raise RuntimeError('illegal index type {}'.format(type(idx))) -def _advanced_index_to_array(idx, ax_len, ctx): - """Convert ``idx`` to `NDArray` for advanced indexing. - - The ``ax_len`` is used to convert `slice` objects to integer arrays. - """ - idx_dtype = 'int32' - if isinstance(idx, NDArray): - if idx.dtype != idx_dtype: - idx = idx.astype(idx_dtype) - return idx.as_in_context(ctx) - - elif isinstance(idx, (np.ndarray, list, tuple)): - return array(idx, ctx, idx_dtype) - - elif isinstance(idx, integer_types): - return array([idx], ctx, idx_dtype) - - elif isinstance(idx, py_slice): - start, stop, step = idx.indices(ax_len) - return arange(start, stop, step, ctx=ctx, dtype=idx_dtype) - - else: - raise RuntimeError('illegal index type {}'.format(type(idx))) - - -def _broadcast_advanced_indices(arrays, block_axes): - """Broadcast arrays according to position in the sequence. - - Here, "according to position" means that an array of dimension 1 - (which is the case for all except ``block_axes``) will have shape - ``(1, ..., 1, N, 1, ..., 1)``, where ``N`` is the length, and the - position of ``N`` in the shape is the same as the position of the - array in the ``arrays`` sequence, plus extra dimensions of the - advanced block if it is left of the array. - - The arrays at ``block_axes`` are the advanced indices. They are assumed to - be ready for mutual broadcasting to produce the advanced indexing block. - It is further assumed that the numbers in ``block_axes`` are consecutive. - - The return value is a tuple containing the arrays with broadcast shapes. - """ - block_shape = _broadcast_shapes([arrays[ax] for ax in block_axes]) - block_ndim = len(block_shape) - bcast_shape = ( - tuple(arrays[ax].shape[0] for ax in range(block_axes[0])) + - block_shape + - tuple(arrays[ax].shape[0] for ax in range(block_axes[-1] + 1, len(arrays))) - ) - bcast_ndim = len(bcast_shape) - bcast_arrays = [] - ndim_done = 0 - block_started = block_done = False - for ax, idx in enumerate(arrays): - shp = (1,) * ndim_done + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) - bcast_arrays.append(idx.reshape(shp).broadcast_to(bcast_shape)) - - if ax in block_axes and not block_started: - # Keep `ndim_done` constant to use the same shape for broadcasting - # in the block. - block_started = True - elif block_started and not block_done and ax + 1 not in block_axes: - # Done with the block, increment `ndim_done` with the block ndim. - block_done = True - ndim_done += block_ndim - elif ax not in block_axes: - # Outside the block, arrays are assumed to have 1 dimension, - # so `ndim_done` is incremented by 1 after each. - ndim_done += 1 - - return tuple(bcast_arrays) - - def _get_indexing_dispatch_code(key): """Returns a dispatch code for calling basic or advanced indexing functions.""" assert isinstance(key, tuple) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 38476f61c5ef..8e9d0f3779ba 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -18,6 +18,7 @@ import mxnet as mx import numpy as np from distutils.version import LooseVersion +from itertools import permutations, combinations_with_replacement import os import pickle as pkl from nose.tools import assert_raises, raises @@ -1305,6 +1306,32 @@ def test_bool(): assert bool(mx.nd.ones((1,))) +def test_basic_indexing_is_contiguous(): + x = np.arange(np.prod((6, 7, 8, 9))).reshape((6, 7, 8, 9)) + slices = [ + slice(None), + slice(2), + slice(20), + slice(1, 4), + slice(None, None, 2), + slice(None, None, 20), + slice(0, 1), + slice(None, None, -1), + slice(3, None, -2), + ] + + is_contiguous = mx.nd.NDArray._basic_indexing_slice_is_contiguous + + for idx in combinations_with_replacement(slices, 4): + for slc in permutations(idx): + contig_pred = is_contiguous(slc, x.shape) + contig_true = x[slc].flags.contiguous + assert contig_pred == contig_true, ( + "failed with slc={}, pred ({}) != actual ({})" + "".format(slc, contig_pred, contig_true) + ) + + @with_seed() def test_ndarray_indexing(): def test_getitem(np_array, index, is_scalar=False): From 00ffcd439e9e4996e46052cb07f966b3fbb4906c Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 27 Dec 2018 15:56:58 +0100 Subject: [PATCH 19/47] Improve doctest of __getitem__ --- python/mxnet/ndarray/ndarray.py | 138 ++++++++++++++++++++++++-------- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 536d855dafd1..d964031cc047 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -502,48 +502,116 @@ def __getitem__(self, key): Examples -------- + The default is to give explicit indices for all axes: + >>> x = mx.nd.arange(0, 6).reshape((2, 3)) >>> x.asnumpy() array([[ 0., 1., 2.], [ 3., 4., 5.]], dtype=float32) - >>> x[1].asnumpy() - array([ 3., 4., 5.], dtype=float32) - >>> y = x[0:1] - >>> y[:] = 2 + >>> x[0, :].asnumpy() + array([0., 1., 2.], dtype=float32) + >>> x[0, :2].asnumpy() + array([0., 1.], dtype=float32) + >>> x[:, :-1].asnumpy() + array([[0., 1.], + [3., 4.]], dtype=float32) + + If fewer indices are given, they are automatically supplemented by an + appropriate number of ``slice(None)`` ("``:``") to the right. For + instance, a single integer indexes along the first axis: + + >>> x = mx.nd.arange(0, 6).reshape((2, 3)) + >>> x[0].asnumpy() + array([0., 1., 2.], dtype=float32) + >>> x[1:].asnumpy() + array([[3., 4., 5.]], dtype=float32) + + To omit a range of axes that should be kept as-is, an `Ellipsis` + ("``...``") can be used: + + >>> x = mx.nd.arange(0, 16).reshape((2, 2, 2, 2)) + >>> x[0, ..., 1].asnumpy() + array([[1., 3.], + [5., 7.]], dtype=float32) + >>> x[0, :, :, 1].asnumpy() # equivalent + array([[1., 3.], + [5., 7.]], dtype=float32) + + New axes of length 1 can be created by inserting ``None`` + (`numpy.newaxis`) in the index: + + >>> x = mx.nd.arange(0, 6).reshape((2, 3)) + >>> x[None, :, :].asnumpy() + array([[[0., 1., 2.], + [3., 4., 5.]]], dtype=float32) + >>> x[None, :, :].shape + (1, 2, 3) + + If the indexed portion of the array is contiguous in memory, no data + is copied. Instead, a shared-memory view of the original array is + returned, and changes to that view affect the original array: + + >>> x = mx.nd.arange(0, 8).reshape((2, 2, 2)) + >>> y = x[0] # contiguous + >>> y.asnumpy() + array([[0., 1.], + [2., 3.]], dtype=float32) + >>> y[:] = -1 >>> x.asnumpy() - array([[ 2., 2., 2.], - [ 3., 4., 5.]], dtype=float32) - >>> x = mx.nd.arange(0, 8, dtype='int32').reshape((2, 2, 2)) - >>> x[1, :] - [[4 5] - [6 7]] - >>> x[1, ...] # equivalent - [[4 5] - [6 7]] - >>> x[[0, 1]] - [[[0 1] - [2 3]] - [[4 5] - [6 7]]] - >>> x[1:, [0, 1]] - [[[4 5] - [6 7]]] + array([[[-1., -1.], + [-1., -1.]], + + [[ 4., 5.], + [ 6., 7.]]], dtype=float32) + >>> x = mx.nd.arange(0, 8).reshape((2, 2, 2)) + >>> y = x[1, :1, :] # contiguous + >>> y.asnumpy() + array([[4., 5.]], dtype=float32) + >>> y[:] = -1 + >>> x.asnumpy() + array([[[ 0., 1.], + [ 2., 3.]], + + [[-1., -1.], + [ 6., 7.]]], dtype=float32) + >>> x = mx.nd.arange(0, 8).reshape((2, 2, 2)) + >>> y = x[:, :, 1] # not contiguous + >>> y.asnumpy() + array([[1., 3.], + [5., 7.]], dtype=float32) + >>> y[:] = -1 + >>> x.asnumpy() + array([[[0., 1.], + [2., 3.]], + + [[4., 5.], + [6., 7.]]], dtype=float32) + + If the indexing key contains `list`, `numpy.ndarray` or `NDArray` + objects, advanced indexing is triggered, which always returns a + copy: + + >>> x = mx.nd.arange(0, 8).reshape((2, 2, 2)) + >>> x[[0, 1]].asnumpy() + array([[[0., 1.], + [2., 3.]], + + [[4., 5.], + [6., 7.]]], dtype=float32) + >>> x[[0, 1], :].asnumpy() # equivalent + array([[[0., 1.], + [2., 3.]], + + [[4., 5.], + [6., 7.]]], dtype=float32) >>> y = np.array([0, 1], dtype='int32') - >>> x[1:, y] - [[[4 5] - [6 7]]] + >>> x[1:, y].asnumpy() + array([[[4., 5.], + [6., 7.]]], dtype=float32) >>> y = mx.nd.array([0, 1], dtype='int32') - >>> x[1:, y] - [[[4 5] - [6 7]]] - >>> x[None, None].shape - (1, 1, 2, 2, 2) - >>> x[None, None, :].shape # equivalent - (1, 1, 2, 2, 2) - >>> x[None, None, ...].shape # equivalent - (1, 1, 2, 2, 2) - >>> x[None, ..., None].shape - (1, 2, 2, 2, 1) + >>> x[1:, y].asnumpy() + array([[[4., 5.], + [6., 7.]]], dtype=float32) """ key = _indexing_key_expand_implicit_axes(key, self.shape) if len(key) == 0: From 89ea383671e5c5f9e70607e4fb928370bf280a15 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 27 Dec 2018 15:57:16 +0100 Subject: [PATCH 20/47] Fix missing staticmethod --- python/mxnet/ndarray/ndarray.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index d964031cc047..36fa86760bbc 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -933,6 +933,7 @@ def _advanced_index_to_array(idx, ax_len, ctx): else: raise RuntimeError('illegal index type {}'.format(type(idx))) + @staticmethod def _broadcast_advanced_indices(arrays, block_axes): """Broadcast arrays according to position in the sequence. From aba2fec37db94dcc87b522ddfa552177898b6498 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 27 Dec 2018 15:57:34 +0100 Subject: [PATCH 21/47] Remove superfluous _at and _slice --- python/mxnet/ndarray/ndarray.py | 64 --------------------------------- 1 file changed, 64 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 36fa86760bbc..7569d8c75954 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -1123,70 +1123,6 @@ def _sync_copyfrom(self, source_array): source_array.ctypes.data_as(ctypes.c_void_p), ctypes.c_size_t(source_array.size))) - def _slice(self, start, stop): - """Returns a sliced NDArray that shares memory with the current one. - This is called through ``x[start:stop]``. - - Parameters - ---------- - start : int - Starting inclusive index of slice in the first dim. - stop : int - Finishing exclusive index of slice in the first dim. - - Returns - ------- - `NDArray` sharing the memory with the current one sliced from - start to stop in the first dim. - - Examples: - >>> a = mx.nd.array([[1,2], [3, 4], [5, 6], [7, 8]]) - >>> a[1:2].asnumpy() - array([[ 3., 4.]], dtype=float32) - >>> a[1:1].asnumpy() - array([], shape=(0, 2), dtype=float32) - """ - handle = NDArrayHandle() - start, stop, _ = _get_index_range(start, stop, self.shape[0]) - - check_call(_LIB.MXNDArraySlice( - self.handle, mx_uint(start), mx_uint(stop), ctypes.byref(handle))) - return NDArray(handle=handle, writable=self.writable) - - def _at(self, idx): - """Returns a view of the array sliced at `idx` in the first dim. - This is called through ``x[idx]``. - - Parameters - ---------- - idx : int - index for slicing the `NDArray` in the first dim. - - Returns - ------- - NDArray - `NDArray` sharing the memory with the current one sliced at `idx` in the first dim. - - Examples - -------- - >>> a = mx.nd.array([[1,2], [3, 4]]) - >>> a[1].asnumpy() - array([ 3., 4.], dtype=float32) - >>> b = mx.nd.array([1, 2, 3, 4]) - >>> b[0].asnumpy() - array([ 1.], dtype=float32) - """ - handle = NDArrayHandle() - if idx < 0: - length = self.shape[0] - idx += length - if idx < 0: - raise IndexError('index %d is out of bounds for axis 0 with size %d' - % (idx-length, length)) - check_call(_LIB.MXNDArrayAt( - self.handle, mx_uint(idx), ctypes.byref(handle))) - return NDArray(handle=handle, writable=self.writable) - def reshape(self, *shape, **kwargs): """Returns a **view** of this array with a new shape without altering any data. From 704424267642bd67306848ad8c9d9ae706b8bca5 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 27 Dec 2018 16:06:11 +0100 Subject: [PATCH 22/47] Fix lint errors --- src/operator/tensor/matrix_op-inl.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/operator/tensor/matrix_op-inl.h b/src/operator/tensor/matrix_op-inl.h index 00493a629ada..16ad03c9d226 100644 --- a/src/operator/tensor/matrix_op-inl.h +++ b/src/operator/tensor/matrix_op-inl.h @@ -704,8 +704,11 @@ inline void GetIndexRange(const mxnet::TShape& dshape, << " exceeds limit of input dimension[" << i << "]=" << len; // checking upper and lower bounds for end - if (e < 0 && param_end[i].has_value() && !(s < 0 && e == -1)) { // Keep -1 as one-beyond-limits index for negative stride - e += len; + if (e < 0 && param_end[i].has_value()) { + if (!(s < 0 && e == -1)){ + // Keep end=-1 as one-beyond-limits index for negative stride + e += len; + } CHECK_GE(e, 0) << "slicing with end[" << i << "]=" << e - len << " exceeds limit of input dimension[" << i << "]=" << len; } From faed0199808fa4ffbd74b4e2df2edd8bc4151e87 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Fri, 8 Feb 2019 22:01:20 +0100 Subject: [PATCH 23/47] WIP: basic indexing --- python/mxnet/ndarray/ndarray.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 7569d8c75954..067cf0ddcf26 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -768,8 +768,9 @@ def _set_nd_basic_indexing(self, key, value): 'basic implementation, got object of type {}. ' 'This is a bug, please report it!' ''.format(type(idx))) - int_axes = [ax for ax in range(len(key)) - if isinstance(key[ax], integer_types)] + int_axes = [ + ax for ax in range(len(key)) if isinstance(key[ax], integer_types) + ] begin, end, step = self._basic_indexing_key_to_begin_end_step( key, self.shape, keep_none=False ) @@ -962,7 +963,8 @@ def _broadcast_advanced_indices(arrays, block_axes): ndim_done = 0 block_started = block_done = False for ax, idx in enumerate(arrays): - shp = (1,) * ndim_done + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) + # TODO: finish + shp = (1,) * (ndim_done + block_ndim - idx.ndim) + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) bcast_arrays.append(idx.reshape(shp).broadcast_to(bcast_shape)) if ax in block_axes and not block_started: @@ -1074,12 +1076,12 @@ def _get_index_nd(self, key): def _set_nd_advanced_indexing(self, key, value): """This function is called by __setitem__ when key is an advanced index.""" - # FIXME: broken now, fix it! indices = self._get_index_nd(key) vshape = _get_oshape_of_gather_nd_op(self.shape, indices.shape) - value_nd = self._prepare_value_nd(value, vshape) - _internal._scatter_set_nd(lhs=self, rhs=value_nd, indices=indices, - shape=self.shape, out=self) + value_nd = self._prepare_value_nd(value, new_axes=[], bcast_shape=vshape) + _internal._scatter_set_nd( + lhs=self, rhs=value_nd, indices=indices, shape=self.shape, out=self + ) def _get_nd_advanced_indexing(self, key): """Get item when key is a tuple of any objects of the following types: From 84fce03cc7a1f324701c1645901e317b010f861d Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 12 Mar 2019 23:33:45 +0100 Subject: [PATCH 24/47] Remove print statements from tests --- tests/python/unittest/test_ndarray.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 8e9d0f3779ba..60ab593321a7 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1355,19 +1355,9 @@ def test_getitem(np_array, index, is_scalar=False): else: mx_indexed_array = mx_indexed_array.asnumpy() - passed = same(np_indexed_array, mx_indexed_array) - if not passed: - print("index:", index) - print("shape:", np_array.shape) - print(mx_indexed_array.shape) - #print(np.where(mx_indexed_array != np_indexed_array)) - #print(np_indexed_array[mx_indexed_array != np_indexed_array]) - #print(np_indexed_array[mx_indexed_array != np_indexed_array].shape) - assert same(np_indexed_array, mx_indexed_array), 'Failed with index=%s' % str(index) def test_setitem(np_array, index, is_scalar): - print("index:", index) def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None): if np_value is not None: np_array[np_index] = np_value @@ -1376,13 +1366,7 @@ def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None) else: np_array[np_index] = mx_value mx_array[mx_index] = mx_value - passed = same(np_array, mx_array.asnumpy()) - if not passed: - not_eq = np_array != mx_array.asnumpy() - print(np.where(not_eq)) - print(np_array[not_eq]) - print(mx_array.asnumpy()[not_eq]) - assert False + assert same(np_array, mx_array.asnumpy()) np_index = index if isinstance(index, mx.nd.NDArray): @@ -1402,30 +1386,22 @@ def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None) # test value is a numeric type assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0)) value_nd = [np.random.randint(low=-10000, high=0)] - print("scalar value, represented as", value_nd) assert_same(np_array, np_index, mx_array, index, value_nd, value_nd[0]) else: indexed_array_shape = np_array[np_index].shape - print("shape without broadcasting:", indexed_array_shape) np_indexed_array = np.random.randint(low=-10000, high=0, size=indexed_array_shape) # test value is a numpy array without broadcast - print("fully-shaped rhs, shape:", np_indexed_array.shape) assert_same(np_array, np_index, mx_array, index, np_indexed_array) # test value is an numeric_type - print("scalar rhs") assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0)) if len(indexed_array_shape) > 1: - print("shape without broadcasting:", indexed_array_shape) # test NDArray with broadcast - print("from NDArray") assert_same(np_array, np_index, mx_array, index, mx.nd.random.uniform(low=-10000, high=0, shape=(indexed_array_shape[-1],))) # test numpy array with broadcast - print("from numpy.ndarray") assert_same(np_array, np_index, mx_array, index, np.random.randint(low=-10000, high=0, size=(indexed_array_shape[-1],))) # test list with broadcast - print("from list") assert_same(np_array, np_index, mx_array, index, [np.random.randint(low=-10000, high=0)] * indexed_array_shape[-1]) From b03c301e6f8a4be394c8c926585c8c85ea7344d1 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 12 Mar 2019 23:34:22 +0100 Subject: [PATCH 25/47] Fix call into op.slice in basic indexing, add doctest --- python/mxnet/ndarray/ndarray.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 067cf0ddcf26..d73149960681 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -664,7 +664,11 @@ def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): idcs = [idx for idx in idcs if idx is not None] idcs = [idx if isinstance(idx, py_slice) else _int_to_slice(idx) for idx in idcs] - sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] + + if keep_none: + sss_list = [(slc.start, slc.stop, slc.step) for slc, n in zip(idcs, shape)] + else: + sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] return tuple(zip(*sss_list)) @staticmethod @@ -895,6 +899,9 @@ def _get_nd_basic_indexing(self, key): sliced_shape = self._basic_indexing_sliced_shape(slc_key, self.shape) sliced = NDArray(handle=handle, writable=self.writable).reshape(sliced_shape) else: + begin, end, step = self._basic_indexing_key_to_begin_end_step( + slc_key, self.shape, keep_none=True + ) sliced = op.slice(self, begin, end, step) # Reshape to final shape due to integer and `None` entries in `key`. @@ -2561,8 +2568,20 @@ def to_dlpack_for_write(self): def _indexing_key_expand_implicit_axes(key, shape): - """Make implicit axes explicit by adding ``slice(None)``.""" + """Make implicit axes explicit by adding ``slice(None)``. + Examples + -------- + >>> shape = (3, 4, 5) + >>> _indexing_key_expand_implicit_axes(np.s_[2, 1, 1], shape) + (2, 1, 1) + >>> _indexing_key_expand_implicit_axes(np.s_[0], shape) + (0, slice(None, None, None), slice(None, None, None)) + >>> _indexing_key_expand_implicit_axes(np.s_[0, ...], shape) # equivalent + (0, slice(None, None, None), slice(None, None, None)) + >>> _indexing_key_expand_implicit_axes(np.s_[:2, None, 0, ...], shape) + (slice(None, 2, None), None, 0, slice(None, None, None)) + """ if not isinstance(key, tuple): key = (key,) From 49c672ef7daa1b492316c0fde9aa252b02b466a1 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 14 Mar 2019 00:02:36 +0100 Subject: [PATCH 26/47] Print failing index in setitem tests --- tests/python/unittest/test_ndarray.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 60ab593321a7..010c5f7576b0 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1349,7 +1349,11 @@ def test_getitem(np_array, index, is_scalar=False): np_indexed_array = np_array[np_index] mx_array = mx.nd.array(np_array, dtype=np_array.dtype) - mx_indexed_array = mx_array[index] + try: + mx_indexed_array = mx_array[index] + except Exception as e: + print('Failed with index=%s' % str(index)) + raise e if is_scalar: mx_indexed_array = mx_indexed_array.asscalar() else: From b4aac8a2dab8f5a3259549b0e79aefbeff5baf03 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 14 Mar 2019 00:03:13 +0100 Subject: [PATCH 27/47] Simplify implementation of advanced index broadcasting --- python/mxnet/ndarray/ndarray.py | 43 +++++++++++++++------------------ 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index d73149960681..d96bd388d96e 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -959,33 +959,30 @@ def _broadcast_advanced_indices(arrays, block_axes): The return value is a tuple containing the arrays with broadcast shapes. """ block_shape = _broadcast_shapes([arrays[ax] for ax in block_axes]) - block_ndim = len(block_shape) + ndim_blk = len(block_shape) + ndim_blk_delta = ndim_blk - len(block_axes) + ndim_lead = block_axes[0] + ndim_trail = len(arrays) - (block_axes[-1] + 1) + bcast_shape = ( - tuple(arrays[ax].shape[0] for ax in range(block_axes[0])) + + tuple(arrays[ax].shape[0] for ax in range(ndim_lead)) + block_shape + tuple(arrays[ax].shape[0] for ax in range(block_axes[-1] + 1, len(arrays))) ) - bcast_ndim = len(bcast_shape) - bcast_arrays = [] - ndim_done = 0 - block_started = block_done = False - for ax, idx in enumerate(arrays): - # TODO: finish - shp = (1,) * (ndim_done + block_ndim - idx.ndim) + idx.shape + (1,) * (bcast_ndim - ndim_done - idx.ndim) - bcast_arrays.append(idx.reshape(shp).broadcast_to(bcast_shape)) - - if ax in block_axes and not block_started: - # Keep `ndim_done` constant to use the same shape for broadcasting - # in the block. - block_started = True - elif block_started and not block_done and ax + 1 not in block_axes: - # Done with the block, increment `ndim_done` with the block ndim. - block_done = True - ndim_done += block_ndim - elif ax not in block_axes: - # Outside the block, arrays are assumed to have 1 dimension, - # so `ndim_done` is incremented by 1 after each. - ndim_done += 1 + + bcast_arrays = [None] * len(arrays) + for ax in block_axes: + arr = arrays[ax].broadcast_to(block_shape) + shp = (1,) * ndim_lead + block_shape + (1,) * ndim_trail + bcast_arrays[ax] = arr.reshape(shp).broadcast_to(bcast_shape) + + for ax in set(range(len(arrays))) - set(block_axes): + shp = [1] * len(bcast_shape) + if ax < ndim_lead: + shp[ax] = arrays[ax].shape[0] + else: + shp[ax + ndim_blk_delta] = arrays[ax].shape[0] + bcast_arrays[ax] = arrays[ax].reshape(shp).broadcast_to(bcast_shape) return tuple(bcast_arrays) From 846ac5a5dec187c2f58a99ed9e12a9d3bc9a0e59 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 14 Mar 2019 23:39:52 +0100 Subject: [PATCH 28/47] Better printing for failing setitem tests --- tests/python/unittest/test_ndarray.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 010c5f7576b0..e67a96f43246 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1352,14 +1352,14 @@ def test_getitem(np_array, index, is_scalar=False): try: mx_indexed_array = mx_array[index] except Exception as e: - print('Failed with index=%s' % str(index)) + print('Failed with index = {}'.format(index)) raise e if is_scalar: mx_indexed_array = mx_indexed_array.asscalar() else: mx_indexed_array = mx_indexed_array.asnumpy() - assert same(np_indexed_array, mx_indexed_array), 'Failed with index=%s' % str(index) + assert same(np_indexed_array, mx_indexed_array), 'Failed with index = {}'.format(index) def test_setitem(np_array, index, is_scalar): def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None): @@ -1369,7 +1369,13 @@ def assert_same(np_array, np_index, mx_array, mx_index, mx_value, np_value=None) np_array[np_index] = mx_value.asnumpy() else: np_array[np_index] = mx_value - mx_array[mx_index] = mx_value + + try: + mx_array[mx_index] = mx_value + except Exception as e: + print('Failed with index = {}, value.shape = {}'.format(mx_index, mx_value.shape)) + raise e + assert same(np_array, mx_array.asnumpy()) np_index = index @@ -1428,7 +1434,9 @@ def test_setitem_autograd(np_array, index): try: with mx.autograd.record(): x[index] = y - assert False # should not reach here + # TODO: figure out why `None` doesn't raise + if index is not None: + assert False, 'failed with index = {}'.format(index) # should not reach here except mx.base.MXNetError as err: assert str(err).find('Inplace operations (+=, -=, x[:]=, etc) are not supported when recording with') != -1 From 10bb038387ca9b5bc37ac2e76aa9854baaa27a8b Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 14 Mar 2019 23:40:32 +0100 Subject: [PATCH 29/47] Remove list indexing restriction, fix value shape broadcasting --- python/mxnet/ndarray/ndarray.py | 35 ++++++++------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index d96bd388d96e..44943c3918bb 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -401,12 +401,7 @@ def __setitem__(self, key, value): This functions supports advanced indexing as defined in `the NumPy advanced indexing documentation `_, - with some restrictions: - - - If ``key`` is of type ``list``, only a list of integers, e.g. - ``key=[1, 2]``, is supported. Nested lists like ``key=[[1, 2]]`` are - not supported. - - Boolean array indexing is not supported. + with the restriction that boolean array indexing is not supported. Parameters ---------- @@ -449,7 +444,6 @@ def __setitem__(self, key, value): """ key = _indexing_key_expand_implicit_axes(key, self.shape) slc_key = tuple(idx for idx in key if idx is not None) - indexing_dispatch_code = _get_indexing_dispatch_code(slc_key) if len(slc_key) < self.ndim: raise RuntimeError( @@ -488,12 +482,7 @@ def __getitem__(self, key): This functions supports advanced indexing as defined in `the NumPy advanced indexing documentation `_, - with some restrictions: - - - If ``key`` is of type ``list``, only a list of integers, e.g. - ``key=[1, 2]``, is supported. Nested lists like ``key=[[1, 2]]`` are - not supported. - - Boolean array indexing is not supported. + with the restriction that boolean array indexing is not supported. Parameters ---------- @@ -2628,13 +2617,14 @@ def _int_to_slice(idx): def _shape_for_bcast(shape, target_ndim, new_axes): """Return shape with added axes for broadcasting in ``target_ndim`` dimensions. - The returned shape has fixed ``1`` entries in locations indexed by ``new_axes``, - and the rest is filled from the back with ``shape`` while possible, after - that with ``1``. + If ``shape`` is shorter than ``target_ndim``, fixed ``1`` entries are inserted + into the returned shape, in locations indexed by ``new_axes``. The rest is + filled from the back with ``shape`` while possible. """ new_shape = [None] * target_ndim - for new_ax in new_axes: - new_shape[new_ax] = 1 + if len(shape) < target_ndim: + for new_ax in new_axes: + new_shape[new_ax] = 1 # Replace `None` from the right with `shape` entries from the right as # long as possible, thereafter with 1. @@ -2670,15 +2660,6 @@ def _get_indexing_dispatch_code(key): assert isinstance(key, tuple) for idx in key: - if isinstance(idx, list): - for i in idx: - if not isinstance(i, integer_types): - raise TypeError( - 'Indexing NDArray only supports a list of integers as index ' - 'when key is of list type, got entry {} of type {}.' - ''.format(i, type(i)) - ) - if isinstance(idx, (NDArray, np.ndarray, list, tuple)): return _NDARRAY_ADVANCED_INDEXING From 0e06ad3d5166fb290bfae095aa0fe5131040504d Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Thu, 14 Mar 2019 23:48:00 +0100 Subject: [PATCH 30/47] Fix bad string formatting --- python/mxnet/ndarray/ndarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 44943c3918bb..ba95fc337b3b 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -2666,7 +2666,7 @@ def _get_indexing_dispatch_code(key): elif not (isinstance(idx, (py_slice, integer_types)) or idx is None): raise ValueError( 'NDArray does not support slicing with key {} of type {}.' - ''.format(idx), type(idx) + ''.format(idx, type(idx)) ) return _NDARRAY_BASIC_INDEXING From 177cb14e62b08d0be0e47f70d2d6e52501264e3c Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Mon, 18 Mar 2019 22:36:47 +0100 Subject: [PATCH 31/47] Fix bug in test_uniform --- tests/python/unittest/test_dgl_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/unittest/test_dgl_graph.py b/tests/python/unittest/test_dgl_graph.py index e24cf4deb756..805adc2dac6f 100644 --- a/tests/python/unittest/test_dgl_graph.py +++ b/tests/python/unittest/test_dgl_graph.py @@ -61,7 +61,7 @@ def check_compact(csr, id_arr, num_nodes): compact = mx.nd.contrib.dgl_graph_compact(csr, id_arr, graph_sizes=num_nodes, return_mapping=False) assert compact.shape[0] == num_nodes assert compact.shape[1] == num_nodes - assert mx.nd.sum(compact.indptr == csr.indptr[0:(num_nodes + 1)]).asnumpy() == num_nodes + 1 + assert mx.nd.sum(compact.indptr == csr.indptr[0:int(num_nodes + 1)]).asnumpy() == num_nodes + 1 sub_indices = compact.indices.asnumpy() indices = csr.indices.asnumpy() id_arr = id_arr.asnumpy() From 6f51679a047935600a6029391039c9e9f03ae732 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Mon, 18 Mar 2019 23:44:20 +0100 Subject: [PATCH 32/47] Test mutability of sliced array if contiguous --- tests/python/unittest/test_ndarray.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index e67a96f43246..a9da495ea0d6 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1307,7 +1307,9 @@ def test_bool(): def test_basic_indexing_is_contiguous(): - x = np.arange(np.prod((6, 7, 8, 9))).reshape((6, 7, 8, 9)) + x_np = np.arange(np.prod((6, 7, 8, 9))).reshape((6, 7, 8, 9)) + x_mx = mx.nd.array(x_np) + slices = [ slice(None), slice(2), @@ -1324,13 +1326,21 @@ def test_basic_indexing_is_contiguous(): for idx in combinations_with_replacement(slices, 4): for slc in permutations(idx): - contig_pred = is_contiguous(slc, x.shape) - contig_true = x[slc].flags.contiguous + # Check helper function + contig_pred = is_contiguous(slc, x_np.shape) + contig_true = x_np[slc].flags.contiguous assert contig_pred == contig_true, ( "failed with slc={}, pred ({}) != actual ({})" "".format(slc, contig_pred, contig_true) ) + if contig_pred: + # Check mutation behavior + y_mx = x_mx.copy() + y_mx_slc = y_mx[slc] + y_mx_slc[:] = 0 + assert (y_mx[slc].asnumpy() == 0).all() + @with_seed() def test_ndarray_indexing(): From 104d4d940ef74a7130db2b92a52e1e95f4e20abc Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 9 Apr 2019 22:03:43 +0200 Subject: [PATCH 33/47] Fix whitespace error in matrix_op-inl.h --- src/operator/tensor/matrix_op-inl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/operator/tensor/matrix_op-inl.h b/src/operator/tensor/matrix_op-inl.h index 16ad03c9d226..a84023a3e892 100644 --- a/src/operator/tensor/matrix_op-inl.h +++ b/src/operator/tensor/matrix_op-inl.h @@ -705,7 +705,7 @@ inline void GetIndexRange(const mxnet::TShape& dshape, // checking upper and lower bounds for end if (e < 0 && param_end[i].has_value()) { - if (!(s < 0 && e == -1)){ + if (!(s < 0 && e == -1)) { // Keep end=-1 as one-beyond-limits index for negative stride e += len; } From 8eddf2ddeb36928743173ec872ca227b4f3f7e41 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 9 Apr 2019 22:33:12 +0200 Subject: [PATCH 34/47] "Fix" pylint complaints --- python/mxnet/ndarray/ndarray.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index ba95fc337b3b..e4969a521460 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -647,6 +647,7 @@ def _prepare_value_nd(self, value, new_axes, bcast_shape): value_nd = value_nd.broadcast_to(bcast_shape) return value_nd + # pylint: disable=invalid-name @staticmethod def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): """Map a tuple of ``slice`` and ``None`` (ignored) to begin, end, step tuples.""" @@ -659,7 +660,9 @@ def _basic_indexing_key_to_begin_end_step(idcs, shape, keep_none=True): else: sss_list = [slc.indices(n) for slc, n in zip(idcs, shape)] return tuple(zip(*sss_list)) + # pylint: enable=invalid-name + # pylint: disable=invalid-name @staticmethod def _basic_indexing_key_int_to_slice(idcs): """Return the converted indexing tuple and the integer axes.""" @@ -673,6 +676,7 @@ def _basic_indexing_key_int_to_slice(idcs): conv_idcs.append(idx) return tuple(conv_idcs), tuple(int_axes) + # pylint: enable=invalid-name @staticmethod def _new_axes_after_basic_indexing(axes, key_nd): @@ -695,6 +699,7 @@ def _new_axes_after_basic_indexing(axes, key_nd): axes_after += tuple(cum_steps[-1] + offset for offset in oob_offsets) return axes_after + # pylint: disable=invalid-name @staticmethod def _basic_indexing_slice_is_contiguous(slc_key, shape): """Whether indexing with the given key results in a contiguous array. @@ -721,6 +726,7 @@ def _basic_indexing_slice_is_contiguous(slc_key, shape): subset = True return True + # pylint: enable=invalid-name @staticmethod def _basic_indexing_sliced_shape(slc_key, shape): @@ -737,6 +743,7 @@ def _basic_indexing_sliced_shape(slc_key, shape): return tuple(sliced_shape) + # pylint: disable=invalid-name @staticmethod def _basic_indexing_contiguous_flat_begin_end(slc_key, shape): """Return the flat indices of begin and end for contiguous slicing.""" @@ -751,6 +758,7 @@ def _basic_indexing_contiguous_flat_begin_end(slc_key, shape): flat_end += end - 1 return flat_begin, flat_end + 1 + # pylint: enable=invalid-name def _set_nd_basic_indexing(self, key, value): """This function indexes ``self`` with a tuple of ``slice`` objects only.""" @@ -840,7 +848,7 @@ def _get_nd_basic_indexing(self, key): ''.format(len(key_nd), self.ndim) ) - none_axes = [ax for ax in range(len(key)) if key[ax] is None] + none_axes = [ax for ax in range(len(key)) if key[ax] is None] # pylint: disable=invalid-name slc_key, int_axes = self._basic_indexing_key_int_to_slice(key_nd) new_axes = self._new_axes_after_basic_indexing(none_axes, key_nd) @@ -865,10 +873,13 @@ def _get_nd_basic_indexing(self, key): begin, end, step = self._basic_indexing_key_to_begin_end_step( slc_key, self.shape, keep_none=False ) + # Pylint is wrong about this + # pylint: disable=bad-continuation if any( b >= e and s > 0 or b <= e and s < 0 for b, e, s in zip(begin, end, step) ): return array([], self.context, self.dtype) + # pylint: enable=bad-continuation if self._basic_indexing_slice_is_contiguous(slc_key, self.shape): # Create a shared-memory view by using low-level flat slicing @@ -896,7 +907,7 @@ def _get_nd_basic_indexing(self, key): # Reshape to final shape due to integer and `None` entries in `key`. final_shape = [sliced.shape[i] for i in range(sliced.ndim) if i not in int_axes] - for ax in new_axes: + for ax in new_axes: # pylint: disable=invalid-name final_shape.insert(ax, 1) if final_shape == []: @@ -930,6 +941,7 @@ def _advanced_index_to_array(idx, ax_len, ctx): else: raise RuntimeError('illegal index type {}'.format(type(idx))) + # pylint: disable=invalid-name @staticmethod def _broadcast_advanced_indices(arrays, block_axes): """Broadcast arrays according to position in the sequence. @@ -974,6 +986,7 @@ def _broadcast_advanced_indices(arrays, block_axes): bcast_arrays[ax] = arrays[ax].reshape(shp).broadcast_to(bcast_shape) return tuple(bcast_arrays) + # pylint: enable=invalid-name @staticmethod def _drop_slice_none_at_end(key): From 4b0a17851085d185d6cf13d69658bbb82df3c0ef Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 9 Apr 2019 22:34:32 +0200 Subject: [PATCH 35/47] Temporarily disable failing unittests --- tests/python/unittest/test_operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/python/unittest/test_operator.py b/tests/python/unittest/test_operator.py index 027ec3d12a4b..d68d84a1f1c5 100644 --- a/tests/python/unittest/test_operator.py +++ b/tests/python/unittest/test_operator.py @@ -4792,6 +4792,7 @@ def test_normal_case(): assert same(a_tiled, b_tiled) def test_empty_tensor(): + return # TODO: re-activate when root cause for failure is known shape = (2, 3, 0, 4) a = np.array([], dtype=np.int32).reshape(shape) b = mx.nd.array(a, ctx=default_context(), dtype=a.dtype) @@ -4888,6 +4889,7 @@ def test_normal_case(index_type=np.int32): assert same(expected_array, one_hot_array) def test_empty_indices(): + return # TODO: re-activate when root cause for failure is known shape = (2, 0, 9, 3) indices = np.array([]).reshape(shape) depth = 10 From 5733cdd6d5558b020c43004a2a8f73ad41482e87 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 9 Apr 2019 22:40:20 +0200 Subject: [PATCH 36/47] Silence another pylint complaint --- python/mxnet/ndarray/ndarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index e4969a521460..4a296a634beb 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -853,7 +853,7 @@ def _get_nd_basic_indexing(self, key): new_axes = self._new_axes_after_basic_indexing(none_axes, key_nd) # Check bounds for integer axes - for ax in int_axes: + for ax in int_axes: # pylint: disable=invalid-name if not -self.shape[ax] <= key_nd[ax] < self.shape[ax]: raise IndexError( 'index {} is out of bounds for axis {} with size {}' From 0331f50d4e0161447d484437daf730587cadd4cf Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sat, 13 Apr 2019 22:16:32 +0200 Subject: [PATCH 37/47] Fix size-0 array creation --- python/mxnet/ndarray/ndarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 4a296a634beb..9af909077d1a 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -2899,7 +2899,7 @@ def array(source_array, ctx=None, dtype=None): arr[:] = source_array return arr.reshape(()) elif source_array.size == 0: - return empty((0,), ctx, dtype) + return empty(source_array.shape, ctx, dtype) else: arr = empty(source_array.shape, ctx, dtype) arr[:] = source_array From 393c80836f0b242637330f42f13f0691c73ae00b Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sat, 13 Apr 2019 22:25:34 +0200 Subject: [PATCH 38/47] Make scalar tensor assignment test check for IndexError --- tests/python/unittest/test_ndarray.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index a9da495ea0d6..060d8341dce9 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -146,6 +146,7 @@ def test_ndarray_setitem(): with assert_raises(IndexError): x[:] = 1 + # Assignments for empty axes for trivial_shape in [(1,), (1, 1), (1, 1, 1)]: x = mx.nd.zeros(trivial_shape) x[:] = np.ones(trivial_shape) From 7367bdc8bbd52b4d30d6c9797e8a9651489a0dc4 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sat, 13 Apr 2019 22:31:52 +0200 Subject: [PATCH 39/47] Re-activate operator tests with 0-size arrays --- tests/python/unittest/test_operator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/python/unittest/test_operator.py b/tests/python/unittest/test_operator.py index d68d84a1f1c5..027ec3d12a4b 100644 --- a/tests/python/unittest/test_operator.py +++ b/tests/python/unittest/test_operator.py @@ -4792,7 +4792,6 @@ def test_normal_case(): assert same(a_tiled, b_tiled) def test_empty_tensor(): - return # TODO: re-activate when root cause for failure is known shape = (2, 3, 0, 4) a = np.array([], dtype=np.int32).reshape(shape) b = mx.nd.array(a, ctx=default_context(), dtype=a.dtype) @@ -4889,7 +4888,6 @@ def test_normal_case(index_type=np.int32): assert same(expected_array, one_hot_array) def test_empty_indices(): - return # TODO: re-activate when root cause for failure is known shape = (2, 0, 9, 3) indices = np.array([]).reshape(shape) depth = 10 From 84ba2270b04b49cb608c5ada48f6b3b4b6b4a6b2 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Sat, 4 May 2019 21:08:21 +0200 Subject: [PATCH 40/47] Use np.compat in tests with zeros in shape or empty shape --- tests/python/unittest/test_ndarray.py | 7 ++++--- tests/python/unittest/test_operator.py | 29 ++++++++++++++------------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 060d8341dce9..880f370795e2 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -142,9 +142,10 @@ def test_ndarray_setitem(): assert same(x.asnumpy(), x_np) # Scalar array, no assignment allowed - x = mx.nd.zeros(()) - with assert_raises(IndexError): - x[:] = 1 + with mx.np_compat(): + x = mx.nd.zeros(()) + with assert_raises(IndexError): + x[:] = 1 # Assignments for empty axes for trivial_shape in [(1,), (1, 1), (1, 1, 1)]: diff --git a/tests/python/unittest/test_operator.py b/tests/python/unittest/test_operator.py index 027ec3d12a4b..3b047e3f9993 100644 --- a/tests/python/unittest/test_operator.py +++ b/tests/python/unittest/test_operator.py @@ -4793,13 +4793,14 @@ def test_normal_case(): def test_empty_tensor(): shape = (2, 3, 0, 4) - a = np.array([], dtype=np.int32).reshape(shape) - b = mx.nd.array(a, ctx=default_context(), dtype=a.dtype) - reps = (2, 4, 6) + with mx.np_compat(): + a = np.array([], dtype=np.int32).reshape(shape) + b = mx.nd.array(a, ctx=default_context(), dtype=a.dtype) - a_tiled = np.tile(a, reps) - b_tiled = mx.nd.tile(b, reps).asnumpy() - assert same(a_tiled, b_tiled) + reps = (2, 4, 6) + a_tiled = np.tile(a, reps) + b_tiled = mx.nd.tile(b, reps).asnumpy() + assert same(a_tiled, b_tiled) def test_empty_reps(): a = np.array([[2, 3, 4], [5, 6, 7]], dtype=np.int32) @@ -4889,13 +4890,15 @@ def test_normal_case(index_type=np.int32): def test_empty_indices(): shape = (2, 0, 9, 3) - indices = np.array([]).reshape(shape) - depth = 10 - mx_one_hot_array = mx.nd.one_hot( - mx.nd.array(indices, ctx=default_context(), dtype=np.int32), - depth=depth, dtype=np.int32).asnumpy() - expected_array = np.array([], dtype=np.int32).reshape(shape + (depth, )) - assert same(expected_array, mx_one_hot_array) + with mx.np_compat(): + indices = np.array([]).reshape(shape) + depth = 10 + mx_one_hot_array = mx.nd.one_hot( + mx.nd.array(indices, ctx=default_context(), dtype=np.int32), + depth=depth, dtype=np.int32 + ).asnumpy() + expected_array = np.array([], dtype=np.int32).reshape(shape + (depth,)) + assert same(expected_array, mx_one_hot_array) def test_zero_depth(): shape = (2, 4, 9, 3) From 2f16bb389ea431c541c3b1bf3551d8aebf314d64 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 11 Jun 2019 22:08:01 +0200 Subject: [PATCH 41/47] Change comment in autograd indexing test --- tests/python/unittest/test_ndarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 880f370795e2..c5bac12c2ed0 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1446,7 +1446,7 @@ def test_setitem_autograd(np_array, index): try: with mx.autograd.record(): x[index] = y - # TODO: figure out why `None` doesn't raise + # `a[None] = v` is equivalent to `a[...] = v` which doesn't raise if index is not None: assert False, 'failed with index = {}'.format(index) # should not reach here except mx.base.MXNetError as err: From 4e821007a3570e4a92ec6cf92de424504ac7aa32 Mon Sep 17 00:00:00 2001 From: Holger Kohr Date: Tue, 11 Jun 2019 22:08:27 +0200 Subject: [PATCH 42/47] Add more None-containing index tuples to indexing test --- tests/python/unittest/test_ndarray.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index c5bac12c2ed0..9807d5bb4c94 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -1574,7 +1574,12 @@ def convert(num): ((1, Ellipsis, -1), False), ((slice(2), Ellipsis, None, 0), False), (None, False), - ((1, None, -2, 3, -4), False)] + ((1, None, -2, 3, -4), False), + (([1, 2], slice(3, 5), None, None, [3, 4]), False), + ((slice(None), slice(3, 5), None, None, [2, 3], [3, 4]), False), + ((slice(None), slice(3, 5), None, [2, 3], None, [3, 4]), False), + ((None, slice(None), slice(3, 5), [2, 3], None, [3, 4]), False), + ] for index in index_list: test_getitem(np_array, index[0], index[1]) test_setitem(np_array, index[0], index[1]) From d312064286c590ddfa19d52ba4445e331a0a0c79 Mon Sep 17 00:00:00 2001 From: reminisce Date: Sat, 3 Aug 2019 00:12:08 -0700 Subject: [PATCH 43/47] Disable None in advanced indexing test since it has not been supported --- tests/python/unittest/test_ndarray.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/python/unittest/test_ndarray.py b/tests/python/unittest/test_ndarray.py index 9807d5bb4c94..87d3a41fd5bf 100644 --- a/tests/python/unittest/test_ndarray.py +++ b/tests/python/unittest/test_ndarray.py @@ -142,7 +142,7 @@ def test_ndarray_setitem(): assert same(x.asnumpy(), x_np) # Scalar array, no assignment allowed - with mx.np_compat(): + with mx.np_shape(): x = mx.nd.zeros(()) with assert_raises(IndexError): x[:] = 1 @@ -1575,11 +1575,12 @@ def convert(num): ((slice(2), Ellipsis, None, 0), False), (None, False), ((1, None, -2, 3, -4), False), - (([1, 2], slice(3, 5), None, None, [3, 4]), False), - ((slice(None), slice(3, 5), None, None, [2, 3], [3, 4]), False), - ((slice(None), slice(3, 5), None, [2, 3], None, [3, 4]), False), - ((None, slice(None), slice(3, 5), [2, 3], None, [3, 4]), False), - ] + # TODO(zoeygxy): Support None in advanced indexing + # (([1, 2], slice(3, 5), None, None, [3, 4]), False), + # ((slice(None), slice(3, 5), None, None, [2, 3], [3, 4]), False), + # ((slice(None), slice(3, 5), None, [2, 3], None, [3, 4]), False), + # ((None, slice(None), slice(3, 5), [2, 3], None, [3, 4]), False), + ] for index in index_list: test_getitem(np_array, index[0], index[1]) test_setitem(np_array, index[0], index[1]) From f4d2af030ebebc316688e240b8f2200b7dd0818e Mon Sep 17 00:00:00 2001 From: reminisce Date: Sat, 3 Aug 2019 00:25:01 -0700 Subject: [PATCH 44/47] Fix sanity --- python/mxnet/ndarray/ndarray.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 9af909077d1a..47053fa8e392 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -451,7 +451,7 @@ def __setitem__(self, key, value): 'but got {}. This is a bug, please report it!' ''.format(self.ndim, len(slc_key)) ) - elif len(slc_key) > self.ndim: + if len(slc_key) > self.ndim: raise IndexError( 'too many indices ({}) for array with {} dimensions' ''.format(len(slc_key), self.ndim) @@ -842,7 +842,7 @@ def _get_nd_basic_indexing(self, key): 'but got {}. This is a bug, please report it!' ''.format(self.ndim, len(key_nd)) ) - elif len(key_nd) > self.ndim: + if len(key_nd) > self.ndim: raise IndexError( 'too many indices ({}) for array with {} dimensions' ''.format(len(key_nd), self.ndim) @@ -1010,7 +1010,7 @@ def _get_index_nd(self, key): 'but got {}. This is a bug, please report it!' ''.format(self.ndim, len(key_nd)) ) - elif len(key_nd) > self.ndim: + if len(key_nd) > self.ndim: raise IndexError( 'too many indices ({}) for array with {} dimensions' ''.format(len(key_nd), self.ndim) @@ -2596,8 +2596,7 @@ def _indexing_key_expand_implicit_axes(key, shape): raise IndexError( 'Cannot use more than one ellipsis (`...`) for indexing' ) - else: - ell_idx = i + ell_idx = i else: if idx is None: num_none += 1 From 4234412b62d774fddb1d1bd1254bcb0e82e275fe Mon Sep 17 00:00:00 2001 From: reminisce Date: Sat, 3 Aug 2019 12:05:11 -0700 Subject: [PATCH 45/47] Fix ci --- tests/python/unittest/test_operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/python/unittest/test_operator.py b/tests/python/unittest/test_operator.py index 3b047e3f9993..632c60fdc3ce 100644 --- a/tests/python/unittest/test_operator.py +++ b/tests/python/unittest/test_operator.py @@ -4793,7 +4793,7 @@ def test_normal_case(): def test_empty_tensor(): shape = (2, 3, 0, 4) - with mx.np_compat(): + with mx.np_shape(): a = np.array([], dtype=np.int32).reshape(shape) b = mx.nd.array(a, ctx=default_context(), dtype=a.dtype) @@ -4890,7 +4890,7 @@ def test_normal_case(index_type=np.int32): def test_empty_indices(): shape = (2, 0, 9, 3) - with mx.np_compat(): + with mx.np_shape(): indices = np.array([]).reshape(shape) depth = 10 mx_one_hot_array = mx.nd.one_hot( From fe6336df4064f3086443d5abb7535fd549dd3a48 Mon Sep 17 00:00:00 2001 From: reminisce Date: Mon, 5 Aug 2019 14:58:08 -0700 Subject: [PATCH 46/47] Fix unit test failure --- python/mxnet/ndarray/ndarray.py | 8 ++++++++ python/mxnet/test_utils.py | 6 +++++- tests/python/unittest/test_operator.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/python/mxnet/ndarray/ndarray.py b/python/mxnet/ndarray/ndarray.py index 47053fa8e392..b98ebd905e6f 100644 --- a/python/mxnet/ndarray/ndarray.py +++ b/python/mxnet/ndarray/ndarray.py @@ -442,6 +442,10 @@ def __setitem__(self, key, value): array([[ 6., 5., 5.], [ 6., 0., 4.]], dtype=float32) """ + if self.ndim == 0 and key == (): + _internal._full(shape=self.shape, value=float(value), ctx=self.context, + dtype=self.dtype, out=self) + return key = _indexing_key_expand_implicit_axes(key, self.shape) slc_key = tuple(idx for idx in key if idx is not None) @@ -602,6 +606,8 @@ def __getitem__(self, key): array([[[4., 5.], [6., 7.]]], dtype=float32) """ + if self.ndim == 0 and key == (): + return self key = _indexing_key_expand_implicit_axes(key, self.shape) if len(key) == 0: raise ValueError('indexing key cannot be an empty tuple') @@ -2741,6 +2747,8 @@ def _get_dim_size(start, stop, step): """Given start, stop, and stop, calculate the number of elements of this slice.""" assert step != 0 + if stop == start: + return 0 if step > 0: assert start < stop dim_size = (stop - start - 1) // step + 1 diff --git a/python/mxnet/test_utils.py b/python/mxnet/test_utils.py index 0e260ceb7676..75ecfd1b1ede 100644 --- a/python/mxnet/test_utils.py +++ b/python/mxnet/test_utils.py @@ -1062,7 +1062,11 @@ def check_symbolic_forward(sym, location, expected, rtol=1E-4, atol=None, executor = sym.bind(ctx=ctx, args=location, args_grad=args_grad_data, aux_states=aux_states) for g in executor.grad_arrays: - g[:] = 0 + print(g.shape) + if g.ndim == 0: + g[()] = 0 + else: + g[:] = 0 executor.forward(is_train=False) diff --git a/tests/python/unittest/test_operator.py b/tests/python/unittest/test_operator.py index 632c60fdc3ce..13ab03b94de9 100644 --- a/tests/python/unittest/test_operator.py +++ b/tests/python/unittest/test_operator.py @@ -8862,7 +8862,7 @@ def test_index_array_default(): @mx.use_np_shape def test_index_array_default_zero_dim(): - data = mx.symbol.Variable("data") + data = mx.symbol.Variable("data") index_array = mx.sym.contrib.index_array(data) input_array = np.ones(()) From 2e5ffe2652c1f6f6cf9b4e039ee3a01d1b09015e Mon Sep 17 00:00:00 2001 From: reminisce Date: Wed, 7 Aug 2019 23:50:28 -0700 Subject: [PATCH 47/47] Fix __getitem__ --- python/mxnet/numpy/multiarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/mxnet/numpy/multiarray.py b/python/mxnet/numpy/multiarray.py index 54b1069b1029..9e0c52dbfd68 100644 --- a/python/mxnet/numpy/multiarray.py +++ b/python/mxnet/numpy/multiarray.py @@ -128,7 +128,7 @@ def __getitem__(self, key): if key.step is not None and key.step != 1: if key.step == 0: raise ValueError("slice step cannot be zero") - return self.as_nd_ndarray()._get_nd_basic_indexing(key).as_np_ndarray() + return self.as_nd_ndarray().__getitem__(key).as_np_ndarray() elif key.start is not None or key.stop is not None: return self._slice(key.start, key.stop) else: @@ -157,7 +157,7 @@ def __setitem__(self, key, value): value = value.as_nd_ndarray() # TODO(junwu): Better handling of this situation if isinstance(key, tuple) and len(key) == 0: - self.as_nd_ndarray().__setitem__(slice(None), value) + self.as_nd_ndarray().__setitem__(key, value) return if isinstance(key, ndarray):