Skip to content
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
8 changes: 8 additions & 0 deletions doc/release/upcoming_changes/25437.new_feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
`numpy.linalg.martrix_rank`, `numpy.sort` and `numpy.argsort` new parameters
----------------------------------------------------------------------------

New keyword parameters were added to improve array API compatibility:

* ``rtol`` keyword parameter was added to `numpy.linalg.martrix_rank`.
Copy link
Member

Choose a reason for hiding this comment

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

The fact that this changes the default, does need it is own release note, as it is a breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

Note that this was not addressed, still need a release note nothing the BC break.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it changes the default.

Before default tol was defined as:

if tol is None:
    tol = (
        S.max(axis=-1, keepdims=True) *
        max(A.shape[-2:]) *
        finfo(S.dtype).eps
    )

And right now it's:

if rtol is None:
    rtol = max(A.shape[-2:]) * finfo(S.dtype).eps
if tol is None:
    tol = S.max(axis=-1, keepdims=True) * rtol

As rtol=None and tol=None by default, then the default didn't change. Or am I missing something?

Copy link
Member

Choose a reason for hiding this comment

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

Right, sorry... it does change for pinv, but not matrix rank.


* ``stable`` keyword parameter was added to `numpy.sort` and `numpy.argsort`.
4 changes: 4 additions & 0 deletions numpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,8 @@ class _ArrayOrScalarCommon:
axis: None | SupportsIndex = ...,
kind: None | _SortKind = ...,
order: None | str | Sequence[str] = ...,
*,
stable: None | bool = ...,
) -> NDArray[Any]: ...

@overload
Expand Down Expand Up @@ -1640,6 +1642,8 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeType, _DType_co]):
axis: SupportsIndex = ...,
kind: None | _SortKind = ...,
order: None | str | Sequence[str] = ...,
*,
stable: None | bool = ...,
) -> None: ...

@overload
Expand Down
29 changes: 22 additions & 7 deletions numpy/_core/fromnumeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,12 +894,12 @@ def argpartition(a, kth, axis=-1, kind='introselect', order=None):
return _wrapfunc(a, 'argpartition', kth, axis=axis, kind=kind, order=order)


def _sort_dispatcher(a, axis=None, kind=None, order=None):
def _sort_dispatcher(a, axis=None, kind=None, order=None, *, stable=None):
return (a,)


@array_function_dispatch(_sort_dispatcher)
def sort(a, axis=-1, kind=None, order=None):
def sort(a, axis=-1, kind=None, order=None, *, stable=None):
"""
Return a sorted copy of an array.

Expand All @@ -925,6 +925,13 @@ def sort(a, axis=-1, kind=None, order=None):
be specified as a string, and not all fields need be specified,
but unspecified fields will still be used, in the order in which
they come up in the dtype, to break ties.
stable : bool, optional
Sort stability. If ``True``, the returned array will maintain
the relative order of ``a`` values which compare as equal.
If ``False`` or ``None``, this is not guaranteed. Internally,
this option selects ``kind='stable'``. Default: ``None``.

.. versionadded:: 2.0.0

Returns
-------
Expand Down Expand Up @@ -1053,16 +1060,16 @@ def sort(a, axis=-1, kind=None, order=None):
axis = -1
else:
a = asanyarray(a).copy(order="K")
a.sort(axis=axis, kind=kind, order=order)
a.sort(axis=axis, kind=kind, order=order, stable=stable)
return a


def _argsort_dispatcher(a, axis=None, kind=None, order=None):
def _argsort_dispatcher(a, axis=None, kind=None, order=None, *, stable=None):
return (a,)


@array_function_dispatch(_argsort_dispatcher)
def argsort(a, axis=-1, kind=None, order=None):
def argsort(a, axis=-1, kind=None, order=None, *, stable=None):
"""
Returns the indices that would sort an array.

