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 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 ) 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 30ac6f6c5d2e..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, ) @@ -1685,6 +1686,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 +1718,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 +1747,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 +1755,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) @@ -1812,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) @@ -1838,6 +1836,20 @@ def test_equal_nan(self, eq_nan_kwd): expected = numpy.unique(a, **eq_nan_kwd) assert_array_equal(result, expected) + # 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 when numpy#29372 is released + 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") @pytest.mark.parametrize("dt", get_float_complex_dtypes()) @pytest.mark.parametrize( "axis_kwd", @@ -1879,14 +1891,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)