From a980c7f26d0ef20974664aa1b2d77fe87001f235 Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Thu, 25 Sep 2025 11:29:24 -0700 Subject: [PATCH 1/8] Implement dpnp.isin --- dpnp/dpnp_iface_logic.py | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/dpnp/dpnp_iface_logic.py b/dpnp/dpnp_iface_logic.py index 67ce9b65b54..0b63ad7ba01 100644 --- a/dpnp/dpnp_iface_logic.py +++ b/dpnp/dpnp_iface_logic.py @@ -69,6 +69,7 @@ "iscomplexobj", "isfinite", "isfortran", + "isin", "isinf", "isnan", "isneginf", @@ -1196,6 +1197,95 @@ def isfortran(a): return a.flags.fnc +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. + + Parameters + ---------- + element : {array_like, dpnp.ndarray, usm_ndarray} + Input array. + test_elements : {array_like, dpnp.ndarray, usm_ndarray} + The values against which to test each value of `element`. + This argument is flattened if it is an array or array_like. + See notes for behavior with non-array-like parameters. + 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. + ``dpnp.isin(a, b, invert=True)`` is equivalent to (but faster + than) ``dpnp.invert(dpnp.isin(a, b))``. + + + Returns + ------- + isin : dpnp.ndarray of bool dtype + Has the same shape as `element`. The values `element[isin]` + are in `test_elements`. + + + Examples + -------- + >>> import dpnp as np + >>> element = 2*np.arange(4).reshape((2, 2)) + >>> element + array([[0, 2], + [4, 6]]) + >>> test_elements = [1, 2, 4, 8] + >>> mask = np.isin(element, test_elements) + >>> mask + array([[False, True], + [ True, False]]) + >>> element[mask] + array([2, 4]) + + The indices of the matched values can be obtained with `nonzero`: + + >>> np.nonzero(mask) + (array([0, 1]), array([1, 0])) + + The test can also be inverted: + + >>> mask = np.isin(element, test_elements, invert=True) + >>> mask + array([[ True, False], + [False, True]]) + >>> element[mask] + array([0, 6]) + + """ + + dpnp.check_supported_arrays_type(element, test_elements, scalar_type=True) + if dpnp.isscalar(element): + usm_element = dpt.asarray( + element, + sycl_queue=test_elements.sycl_queue, + usm_type=test_elements.usm_type, + ) + usm_test = dpnp.get_usm_ndarray(test_elements) + elif dpnp.isscalar(test_elements): + usm_test = dpt.asarray( + test_elements, + sycl_queue=element.sycl_queue, + usm_type=element.usm_type, + ) + usm_element = dpnp.get_usm_ndarray(element) + else: + usm_element = dpnp.get_usm_ndarray(element) + usm_test = dpnp.get_usm_ndarray(test_elements) + return dpnp.get_result_array( + dpt.isin( + usm_element, + usm_test, + assume_unique=assume_unique, + invert=invert, + ) + ) + + _ISINF_DOCSTRING = """ Tests each element :math:`x_i` of the input array `x` to determine if equal to positive or negative infinity. From 086f9c713f299fb600297f710bb70865bbfcdcca Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Thu, 25 Sep 2025 13:48:38 -0700 Subject: [PATCH 2/8] unskip cupy isin tests --- dpnp/tests/third_party/cupy/logic_tests/test_truth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dpnp/tests/third_party/cupy/logic_tests/test_truth.py b/dpnp/tests/third_party/cupy/logic_tests/test_truth.py index db9c61426f4..0ba35397210 100644 --- a/dpnp/tests/third_party/cupy/logic_tests/test_truth.py +++ b/dpnp/tests/third_party/cupy/logic_tests/test_truth.py @@ -89,7 +89,6 @@ def test_with_out(self, xp, dtype): return out -@pytest.mark.skip("isin() is not supported yet") @testing.parameterize( *testing.product( { From 9e67af08550eb9ca68b1b77779a758f6736118dd Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Wed, 19 Nov 2025 18:52:58 -0800 Subject: [PATCH 3/8] address review comments --- dpnp/dpnp_iface_logic.py | 45 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/dpnp/dpnp_iface_logic.py b/dpnp/dpnp_iface_logic.py index 0b63ad7ba01..7c6d560d8ae 100644 --- a/dpnp/dpnp_iface_logic.py +++ b/dpnp/dpnp_iface_logic.py @@ -1200,24 +1200,27 @@ def isfortran(a): 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. + 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. + + For full documentation refer to :obj:`numpy.isin`. Parameters ---------- - element : {array_like, dpnp.ndarray, usm_ndarray} + element : {dpnp.ndarray, usm_ndarray, scalar} Input array. - test_elements : {array_like, dpnp.ndarray, usm_ndarray} + test_elements : {dpnp.ndarray, usm_ndarray, scalar} The values against which to test each value of `element`. - This argument is flattened if it is an array or array_like. - See notes for behavior with non-array-like parameters. + This argument is flattened if it is an array. 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. + If ``True``, the values in the returned array are inverted, as if + calculating `element not in test_elements`. ``dpnp.isin(a, b, invert=True)`` is equivalent to (but faster than) ``dpnp.invert(dpnp.isin(a, b))``. + Default: ``False``. Returns @@ -1259,28 +1262,18 @@ def isin(element, test_elements, assume_unique=False, invert=False): """ dpnp.check_supported_arrays_type(element, test_elements, scalar_type=True) - if dpnp.isscalar(element): - usm_element = dpt.asarray( - element, - sycl_queue=test_elements.sycl_queue, - usm_type=test_elements.usm_type, - ) - usm_test = dpnp.get_usm_ndarray(test_elements) - elif dpnp.isscalar(test_elements): - usm_test = dpt.asarray( - test_elements, - sycl_queue=element.sycl_queue, - usm_type=element.usm_type, - ) - usm_element = dpnp.get_usm_ndarray(element) - else: - usm_element = dpnp.get_usm_ndarray(element) - usm_test = dpnp.get_usm_ndarray(test_elements) + usm_element = dpnp.as_usm_ndarray( + element, usm_type=element.usm_type, sycl_queue=element.sycl_queue + ) + usm_test = dpnp.as_usm_ndarray( + test_elements, + usm_type=test_elements.usm_type, + sycl_queue=test_elements.sycl_queue, + ) return dpnp.get_result_array( dpt.isin( usm_element, usm_test, - assume_unique=assume_unique, invert=invert, ) ) From 92f5ae54aa8346f09e0efb2043df9cd3214e6b82 Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Wed, 19 Nov 2025 18:54:10 -0800 Subject: [PATCH 4/8] add to `assume_unique` documentation --- dpnp/dpnp_iface_logic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_logic.py b/dpnp/dpnp_iface_logic.py index 7c6d560d8ae..8f135b78693 100644 --- a/dpnp/dpnp_iface_logic.py +++ b/dpnp/dpnp_iface_logic.py @@ -1214,7 +1214,9 @@ def isin(element, test_elements, assume_unique=False, invert=False): The values against which to test each value of `element`. This argument is flattened if it is an array. assume_unique : bool, optional - Ignored + Ignored, as no performance benefit is gained by assuming the + input arrays are unique. Included for compatibility with NumPy. + Default: ``False``. invert : bool, optional If ``True``, the values in the returned array are inverted, as if calculating `element not in test_elements`. From c5aff447c6d8990c014495142f19f0122d977c4c Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Wed, 19 Nov 2025 19:03:58 -0800 Subject: [PATCH 5/8] add isin tests --- dpnp/tests/test_logic.py | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/dpnp/tests/test_logic.py b/dpnp/tests/test_logic.py index efcdf2b0be6..ab5c849ebfb 100644 --- a/dpnp/tests/test_logic.py +++ b/dpnp/tests/test_logic.py @@ -795,3 +795,100 @@ def test_array_equal_nan(a): result = dpnp.array_equal(dpnp.array(a), dpnp.array(b), equal_nan=True) expected = numpy.array_equal(a, b, equal_nan=True) assert_equal(result, expected) + + +@pytest.mark.parametrize( + "a", + [ + numpy.array([1, 2, 3, 4]), + numpy.array([[1, 2], [3, 4]]), + ], +) +@pytest.mark.parametrize( + "b", + [ + numpy.array([2, 4, 6]), + numpy.array([[1, 3], [5, 7]]), + ], +) +def test_isin_basic(a, b): + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +@pytest.mark.parametrize("dtype", get_all_dtypes()) +def test_isin_dtype(dtype): + a = numpy.array([1, 2, 3, 4], dtype=dtype) + b = numpy.array([2, 4], dtype=dtype) + + dp_a = dpnp.array(a, dtype=dtype) + dp_b = dpnp.array(b, dtype=dtype) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +@pytest.mark.parametrize("sh_a, sh_b", [((3, 1), (1, 4)), ((2, 3, 1), (1, 1))]) +def test_isin_broadcast(sh_a, sh_b): + a = numpy.arange(numpy.prod(sh_a)).reshape(sh_a) + b = numpy.arange(numpy.prod(sh_b)).reshape(sh_b) + + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +def test_isin_scalar_elements(): + a = numpy.array([1, 2, 3]) + b = 2 + + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +def test_isin_scalar_test_elements(): + a = 2 + b = numpy.array([1, 2, 3]) + + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +def test_isin_empty(): + a = numpy.array([], dtype=int) + b = numpy.array([1, 2, 3]) + + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + + +def test_isin_errors(): + a = dpnp.arange(5) + b = dpnp.arange(3) + + # unsupported type for elements or test_elements + with pytest.raises(TypeError): + dpnp.isin(dict(), b) + + with pytest.raises(TypeError): + dpnp.isin(a, dict()) From 42e989342f97bca47edd55bb9d391dc1afcefd0e Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Wed, 19 Nov 2025 19:16:44 -0800 Subject: [PATCH 6/8] disable pylint for isin unused `assume_unique` argument --- dpnp/dpnp_iface_logic.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_logic.py b/dpnp/dpnp_iface_logic.py index 8f135b78693..1d979935299 100644 --- a/dpnp/dpnp_iface_logic.py +++ b/dpnp/dpnp_iface_logic.py @@ -1197,7 +1197,12 @@ def isfortran(a): return a.flags.fnc -def isin(element, test_elements, assume_unique=False, invert=False): +def isin( + element, + test_elements, + assume_unique=False, # pylint: disable=unused-argument + invert=False, +): """ Calculates ``element in test_elements``, broadcasting over `element` only. Returns a boolean array of the same shape as `element` that is ``True`` From a043ae90a13a0a3a5eb78c8c3f7cf592732c87bc Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Thu, 20 Nov 2025 23:16:24 -0800 Subject: [PATCH 7/8] address review comments --- dpnp/dpnp_iface_logic.py | 45 +++++++++--- dpnp/tests/test_logic.py | 149 +++++++++++++++++++-------------------- 2 files changed, 108 insertions(+), 86 deletions(-) diff --git a/dpnp/dpnp_iface_logic.py b/dpnp/dpnp_iface_logic.py index 1d979935299..a48043208a6 100644 --- a/dpnp/dpnp_iface_logic.py +++ b/dpnp/dpnp_iface_logic.py @@ -1202,6 +1202,8 @@ def isin( test_elements, assume_unique=False, # pylint: disable=unused-argument invert=False, + *, + kind=None, # pylint: disable=unused-argument ): """ Calculates ``element in test_elements``, broadcasting over `element` only. @@ -1221,14 +1223,20 @@ def isin( assume_unique : bool, optional Ignored, as no performance benefit is gained by assuming the input arrays are unique. Included for compatibility with NumPy. + Default: ``False``. invert : bool, optional If ``True``, the values in the returned array are inverted, as if - calculating `element not in test_elements`. + calculating ``element not in test_elements``. ``dpnp.isin(a, b, invert=True)`` is equivalent to (but faster than) ``dpnp.invert(dpnp.isin(a, b))``. + Default: ``False``. + kind : {None, "sort"}, optional + Ignored, as the only algorithm implemented is ``"sort"``. Included for + compatibility with NumPy. + Default: ``None``. Returns ------- @@ -1236,7 +1244,6 @@ def isin( Has the same shape as `element`. The values `element[isin]` are in `test_elements`. - Examples -------- >>> import dpnp as np @@ -1269,14 +1276,32 @@ def isin( """ dpnp.check_supported_arrays_type(element, test_elements, scalar_type=True) - usm_element = dpnp.as_usm_ndarray( - element, usm_type=element.usm_type, sycl_queue=element.sycl_queue - ) - usm_test = dpnp.as_usm_ndarray( - test_elements, - usm_type=test_elements.usm_type, - sycl_queue=test_elements.sycl_queue, - ) + if dpnp.isscalar(element): + usm_element = dpnp.as_usm_ndarray( + element, + usm_type=test_elements.usm_type, + sycl_queue=test_elements.sycl_queue, + ) + usm_test = dpnp.get_usm_ndarray(test_elements) + elif dpnp.isscalar(test_elements): + usm_test = dpnp.as_usm_ndarray( + test_elements, + usm_type=element.usm_type, + sycl_queue=element.sycl_queue, + ) + usm_element = dpnp.get_usm_ndarray(element) + else: + if ( + dpu.get_execution_queue( + (element.sycl_queue, test_elements.sycl_queue) + ) + is None + ): + raise dpu.ExecutionPlacementError( + "Input arrays have incompatible allocation queues" + ) + usm_element = dpnp.get_usm_ndarray(element) + usm_test = dpnp.get_usm_ndarray(test_elements) return dpnp.get_result_array( dpt.isin( usm_element, diff --git a/dpnp/tests/test_logic.py b/dpnp/tests/test_logic.py index ab5c849ebfb..0fcb63f89c2 100644 --- a/dpnp/tests/test_logic.py +++ b/dpnp/tests/test_logic.py @@ -797,98 +797,95 @@ def test_array_equal_nan(a): assert_equal(result, expected) -@pytest.mark.parametrize( - "a", - [ - numpy.array([1, 2, 3, 4]), - numpy.array([[1, 2], [3, 4]]), - ], -) -@pytest.mark.parametrize( - "b", - [ - numpy.array([2, 4, 6]), - numpy.array([[1, 3], [5, 7]]), - ], -) -def test_isin_basic(a, b): - dp_a = dpnp.array(a) - dp_b = dpnp.array(b) - - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) - - -@pytest.mark.parametrize("dtype", get_all_dtypes()) -def test_isin_dtype(dtype): - a = numpy.array([1, 2, 3, 4], dtype=dtype) - b = numpy.array([2, 4], dtype=dtype) - - dp_a = dpnp.array(a, dtype=dtype) - dp_b = dpnp.array(b, dtype=dtype) - - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) - +class TestIsin: + @pytest.mark.parametrize( + "a", + [ + numpy.array([1, 2, 3, 4]), + numpy.array([[1, 2], [3, 4]]), + ], + ) + @pytest.mark.parametrize( + "b", + [ + numpy.array([2, 4, 6]), + numpy.array([[1, 3], [5, 7]]), + ], + ) + def test_isin_basic(self, a, b): + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) -@pytest.mark.parametrize("sh_a, sh_b", [((3, 1), (1, 4)), ((2, 3, 1), (1, 1))]) -def test_isin_broadcast(sh_a, sh_b): - a = numpy.arange(numpy.prod(sh_a)).reshape(sh_a) - b = numpy.arange(numpy.prod(sh_b)).reshape(sh_b) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) - dp_a = dpnp.array(a) - dp_b = dpnp.array(b) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + def test_isin_dtype(self, dtype): + a = numpy.array([1, 2, 3, 4], dtype=dtype) + b = numpy.array([2, 4], dtype=dtype) - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) + dp_a = dpnp.array(a, dtype=dtype) + dp_b = dpnp.array(b, dtype=dtype) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) -def test_isin_scalar_elements(): - a = numpy.array([1, 2, 3]) - b = 2 + @pytest.mark.parametrize( + "sh_a, sh_b", [((3, 1), (1, 4)), ((2, 3, 1), (1, 1))] + ) + def test_isin_broadcast(self, sh_a, sh_b): + a = numpy.arange(numpy.prod(sh_a)).reshape(sh_a) + b = numpy.arange(numpy.prod(sh_b)).reshape(sh_b) - dp_a = dpnp.array(a) - dp_b = dpnp.array(b) + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) + def test_isin_scalar_elements(self): + a = numpy.array([1, 2, 3]) + b = 2 -def test_isin_scalar_test_elements(): - a = 2 - b = numpy.array([1, 2, 3]) + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) - dp_a = dpnp.array(a) - dp_b = dpnp.array(b) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) + def test_isin_scalar_test_elements(self): + a = 2 + b = numpy.array([1, 2, 3]) + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) -def test_isin_empty(): - a = numpy.array([], dtype=int) - b = numpy.array([1, 2, 3]) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) - dp_a = dpnp.array(a) - dp_b = dpnp.array(b) + def test_isin_empty(self): + a = numpy.array([], dtype=int) + b = numpy.array([1, 2, 3]) - expected = numpy.isin(a, b) - result = dpnp.isin(dp_a, dp_b) - assert_equal(result, expected) + dp_a = dpnp.array(a) + dp_b = dpnp.array(b) + expected = numpy.isin(a, b) + result = dpnp.isin(dp_a, dp_b) + assert_equal(result, expected) -def test_isin_errors(): - a = dpnp.arange(5) - b = dpnp.arange(3) + def test_isin_errors(self): + a = dpnp.arange(5) + b = dpnp.arange(3) - # unsupported type for elements or test_elements - with pytest.raises(TypeError): - dpnp.isin(dict(), b) + # unsupported type for elements or test_elements + with pytest.raises(TypeError): + dpnp.isin(dict(), b) - with pytest.raises(TypeError): - dpnp.isin(a, dict()) + with pytest.raises(TypeError): + dpnp.isin(a, dict()) From 02dd3916868059a370334729f2d4601495614999 Mon Sep 17 00:00:00 2001 From: Nikita Grigorian Date: Thu, 20 Nov 2025 23:19:11 -0800 Subject: [PATCH 8/8] add compute follows data tests for `isin` --- dpnp/tests/test_sycl_queue.py | 1 + dpnp/tests/test_usm_type.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dpnp/tests/test_sycl_queue.py b/dpnp/tests/test_sycl_queue.py index 3ea4105a2cb..444640ecab0 100644 --- a/dpnp/tests/test_sycl_queue.py +++ b/dpnp/tests/test_sycl_queue.py @@ -536,6 +536,7 @@ def test_logic_op_1in(op, device): "greater", "greater_equal", "isclose", + "isin", "less", "less_equal", "logical_and", diff --git a/dpnp/tests/test_usm_type.py b/dpnp/tests/test_usm_type.py index c8598263c66..b595f7eb04b 100644 --- a/dpnp/tests/test_usm_type.py +++ b/dpnp/tests/test_usm_type.py @@ -355,6 +355,7 @@ def test_logic_op_1in(op, usm_type_x): "greater", "greater_equal", "isclose", + "isin", "less", "less_equal", "logical_and",