Expand Down Expand Up @@ -1091,6 +1098,13 @@ def argsort(a, axis=-1, kind=None, order=None):
be specified as a string, and not all fields need be specified,
but unspecified fields will still be used, in the order in which
they come up in the dtype, to break ties.
stable : bool, optional
Sort stability. If ``True``, the returned array will maintain
the relative order of ``a`` values which compare as equal.
If ``False`` or ``None``, this is not guaranteed. Internally,
this option selects ``kind='stable'``. Default: ``None``.

.. versionadded:: 2.0.0

Returns
-------
Expand Down Expand Up @@ -1169,8 +1183,9 @@ def argsort(a, axis=-1, kind=None, order=None):
array([0, 1])

"""
return _wrapfunc(a, 'argsort', axis=axis, kind=kind, order=order)

return _wrapfunc(
a, 'argsort', axis=axis, kind=kind, order=order, stable=stable
)

def _argmax_dispatcher(a, axis=None, out=None, *, keepdims=np._NoValue):
return (a, out)
Expand Down
6 changes: 6 additions & 0 deletions numpy/_core/fromnumeric.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -211,20 +211,26 @@ def sort(
axis: None | SupportsIndex = ...,
kind: None | _SortKind = ...,
order: None | str | Sequence[str] = ...,
*,
stable: None | bool = ...,
) -> NDArray[_SCT]: ...
@overload
def sort(
a: ArrayLike,
axis: None | SupportsIndex = ...,
kind: None | _SortKind = ...,
order: None | str | Sequence[str] = ...,
*,
stable: None | bool = ...,
) -> NDArray[Any]: ...

def argsort(
a: ArrayLike,
axis: None | SupportsIndex = ...,
kind: None | _SortKind = ...,
order: None | str | Sequence[str] = ...,
*,
stable: None | bool = ...,
) -> NDArray[intp]: ...

@overload
Expand Down
1 change: 1 addition & 0 deletions numpy/_core/include/numpy/ndarraytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ enum NPY_TYPECHAR {
* depend on the data type.
*/
typedef enum {
_NPY_SORT_UNDEFINED=-1,
NPY_QUICKSORT=0,
NPY_HEAPSORT=1,
NPY_MERGESORT=2,
Expand Down
1 change: 1 addition & 0 deletions numpy/_core/src/multiarray/_multiarray_tests.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,7 @@ run_sortkind_converter(PyObject* NPY_UNUSED(self), PyObject *args)
return NULL;
}
switch (kind) {
case _NPY_SORT_UNDEFINED: return PyUnicode_FromString("_NPY_SORT_UNDEFINED");
case NPY_QUICKSORT: return PyUnicode_FromString("NPY_QUICKSORT");
case NPY_HEAPSORT: return PyUnicode_FromString("NPY_HEAPSORT");
case NPY_STABLESORT: return PyUnicode_FromString("NPY_STABLESORT");
Expand Down
22 changes: 22 additions & 0 deletions numpy/_core/src/multiarray/conversion_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,28 @@ PyArray_BoolConverter(PyObject *object, npy_bool *val)
return NPY_SUCCEED;
}

/*
* Optionally convert an object to true / false
*/
NPY_NO_EXPORT int
PyArray_OptionalBoolConverter(PyObject *object, int *val)
{
/* Leave the desired default from the caller for Py_None */
if (object == Py_None) {
return NPY_SUCCEED;
}
if (PyObject_IsTrue(object)) {
*val = 1;
}
else {
*val = 0;
}
if (PyErr_Occurred()) {
return NPY_FAIL;
}
return NPY_SUCCEED;
}

static int
string_converter_helper(
PyObject *object,
Expand Down
3 changes: 3 additions & 0 deletions numpy/_core/src/multiarray/conversion_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ PyArray_BufferConverter(PyObject *obj, PyArray_Chunk *buf);
NPY_NO_EXPORT int
PyArray_BoolConverter(PyObject *object, npy_bool *val);

NPY_NO_EXPORT int
PyArray_OptionalBoolConverter(PyObject *object, int *val);

NPY_NO_EXPORT int
PyArray_ByteorderConverter(PyObject *obj, char *endian);

Expand Down
34 changes: 31 additions & 3 deletions numpy/_core/src/multiarray/methods.c
Original file line number Diff line number Diff line change
Expand Up @@ -1235,18 +1235,20 @@ static PyObject *
array_sort(PyArrayObject *self,
PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames)
{
int axis=-1;
int axis = -1;
int val;
NPY_SORTKIND sortkind = NPY_QUICKSORT;
NPY_SORTKIND sortkind = _NPY_SORT_UNDEFINED;
PyObject *order = NULL;
PyArray_Descr *saved = NULL;
PyArray_Descr *newd;
int stable = -1;
NPY_PREPARE_ARGPARSER;

if (npy_parse_arguments("sort", args, len_args, kwnames,
"|axis", &PyArray_PythonPyIntFromInt, &axis,
"|kind", &PyArray_SortkindConverter, &sortkind,
"|order", NULL, &order,
"$stable", &PyArray_OptionalBoolConverter, &stable,
NULL, NULL, NULL) < 0) {
return NULL;
}
Expand Down Expand Up @@ -1281,6 +1283,18 @@ array_sort(PyArrayObject *self,
newd->names = new_name;
((PyArrayObject_fields *)self)->descr = newd;
}
if (sortkind != _NPY_SORT_UNDEFINED && stable != -1) {
PyErr_SetString(PyExc_ValueError,
"`kind` and `stable` parameters can't be provided at "
"the same time. Use only one of them.");
return NULL;
}
else if ((sortkind == _NPY_SORT_UNDEFINED && stable == -1) || (stable == 0)) {
sortkind = NPY_QUICKSORT;
}
else if (stable == 1) {
sortkind = NPY_STABLESORT;
}

val = PyArray_Sort(self, axis, sortkind);
if (order != NULL) {
Expand Down Expand Up @@ -1371,15 +1385,17 @@ array_argsort(PyArrayObject *self,
PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames)
{
int axis = -1;
NPY_SORTKIND sortkind = NPY_QUICKSORT;
NPY_SORTKIND sortkind = _NPY_SORT_UNDEFINED;
PyObject *order = NULL, *res;
PyArray_Descr *newd, *saved=NULL;
int stable = -1;
NPY_PREPARE_ARGPARSER;

if (npy_parse_arguments("argsort", args, len_args, kwnames,
"|axis", &PyArray_AxisConverter, &axis,
"|kind", &PyArray_SortkindConverter, &sortkind,
"|order", NULL, &order,
"$stable", &PyArray_OptionalBoolConverter, &stable,
NULL, NULL, NULL) < 0) {
return NULL;
}
Expand Down Expand Up @@ -1414,6 +1430,18 @@ array_argsort(PyArrayObject *self,
newd->names = new_name;
((PyArrayObject_fields *)self)->descr = newd;
}
if (sortkind != _NPY_SORT_UNDEFINED && stable != -1) {
PyErr_SetString(PyExc_ValueError,
"`kind` and `stable` parameters can't be provided at "
"the same time. Use only one of them.");
return NULL;
}
else if ((sortkind == _NPY_SORT_UNDEFINED && stable == -1) || (stable == 0)) {
sortkind = NPY_QUICKSORT;
}
else if (stable == 1) {
sortkind = NPY_STABLESORT;
}

res = PyArray_ArgSort(self, axis, sortkind);
if (order != NULL) {
Expand Down
12 changes: 12 additions & 0 deletions numpy/_core/tests/test_multiarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,12 @@ def test_sort(self):
b = np.sort(a)
assert_equal(b, a[::-1], msg)

with assert_raises_regex(
ValueError,
"kind` and `stable` parameters can't be provided at the same time"
):
np.sort(a, kind="stable", stable=True)

# all c scalar sorts use the same code with different types
# so it suffices to run a quick check with one type. The number
# of sorted items must be greater than ~50 to check the actual
Expand Down Expand Up @@ -2481,6 +2487,12 @@ def test_argsort(self):
a = np.array(['aaaaaaaaa' for i in range(100)], dtype=np.str_)
assert_equal(a.argsort(kind='m'), r)

with assert_raises_regex(
ValueError,
"kind` and `stable` parameters can't be provided at the same time"
):
np.argsort(a, kind="stable", stable=True)

def test_sort_unicode_kind(self):
d = np.arange(10)
k = b'\xc3\xa4'.decode("UTF8")
Expand Down
19 changes: 12 additions & 7 deletions numpy/linalg/_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1965,12 +1965,12 @@ def cond(x, p=None):
return r


def _matrix_rank_dispatcher(A, tol=None, hermitian=None):
def _matrix_rank_dispatcher(A, tol=None, hermitian=None, *, rtol=None):
return (A,)


@array_function_dispatch(_matrix_rank_dispatcher)
def matrix_rank(A, tol=None, hermitian=False):
def matrix_rank(A, tol=None, hermitian=False, *, rtol=None):
"""
Return matrix rank of array using SVD method

