From ff4c32a489b4317c7b986c154a33ede85584d137 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:03:53 +0200 Subject: [PATCH 1/7] Fix dpnp.unique with axis=0 and 1d input --- dpnp/dpnp_iface_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 19bcfd1e67b3..3f991ebd6751 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -4245,7 +4245,7 @@ def unique( """ - if axis is None: + if axis is None or (axis == 0 and ar.ndim == 1): return _unique_1d( ar, return_index, return_inverse, return_counts, equal_nan ) From 607be705d447de0b73f0528793e2936ef84cb80e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:04:46 +0200 Subject: [PATCH 2/7] Add a test to cover that --- dpnp/tests/test_manipulation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 30ac6f6c5d2e..548adc3dbc54 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -1838,6 +1838,15 @@ def test_equal_nan(self, eq_nan_kwd): expected = numpy.unique(a, **eq_nan_kwd) assert_array_equal(result, expected) + @testing.with_requires("numpy>=2.3.2") + def test_1d_equal_nan_axis0(self): + a = numpy.array([numpy.nan, 0, 0, numpy.nan]) + ia = dpnp.array(a) + + result = dpnp.unique(ia, axis=0, equal_nan=True) + expected = numpy.unique(a, axis=0, equal_nan=True) + assert_array_equal(result, expected) + @pytest.mark.parametrize("dt", get_float_complex_dtypes()) @pytest.mark.parametrize( "axis_kwd", From 2ec6330ad6a7d36d72239a094b75927b10fd0e05 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:10:15 +0200 Subject: [PATCH 3/7] Simplify unique tests requiring numpy>=2.0.1 where applicable --- dpnp/tests/test_manipulation.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 548adc3dbc54..08166cf42a3f 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -1685,6 +1685,7 @@ def test_axis_list(self, axis): expected = numpy.unique(a, axis=axis) assert_array_equal(result, expected) + @testing.with_requires("numpy>=2.0.1") @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) @pytest.mark.parametrize( "axis_kwd", @@ -1716,17 +1717,6 @@ def test_2d_axis(self, dt, axis_kwd, return_kwds): if len(return_kwds) == 0: assert_array_equal(result, expected) else: - if ( - len(axis_kwd) == 0 - and numpy.lib.NumpyVersion(numpy.__version__) < "2.0.1" - ): - # gh-26961: numpy.unique(..., return_inverse=True, axis=None) - # returned flatten unique_inverse till 2.0.1 version - expected = ( - expected[:2] - + (expected[2].reshape(a.shape),) - + expected[3:] - ) for iv, v in zip(result, expected): assert_array_equal(iv, v) @@ -1756,6 +1746,7 @@ def test_1d_axis(self, axis): expected = numpy.unique(a, axis=axis) assert_array_equal(result, expected) + @testing.with_requires("numpy>=2.0.1") @pytest.mark.parametrize("axis", [None, 0, -1]) def test_2d_axis_inverse(self, axis): a = numpy.array([[4, 4, 3], [2, 2, 1], [2, 2, 1], [4, 4, 3]]) @@ -1763,10 +1754,6 @@ def test_2d_axis_inverse(self, axis): result = dpnp.unique(ia, return_inverse=True, axis=axis) expected = numpy.unique(a, return_inverse=True, axis=axis) - if axis is None and numpy.lib.NumpyVersion(numpy.__version__) < "2.0.1": - # gh-26961: numpy.unique(..., return_inverse=True, axis=None) - # returned flatten unique_inverse till 2.0.1 version - expected = expected[:1] + (expected[1].reshape(a.shape),) for iv, v in zip(result, expected): assert_array_equal(iv, v) @@ -1847,6 +1834,7 @@ def test_1d_equal_nan_axis0(self): expected = numpy.unique(a, axis=0, equal_nan=True) assert_array_equal(result, expected) + @testing.with_requires("numpy>=2.0.1") @pytest.mark.parametrize("dt", get_float_complex_dtypes()) @pytest.mark.parametrize( "axis_kwd", @@ -1888,14 +1876,6 @@ def test_2d_axis_nans(self, dt, axis_kwd, return_kwds, row): if len(return_kwds) == 0: assert_array_equal(result, expected) else: - if len(axis_kwd) == 0 and numpy_version() < "2.0.1": - # gh-26961: numpy.unique(..., return_inverse=True, axis=None) - # returned flatten unique_inverse till 2.0.1 version - expected = ( - expected[:2] - + (expected[2].reshape(a.shape),) - + expected[3:] - ) for iv, v in zip(result, expected): assert_array_equal(iv, v) From be34b6d269607aa90cc3cce836ae2619ef1c76aa Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:17:36 +0200 Subject: [PATCH 4/7] Update the expected result till the new numpy release with the fix is available --- dpnp/tests/test_manipulation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 08166cf42a3f..d6115bfbe133 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -1825,13 +1825,17 @@ def test_equal_nan(self, eq_nan_kwd): expected = numpy.unique(a, **eq_nan_kwd) assert_array_equal(result, expected) - @testing.with_requires("numpy>=2.3.2") + # TODO: uncomment once numpy 2.3.2 release is published + # @testing.with_requires("numpy>=2.3.2") def test_1d_equal_nan_axis0(self): a = numpy.array([numpy.nan, 0, 0, numpy.nan]) ia = dpnp.array(a) result = dpnp.unique(ia, axis=0, equal_nan=True) expected = numpy.unique(a, axis=0, equal_nan=True) + # TODO: remove + if numpy_version() < "2.3.2": + expected = numpy.array([0.0, numpy.nan]) assert_array_equal(result, expected) @testing.with_requires("numpy>=2.0.1") From 7ffe62c03933c16fc3463081f0674c2977fbf08f Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:27:36 +0200 Subject: [PATCH 5/7] Add PR to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4326a256ce..97c45c670ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated `pre-commit` GitHub workflow to pass `no-commit-to-branch` check [#2501](https://github.com/IntelPython/dpnp/pull/2501) * Updated the math formulas in summary of `dpnp.matvec` and `dpnp.vecmat` to correct a typo [#2503](https://github.com/IntelPython/dpnp/pull/2503) * Avoided negating unsigned integers in ceil division used in `dpnp.resize` implementation [#2508](https://github.com/IntelPython/dpnp/pull/2508) +* Fixed `dpnp.unique` with 1d input array and `axis=0`, `equal_nan=True` keywords passed where the produced result doesn't collapse the NaNs [#2530](https://github.com/IntelPython/dpnp/pull/2530) ### Security From 234c8af768038d523b887bc0018087f49aac186a Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 12:28:47 +0200 Subject: [PATCH 6/7] Update the TODO test comment --- dpnp/tests/test_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index d6115bfbe133..f53c6d9b5dd6 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -1833,7 +1833,7 @@ def test_1d_equal_nan_axis0(self): result = dpnp.unique(ia, axis=0, equal_nan=True) expected = numpy.unique(a, axis=0, equal_nan=True) - # TODO: remove + # TODO: remove when numpy#29372 is released if numpy_version() < "2.3.2": expected = numpy.array([0.0, numpy.nan]) assert_array_equal(result, expected) From 1b7d987893315e2dd35dfaf44a4f99a413334f67 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 17 Jul 2025 13:38:56 +0200 Subject: [PATCH 7/7] Add test_2d_axis_unsigned_inetger to keep the same coverage lever --- dpnp/tests/helper.py | 10 +++++++++- dpnp/tests/test_manipulation.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/dpnp/tests/helper.py b/dpnp/tests/helper.py index 075b14fbc011..80043464f791 100644 --- a/dpnp/tests/helper.py +++ b/dpnp/tests/helper.py @@ -343,7 +343,7 @@ def get_integer_dtypes(all_int_types=False, no_unsigned=False): if config.all_int_types or all_int_types: dtypes += [dpnp.int8, dpnp.int16] if not no_unsigned: - dtypes += [dpnp.uint8, dpnp.uint16, dpnp.uint32, dpnp.uint64] + dtypes += get_unsigned_dtypes() return dtypes @@ -378,6 +378,14 @@ def not_excluded(dtype): return dtypes +def get_unsigned_dtypes(): + """ + Build a list of unsigned integer types supported by DPNP. + """ + + return [dpnp.uint8, dpnp.uint16, dpnp.uint32, dpnp.uint64] + + def has_support_aspect16(device=None): """ Return True if the device supports 16-bit precision floating point operations, diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index f53c6d9b5dd6..5fc3d7df8c5f 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -21,6 +21,7 @@ get_float_dtypes, get_integer_dtypes, get_integer_float_dtypes, + get_unsigned_dtypes, has_support_aspect64, numpy_version, ) @@ -1799,8 +1800,18 @@ def test_2d_axis_signed_inetger(self, dt): expected = numpy.unique(a, axis=0) assert_array_equal(result, expected) + @pytest.mark.parametrize("axis", [None, 0, 1]) + @pytest.mark.parametrize("dt", get_unsigned_dtypes()) + def test_2d_axis_unsigned_inetger(self, axis, dt): + a = numpy.array([[7, 1, 2, 1], [5, 7, 5, 7]], dtype=dt) + ia = dpnp.array(a) + + result = dpnp.unique(ia, axis=axis) + expected = numpy.unique(a, axis=axis) + assert_array_equal(result, expected) + @pytest.mark.parametrize("axis", [None, 0]) - @pytest.mark.parametrize("dt", "bBhHiIlLqQ") + @pytest.mark.parametrize("dt", get_integer_dtypes(all_int_types=True)) def test_1d_axis_all_inetger(self, axis, dt): a = numpy.array([5, 7, 1, 2, 1, 5, 7], dtype=dt) ia = dpnp.array(a)