8000 ENH: Upgrade Array API version to 2024.12 by mtsokol · Pull Request #28615 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH: Upgrade Array API version to 2024.12 #28615

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 5 commits into from
May 10, 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
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: data-apis/array-api-tests
ref: '827edd804bcace9d64176b8115138d29ae3e8dec' # Latest commit as of 2024-07-30
ref: 'c48410f96fc58e02eea844e6b7f6cc01680f77ce' # Latest commit as of 2025-04-01
submodules: 'true'
path: 'array-api-tests'
persist-credentials: false
Expand Down
5 changes: 5 additions & 0 deletions doc/release/upcoming_changes/28615.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* NumPy's ``__array_api_version__`` was upgraded from ``2023.12`` to ``2024.12``.
* `numpy.count_nonzero` for ``axis=None`` (default) now returns a NumPy scalar
instead of a Python integer.
* The parameter ``axis`` in `numpy.take_along_axis` function has now a default
value of ``-1``.
2 changes: 1 addition & 1 deletion numpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@
# import with `from numpy import *`.
__future_scalars__ = {"str", "bytes", "object"}

__array_api_version__ = "2023.12"
__array_api_version__ = "2024.12"

from ._array_api_info import __array_namespace_info__

Expand Down
4 changes: 2 additions & 2 deletions numpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ _DTypeNum: TypeAlias = L[
]
_DTypeBuiltinKind: TypeAlias = L[0, 1, 2]

_ArrayAPIVersion: TypeAlias = L["2021.12", "2022.12", "2023.12"]
_ArrayAPIVersion: TypeAlias = L["2021.12", "2022.12", "2023.12", "2024.12"]

_CastingKind: TypeAlias = L["no", "equiv", "safe", "same_kind", "unsafe"]

Expand Down Expand Up @@ -1153,7 +1153,7 @@ __NUMPY_SETUP__: Final[L[False]] = False
__numpy_submodules__: Final[set[LiteralString]] = ...
__former_attrs__: Final[_FormerAttrsDict] = ...
__future_scalars__: Final[set[L["bytes", "str", "object"]]] = ...
__array_api_version__: Final[L["2023.12"]] = "2023.12"
__array_api_version__: Final[L["2024.12"]] = "2024.12"
test: Final[PytestTester] = ...