Expand Down Expand Up @@ -1998,6 +1998,11 @@ def matrix_rank(A, tol=None, hermitian=False):
Defaults to False.

.. versionadded:: 1.14
rtol : (...) array_like, float, optional
Parameter for the relative tolerance component. Only ``tol`` or
``rtol`` can be set at a time. Defaults to ``max(M, N) * eps``.

.. versionadded:: 2.0.0

Returns
-------
Expand Down Expand Up @@ -2067,12 +2072,12 @@ def matrix_rank(A, tol=None, hermitian=False):
if A.ndim < 2:
return int(not all(A == 0))
S = svd(A, compute_uv=False, hermitian=hermitian)
if rtol is not None and tol is not None:
raise ValueError("`tol` and `rtol` can't be both set.")
if rtol is None:
rtol = max(A.shape[-2:]) * finfo(S.dtype).eps
if tol is None:
tol = (
S.max(axis=-1, keepdims=True) *
max(A.shape[-2:]) *
finfo(S.dtype).eps
)
tol = S.max(axis=-1, keepdims=True) * rtol
Copy link
Member

Choose a reason for hiding this comment

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

I think this logic is incorrect. tol should also have a newaxis appended in the end in this case too (i.e., the else below should be removed and the tol = asarray(tol)[..., newaxis] line should be run unconditionally). Right now we have:

