Repository: cupy/cupy

Issue:
[Feature Request] Equivalent of numpy.isin
As a user, I'd like to be able to quickly and easily do the equivalent of `numpy.isin` with cupy.

To quote the NumPy docs, "isin is an element-wise function version of the python keyword `in`. isin(a, b) is roughly equivalent to np.array([item in b for item in a]) if a and b are 1-D sequences."

The NumPy documentation can be found [here](https://docs.scipy.org/doc/numpy/reference/generated/numpy.isin.html), and the source code [here](https://github.com/numpy/numpy/blob/v1.16.1/numpy/lib/arraysetops.py#L601-L697).


```python
import numpy as np
​
arr = np.array([0, 4, 5])
np.isin(arr, [0,3])
# array([ True, False, False])
​
arr = np.array([0, 4, 5])
other = [0, 3]
np.isin(arr, other)
# array([ True, False, False])
​
arr = np.array([[0, 4, 5],
                [12, 4, 3]])
np.isin(arr, [0,3])
# array([[ True, False, False],
#       [False, False,  True]])
```


Discussion:
I'll work on this

Resolving patch:
diff --git a/cupy/__init__.py b/cupy/__init__.py
--- a/cupy/__init__.py
+++ b/cupy/__init__.py
@@ -479,12 +479,18 @@ def base_repr(number, base=2, padding=0):  # NOQA (needed to avoid redefinition
 from cupy.logic.content import isinf  # NOQA
 from cupy.logic.content import isnan  # NOQA
 
+from cupy.logic.truth import in1d  # NOQA
+from cupy.logic.truth import isin  # NOQA
+
 from cupy.logic.type_test import iscomplex  # NOQA
 from cupy.logic.type_test import iscomplexobj  # NOQA
 from cupy.logic.type_test import isfortran  # NOQA
 from cupy.logic.type_test import isreal  # NOQA
 from cupy.logic.type_test import isrealobj  # NOQA
 
+from cupy.logic.truth import in1d  # NOQA
+from cupy.logic.truth import isin  # NOQA
+
 
 def isscalar(num):
     """Returns True if the type of num is a scalar type.
diff --git a/cupy/logic/truth.py b/cupy/logic/truth.py
--- a/cupy/logic/truth.py
+++ b/cupy/logic/truth.py
@@ -57,3 +57,67 @@ def any(a, axis=None, out=None, keepdims=False):
 
     assert isinstance(a, cupy.ndarray)
     return a.any(axis=axis, out=out, keepdims=keepdims)
+
+
+def in1d(ar1, ar2, assume_unique=False, invert=False):
+    """Tests whether each element of a 1-D array is also present in a second
+    array.
+
+    Returns a boolean array the same length as ``ar1`` that is ``True``
+    where an element of ``ar1`` is in ``ar2`` and ``False`` otherwise.
+
+    Args:
+        ar1 (cupy.ndarray): Input array.
+        ar2 (cupy.ndarray): The values against which to test each value of
+            ``ar1``.
+        assume_unique (bool, optional): Ignored
+        invert (bool, optional): If ``True``, the values in the returned array
+            are inverted (that is, ``False`` where an element of ``ar1`` is in
+            ``ar2`` and ``True`` otherwise). Default is ``False``.
+
+    Returns:
+        cupy.ndarray, bool: The values ``ar1[in1d]`` are in ``ar2``.
+
+    """
+    # Ravel both arrays, behavior for the first array could be different
+    ar1 = ar1.ravel()
+    ar2 = ar2.ravel()
+    if ar1.size == 0 or ar2.size == 0:
+        if invert:
+            return cupy.ones(ar1.shape, dtype=cupy.bool_)
+        else:
+            return cupy.zeros(ar1.shape, dtype=cupy.bool_)
+
+    shape = (ar1.size, ar2.size)
+    ar1_broadcast = cupy.broadcast_to(ar1[..., cupy.newaxis], shape)
+    ar2_broadcast = cupy.broadcast_to(ar2, shape)
+    count = (ar1_broadcast == ar2_broadcast).sum(axis=1)
+    if invert:
+        return count == 0
+    else:
+        return count > 0
+
+
+def isin(element, test_elements, assume_unique=False, invert=False):
+    """Calculates element in ``test_elements``, broadcasting over ``element``
+    only. Returns a boolean array of the same shape as ``element`` that is
+    ``True`` where an element of ``element`` is in ``test_elements`` and
+    ``False`` otherwise.
+
+    Args:
+        element (cupy.ndarray): Input array.
+        test_elements (cupy.ndarray): The values against which to test each
+            value of ``element``. This argument is flattened if it is an
+            array or array_like.
+        assume_unique (bool, optional): Ignored
+        invert (bool, optional): If ``True``, the values in the returned array
+            are inverted, as if calculating element not in ``test_elements``.
+            Default is ``False``.
+
+    Returns:
+        cupy.ndarray, bool:
+            Has the same shape as ``element``. The values ``element[isin]``
+            are in ``test_elements``.
+    """
+    return in1d(element, test_elements, assume_unique=assume_unique,
+                invert=invert).reshape(element.shape)


Test patch:
diff --git a/tests/cupy_tests/logic_tests/test_truth.py b/tests/cupy_tests/logic_tests/test_truth.py
--- a/tests/cupy_tests/logic_tests/test_truth.py
+++ b/tests/cupy_tests/logic_tests/test_truth.py
@@ -81,3 +81,38 @@ def test_with_out(self, xp, dtype):
         out = xp.empty(out_shape, dtype=x.dtype)
         getattr(xp, self.f)(x, self.axis, out, self.keepdims)
         return out
+
+
+@testing.parameterize(
+    *testing.product(
+        {'f': ['in1d', 'isin'],
+         'shape_x': [
+             (0, ),
+             (3, ),
+             (2, 3),
+             (2, 1, 3),
+             (2, 0, 1),
+             (2, 0, 1, 1)
+        ],
+            'shape_y': [
+             (0, ),
+             (3, ),
+             (2, 3),
+             (2, 1, 3),
+             (2, 0, 1),
+             (2, 0, 1, 1)
+        ],
+            'assume_unique': [False, True],
+            'invert': [False, True]}))
+@testing.gpu
+class TestIn1DIsIn(unittest.TestCase):
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_array_equal()
+    def test(self, xp, dtype):
+        x = testing.shaped_arange(self.shape_x, xp, dtype)
+        y = testing.shaped_arange(self.shape_y, xp, dtype)
+        if xp is numpy and self.f == 'isin':
+            return xp.in1d(x, y, self.assume_unique, self.invert)\
+                .reshape(x.shape)
+        return getattr(xp, self.f)(x, y, self.assume_unique, self.invert)