Repository: chainer/chainer

Issue:
[feature] cupy: `repeat` method
I need this method to implement `forward_gpu` for https://github.com/pfnet/chainer/pull/599.



Discussion:

Resolving patch:
diff --git a/cupy/__init__.py b/cupy/__init__.py
--- a/cupy/__init__.py
+++ b/cupy/__init__.py
@@ -501,8 +501,16 @@ def take(self, indices, axis=None, out=None):
         """
         return take(self, indices, axis, out)
 
+    def repeat(self, repeats, axis=None):
+        """Returns an array with repeated arrays along an axis.
+
+        .. seealso::
+            :func:`cupy.repeat` for full documentation,
+            :meth:`numpy.ndarray.repeat`
+        """
+        return repeat(self, repeats, axis)
+
     # TODO(okuta): Implement put
-    # TODO(okuta): Implement repeat
     # TODO(okuta): Implement choose
     # TODO(okuta): Implement sort
     # TODO(okuta): Implement argsort
@@ -1208,6 +1216,8 @@ def _should_use_rop(self, a):
 split = manipulation.split.split
 vsplit = manipulation.split.vsplit
 
+repeat = manipulation.tiling.repeat
+
 # -----------------------------------------------------------------------------
 # Binary operations
 # -----------------------------------------------------------------------------
diff --git a/cupy/manipulation/tiling.py b/cupy/manipulation/tiling.py
--- a/cupy/manipulation/tiling.py
+++ b/cupy/manipulation/tiling.py
@@ -1,7 +1,68 @@
-# flake8: NOQA
-# "flake8: NOQA" to suppress warning "H104  File contains nothing but comments"
-
 # TODO(okuta): Implement tile
 
+import collections
+import six
+
+import cupy
+
+
+def repeat(a, repeats, axis=None):
+    """Repeat arrays along an axis.
+
+    Args:
+        a (cupy.ndarray): Array to transform.
+        repeats (int, list or tuple): The number of repeats.
+        axis (int): The axis to repeat.
+
+    Returns:
+        cupy.ndarray: Transformed array with repeats.
+
+    .. seealso:: :func:`numpy.repeat`
+
+    """
+    if isinstance(repeats, int):
+        if repeats < 0:
+            raise ValueError(
+                "'repeats' should not be negative: {}".format(repeats))
+        if axis is None:
+            b = a.reshape((-1, 1))
+            ret = cupy.empty((len(b), repeats), dtype=b.dtype)
+            if ret.size:
+                ret[...] = b
+            return ret.ravel()
+
+        repeats = [repeats] * a.shape[axis]
+    elif isinstance(repeats, collections.Sequence):
+        if any(rep < 0 for rep in repeats):
+            raise ValueError(
+                "all elements of 'repeats' should not be negative: {}"
+                .format(repeats))
+        if axis is None:
+            raise ValueError(
+                "'axis' should be specified if 'repeats' is sequence")
+        if a.shape[axis] != len(repeats):
+            raise ValueError(
+                "'repeats' and 'axis' of 'a' should be same length: {} != {}"
+                .format(a.shape[axis], len(repeats)))
+    else:
+        raise ValueError(
+            "'repeats' should be int or sequence: {}".format(repeats))
+
+    if axis < 0:
+        axis = len(a.shape) + axis
 
-# TODO(okuta): Implement repeat
+    ret_shape = list(a.shape)
+    ret_shape[axis] = sum(repeats)
+    ret = cupy.empty((ret_shape), dtype=a.dtype)
+    a_index = [slice(None)] * len(ret_shape)
+    ret_index = list(a_index)
+    offset = 0
+    for i in six.moves.xrange(a.shape[axis]):
+        if repeats[i] == 0:
+            continue
+        a_index[axis] = slice(i, i + 1)
+        ret_index[axis] = slice(offset, offset + repeats[i])
+        # convert to tuple because cupy has a indexing bug
+        ret[tuple(ret_index)] = a[tuple(a_index)]
+        offset += repeats[i]
+    return ret


Test patch:
diff --git a/tests/cupy_tests/manipulation_tests/test_tiling.py b/tests/cupy_tests/manipulation_tests/test_tiling.py
--- a/tests/cupy_tests/manipulation_tests/test_tiling.py
+++ b/tests/cupy_tests/manipulation_tests/test_tiling.py
@@ -1,9 +1,41 @@
 import unittest
 
+import cupy
 from cupy import testing
 
 
+@testing.parameterize(
+    {'repeats': 0, 'axis': None},
+    {'repeats': 2, 'axis': None},
+    {'repeats': 2, 'axis': 1},
+    {'repeats': 2, 'axis': -1},
+    {'repeats': [0, 0, 0], 'axis': 1},
+    {'repeats': [1, 2, 3], 'axis': 1},
+    {'repeats': [1, 2, 3], 'axis': -2},
+)
 @testing.gpu
-class TestTiling(unittest.TestCase):
+class TestRepeat(unittest.TestCase):
 
     _multiprocess_can_split_ = True
+
+    @testing.numpy_cupy_array_equal()
+    def test_array_repeat(self, xp):
+        x = testing.shaped_arange((2, 3, 4), xp)
+        return xp.repeat(x, self.repeats, self.axis)
+
+
+@testing.parameterize(
+    {'repeats': -3, 'axis': None},
+    {'repeats': [-3, -3], 'axis': 0},
+    {'repeats': [1, 2, 3], 'axis': None},
+    {'repeats': [1, 2], 'axis': 1},
+)
+@testing.gpu
+class TestRepeatFailure(unittest.TestCase):
+
+    _multiprocess_can_split_ = True
+
+    @testing.numpy_cupy_raises()
+    def test_repeat_failure(self):
+        x = testing.shaped_arange((2, 3, 4))
+        cupy.repeat(x, -3)