>>> import numpy as np
>>> x = np.zeros((4, 3, 2))
>>> rtol = np.zeros((4,))
>>> np.linalg.matrix_rank(x, tol=rtol)
array([0, 0, 0, 0])
>>> np.linalg.matrix_rank(x, rtol=rtol)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/aaronmeurer/Documents/numpy/numpy/linalg/_linalg.py", line 2083, in matrix_rank
    return count_nonzero(S > tol, axis=-1)
                         ^^^^^^^
ValueError: operands could not be broadcast together with shapes (4,2) (4,4)

The broadcasting for rtol should be the same as for tol. It should broadcast against the stack shape (x.shape[:-2]).

Copy link
Member Author

Choose a reason for hiding this comment

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

That's right - Here's a fix for it: #25877

else:
tol = asarray(tol)[..., newaxis]
return count_nonzero(S > tol, axis=-1)
Expand Down
2 changes: 2 additions & 0 deletions numpy/linalg/_linalg.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ def matrix_rank(
A: _ArrayLikeComplex_co,
tol: None | _ArrayLikeFloat_co = ...,
hermitian: bool = ...,
*,
rtol: None | _ArrayLikeFloat_co = ...,
) -> Any: ...

@overload
Expand Down
5 changes: 5 additions & 0 deletions numpy/linalg/tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,11 @@ def test_matrix_rank(self):
# works on scalar
assert_equal(matrix_rank(1), 1)

with assert_raises_regex(
ValueError, "`tol` and `rtol` can\'t be both set."
):
matrix_rank(I, tol=0.01, rtol=0.01)

def test_symmetric_rank(self):
assert_equal(4, matrix_rank(np.eye(4), hermitian=True))
assert_equal(1, matrix_rank(np.ones((4, 4)), hermitian=True))
Expand Down
Loading