@type_check_only
Expand Down
2 changes: 1 addition & 1 deletion numpy/_core/numeric.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def full_like(

#
@overload
def count_nonzero(a: ArrayLike, axis: None = None, *, keepdims: L[False] = False) -> int: ...
def count_nonzero(a: ArrayLike, axis: None = None, *, keepdims: L[False] = False) -> np.intp: ...
@overload
def count_nonzero(a: _ScalarLike_co, axis: _ShapeLike | None = None, *, keepdims: L[True]) -> np.intp: ...
@overload
Expand Down
3 changes: 2 additions & 1 deletion numpy/_core/src/multiarray/array_api_standard.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ array_array_namespace(PyObject *NPY_UNUSED(self), PyObject *args, PyObject *kwds
return NULL;
} else if (PyUnicode_CompareWithASCIIString(array_api_version, "2021.12") != 0 &&
PyUnicode_CompareWithASCIIString(array_api_version, "2022.12") != 0 &&
PyUnicode_CompareWithASCIIString(array_api_version, "2023.12") != 0)
PyUnicode_CompareWithASCIIString(array_api_version, "2023.12") != 0 &&
PyUnicode_CompareWithASCIIString(array_api_version, "2024.12") != 0)
{
PyErr_Format(PyExc_ValueError,
"Version \"%U\" of the Array API Standard is not supported.",
Expand Down
10 changes: 7 additions & 3 deletions numpy/_core/src/multiarray/multiarraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2280,14 +2280,18 @@ array_count_nonzero(PyObject *NPY_UNUSED(self), PyObject *const *args, Py_ssize_
return NULL;
}

count = PyArray_CountNonzero(array);

count = PyArray_CountNonzero(array);
Py_DECREF(array);

if (count == -1) {
return NULL;
}
return PyLong_FromSsize_t(count);

PyArray_Descr *descr = PyArray_DescrFromType(NPY_INTP);
if (descr == NULL) {
return NULL;
}
return PyArray_Scalar(&count, descr, NULL);
Copy link
Member
@seberg seberg Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a more significant change than it may look, since it has serious impact on promotion for count_nonzero without an axis.
(I.e. code like arr.sum() / count_nonzero(arr) can behave differently.)

Maybe we can do it, but we should discuss it briefly/add a release note for visibility.
But I am tempted to fix it in the array api tests to say that it is completely fine to return an integer for count_nonzero(arr, axis=None).

(I think an integer return is just better for NumPy users, the argument against it is only that we can also return arrays of course, for which there is no equivalent behavior obviously.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I can just skip this test or we can keep this change, I'm Ok with both. I added it to today's triage meeting for broader discussion. I can't attend myself today so just ping me if anything was decided.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I just checked and NumPy doesn't really run into this (I suppose we don't really have code paths that never pass an axis, so have to provision anyway).
Still think we should at least mention it in a release note as a subtle change, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I added a release note.

Right now I'm also in favor of this change - returning a NumPy scalar when axis=None makes it coherent when axis is passed and the result is 0-d. Right now we have:

In [1]: np.count_nonzero(np.array([1,0,3,1]))
Out[1]: 3

In [2]: np.count_nonzero(np.array([1,0,3,1]), axis=0)
Out[2]: np.int64(3)

In [3]: np.count_nonzero(np.array([[1,0,3,1]]), axis=(0,1))
Out[3]: np.int64(3)

After this change it's also np.int64(3) for axis=None.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand that consistency is better with the change. But in contexts where axis is always None, the integer return is more useful and changing it can change results, because:

arr = np.linspace(0, 100, 10000, dtype=np.float32)
res = arr / np.count_nonzero(arr)

will change from being a float32 result to a float64 one.

Not that I suspect this to be seen often. skimage has a function that will return a float64 rather than a Python float with this change for example, I am sure that usually doesn't matter.

}

static PyObject *
Expand Down
8 changes: 5 additions & 3 deletions numpy/_core/tests/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -2570,21 +2570,23 @@ def test__array_namespace__(self):
assert xp is np
xp = arr.__array_namespace__(api_version="2023.12")
assert xp is np
xp = arr.__array_namespace__(api_version="2024.12")
assert xp is np
xp = arr.__array_namespace__(api_version=None)
assert xp is np

with pytest.raises(
ValueError,
match="Version \"2024.12\" of the Array API Standard "
match="Version \"2025.12\" of the Array API Standard "
"is not supported."
):
arr.__array_namespace__(api_version="2024.12")
arr.__array_namespace__(api_version="2025.12")

with pytest.raises(
ValueError,
match="Only None and strings are allowed as the Array API version"
):
arr.__array_namespace__(api_version=2023)
arr.__array_namespace__(api_version=2024)

def test_isin_refcnt_bug(self):
# gh-25295
Expand Down
15 changes: 9 additions & 6 deletions numpy/lib/_shape_base_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ def _make_along_axis_idx(arr_shape, indices, axis):
return tuple(fancy_index)


def _take_along_axis_dispatcher(arr, indices, axis):
def _take_along_axis_dispatcher(arr, indices, axis=None):
return (arr, indices)


@array_function_dispatch(_take_along_axis_dispatcher)
def take_along_axis(arr, indices, axis):
def take_along_axis(arr, indices, axis=-1):
"""
Take values from the input array by matching 1d index and data slices.

Expand All @@ -71,14 +71,17 @@ def take_along_axis(arr, indices, axis):
arr : ndarray (Ni..., M, Nk...)
Source array
indices : ndarray (Ni..., J, Nk...)
Indices to take along each 1d slice of `arr`. This must match the
dimension of arr, but dimensions Ni and Nj only need to broadcast
against `arr`.
axis : int
Indices to take along each 1d slice of ``arr``. This must match the
dimension of ``arr``, but dimensions Ni and Nj only need to broadcast
against ``arr``.
axis : int or None, optional
The axis to take 1d slices along. If axis is None, the input array is
treated as if it had first been flattened to 1d, for consistency with
`sort` and `argsort`.

.. versionchanged:: 2.3
The default value is now ``-1``.

Returns
-------
out: ndarray (Ni..., J, Nk...)
Expand Down
2 changes: 1 addition & 1 deletion numpy/lib/_shape_base_impl.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class _SupportsArrayWrap(Protocol):
def take_along_axis(
arr: _ScalarT | NDArray[_ScalarT],
indices: NDArray[integer],
axis: int | None,
axis: int | None = ...,
) -> NDArray[_ScalarT]: ...

def put_along_axis(
Expand Down
6 changes: 3 additions & 3 deletions numpy/typing/tests/data/reveal/numeric.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ AR_O: npt.NDArray[np.object_]
B: list[int]
C: SubClass

assert_type(np.count_nonzero(i8), int)
assert_type(np.count_nonzero(AR_i8), int)
assert_type(np.count_nonzero(B), int)
assert_type(np.count_nonzero(i8), np.intp)
assert_type(np.count_nonzero(AR_i8), np.intp)
assert_type(np.count_nonzero(B), np.intp)
assert_type(np.count_nonzero(AR_i8, keepdims=True), npt.NDArray[np.intp])
assert_type(np.count_nonzero(AR_i8, axis=0), Any)

Expand Down
27 changes: 27 additions & 0 deletions tools/ci/array-api-xfails.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,30 @@ array_api_tests/test_signatures.py::test_func_signature[vecdot]

# input is cast to min/max's dtype if they're different
array_api_tests/test_operators_and_elementwise_functions.py::test_clip

# missing 'dtype' keyword argument
array_api_tests/test_signatures.py::test_extension_func_signature[fft.fftfreq]
array_api_tests/test_signatures.py::test_extension_func_signature[fft.rfftfreq]

# fails on np.repeat(np.array([]), np.array([])) test case
array_api_tests/test_manipulation_functions.py::test_repeat

# NumPy matches Python behavior and it returns NaN and -1 in these cases
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
array_api_tests/test_special_cases.py::test_binary[floor_divide(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
array_api_tests/test_special_cases.py::test_binary[__floordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is +infinity and isfinite(x2_i) and x2_i < 0) -> -infinity]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i > 0) -> -infinity]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(x1_i is -infinity and isfinite(x2_i) and x2_i < 0) -> +infinity]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i > 0 and x2_i is -infinity) -> -0]
array_api_tests/test_special_cases.py::test_iop[__ifloordiv__(isfinite(x1_i) and x1_i < 0 and x2_i is +infinity) -> -0]
Loading
0