Skip to content

Add implementation of unique functions from Python array API #2320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/workflows/array-api-skips.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
# array API tests to be skipped

# missing unique-like functions
array_api_tests/test_has_names.py::test_has_names[set-unique_all]
array_api_tests/test_has_names.py::test_has_names[set-unique_counts]
array_api_tests/test_has_names.py::test_has_names[set-unique_inverse]
array_api_tests/test_has_names.py::test_has_names[set-unique_values]
array_api_tests/test_set_functions.py::test_unique_all
array_api_tests/test_set_functions.py::test_unique_counts
array_api_tests/test_set_functions.py::test_unique_inverse
array_api_tests/test_set_functions.py::test_unique_values
array_api_tests/test_signatures.py::test_func_signature[unique_all]
array_api_tests/test_signatures.py::test_func_signature[unique_counts]
array_api_tests/test_signatures.py::test_func_signature[unique_inverse]
array_api_tests/test_signatures.py::test_func_signature[unique_values]

# hypothesis found failures
array_api_tests/test_operators_and_elementwise_functions.py::test_clip

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.. _routines.creation:
.. _routines.array-creation:

Array creation routines
=======================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ Adding and removing elements
dpnp.append
dpnp.resize
dpnp.trim_zeros
dpnp.unique
dpnp.pad


Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion doc/reference/misc.rst → doc/reference/other.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Utility
:toctree: generated/
:nosignatures:

dpnp.broadcast_shapes
dpnp.byte_bounds
dpnp.get_include
dpnp.show_config
dpnp.show_runtime
dpnp.broadcast_shapes
10 changes: 6 additions & 4 deletions doc/reference/routines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ These functions cover a subset of
.. toctree::
:maxdepth: 2

creation
manipulation
binary
array-creation
array-manipulation
bitwise
dtype
fft
functional
indexing
linalg
logic
math
other
.. polynomials
random
sorting
set
sort
statistics
29 changes: 29 additions & 0 deletions doc/reference/set.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Set routines
============

.. https://numpy.org/doc/stable/reference/routines.set.html

Making proper sets
------------------
.. autosummary::
:toctree: generated/
:nosignatures:

dpnp.unique
dpnp.unique_all
dpnp.unique_counts
dpnp.unique_inverse
dpnp.unique_values

Boolean operations
------------------
.. autosummary::
:toctree: generated/
:nosignatures:

dpnp.in1d
dpnp.intersect1d
dpnp.isin
dpnp.setdiff1d
dpnp.setxor1d
dpnp.union1d
File renamed without changes.
205 changes: 205 additions & 0 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ class InsertDeleteParams(NamedTuple):
usm_type: str


# pylint:disable=missing-class-docstring
class UniqueAllResult(NamedTuple):
values: dpnp.ndarray
indices: dpnp.ndarray
inverse_indices: dpnp.ndarray
counts: dpnp.ndarray


class UniqueCountsResult(NamedTuple):
values: dpnp.ndarray
counts: dpnp.ndarray


class UniqueInverseResult(NamedTuple):
values: dpnp.ndarray
inverse_indices: dpnp.ndarray


__all__ = [
"append",
"array_split",
Expand Down Expand Up @@ -122,6 +140,10 @@ class InsertDeleteParams(NamedTuple):
"transpose",
"trim_zeros",
"unique",
"unique_all",
"unique_counts",
"unique_inverse",
"unique_values",
"unstack",
"vsplit",
"vstack",
Expand Down Expand Up @@ -4276,6 +4298,189 @@ def unique(
return _unpack_tuple(result)


def unique_all(x, /):
"""
Find the unique elements of an array, and counts, inverse, and indices.

For full documentation refer to :obj:`numpy.unique_all`.

Parameters
----------
x : {dpnp.ndarray, usm_ndarray}
Input array. It will be flattened if it is not already 1-D.

Returns
-------
A namedtuple with the following attributes:

values : dpnp.ndarray
The unique elements of an input array.
indices : dpnp.ndarray
The first occurring indices for each unique element.
inverse_indices : dpnp.ndarray
The indices from the set of unique elements that reconstruct `x`.
counts : dpnp.ndarray
The corresponding counts for each unique element.

See Also
--------
:obj:`dpnp.unique` : Find the unique elements of an array.

Examples
--------
>>> import dpnp as np
>>> x = np.array([1, 1, 2])
>>> uniq = np.unique_all(x)
>>> uniq.values
array([1, 2])
>>> uniq.indices
array([0, 2])
>>> uniq.inverse_indices
array([0, 0, 1])
>>> uniq.counts
array([2, 1])

"""

result = dpnp.unique(
x,
return_index=True,
return_inverse=True,
return_counts=True,
equal_nan=False,
)
return UniqueAllResult(*result)


def unique_counts(x, /):
"""
Find the unique elements and counts of an input array `x`.

For full documentation refer to :obj:`numpy.unique_counts`.

Parameters
----------
x : {dpnp.ndarray, usm_ndarray}
Input array. It will be flattened if it is not already 1-D.

Returns
-------
A namedtuple with the following attributes:

values : dpnp.ndarray
The unique elements of an input array.
counts : dpnp.ndarray
The corresponding counts for each unique element.

See Also
--------
:obj:`dpnp.unique` : Find the unique elements of an array.

Examples
--------
>>> import dpnp as np
>>> x = np.array([1, 1, 2])
>>> uniq = np.unique_counts(x)
>>> uniq.values
array([1, 2])
>>> uniq.counts
array([2, 1])

"""

result = dpnp.unique(
x,
return_index=False,
return_inverse=False,
return_counts=True,
equal_nan=False,
)
return UniqueCountsResult(*result)


def unique_inverse(x, /):
"""
Find the unique elements of `x` and indices to reconstruct `x`.

For full documentation refer to :obj:`numpy.unique_inverse`.

Parameters
----------
x : {dpnp.ndarray, usm_ndarray}
Input array. It will be flattened if it is not already 1-D.

Returns
-------
A namedtuple with the following attributes:

values : dpnp.ndarray
The unique elements of an input array.
inverse_indices : dpnp.ndarray
The indices from the set of unique elements that reconstruct `x`.

See Also
--------
:obj:`dpnp.unique` : Find the unique elements of an array.

Examples
--------
>>> import dpnp as np
>>> x = np.array([1, 1, 2])
>>> uniq = np.unique_inverse(x)
>>> uniq.values
array([1, 2])
>>> uniq.inverse_indices
array([0, 0, 1])

"""

result = dpnp.unique(
x,
return_index=False,
return_inverse=True,
return_counts=False,
equal_nan=False,
)
return UniqueInverseResult(*result)


def unique_values(x, /):
"""
Returns the unique elements of an input array `x`.

For full documentation refer to :obj:`numpy.unique_values`.

Parameters
----------
x : {dpnp.ndarray, usm_ndarray}
Input array. It will be flattened if it is not already 1-D.

Returns
-------
out : dpnp.ndarray
The unique elements of an input array.

See Also
--------
:obj:`dpnp.unique` : Find the unique elements of an array.

Examples
--------
>>> import dpnp as np
>>> np.unique_values(np.array([1, 1, 2]))
array([1, 2])

"""

return dpnp.unique(
x,
return_index=False,
return_inverse=False,
return_counts=False,
equal_nan=False,
)


def unstack(x, /, *, axis=0):
"""
Split an array into a sequence of arrays along the given axis.
Expand Down
20 changes: 16 additions & 4 deletions dpnp/tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
get_float_dtypes,
get_integer_dtypes,
has_support_aspect64,
numpy_version,
)
from .third_party.cupy import testing

Expand Down Expand Up @@ -1822,10 +1823,7 @@ 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.lib.NumpyVersion(numpy.__version__) < "2.0.1"
):
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 = (
Expand All @@ -1836,6 +1834,20 @@ def test_2d_axis_nans(self, dt, axis_kwd, return_kwds, row):
for iv, v in zip(result, expected):
assert_array_equal(iv, v)

@testing.with_requires("numpy>=2.0")
@pytest.mark.parametrize(
"func",
["unique_all", "unique_counts", "unique_inverse", "unique_values"],
)
def test_array_api_functions(self, func):
a = numpy.array([numpy.nan, 1, 4, 1, 3, 4, 5, 5, 1])
ia = dpnp.array(a)

result = getattr(dpnp, func)(ia)
expected = getattr(numpy, func)(a)
for iv, v in zip(result, expected):
assert_array_equal(iv, v)


class TestVsplit:
@pytest.mark.parametrize("xp", [numpy, dpnp])
Expand Down
Loading
Loading