Skip to content

Commit e8f348b

Browse files
authored
add rtol kwarg to dpnp.linalg.pinv and dpnp.linalg.matrix_rank (#2124)
* add rtol keyword argument to dpnp.linalg.pinv and dpnp.linalg.matrix_rank * address comments
1 parent 45d9bcc commit e8f348b

File tree

3 files changed

+111
-23
lines changed

3 files changed

+111
-23
lines changed

dpnp/linalg/dpnp_iface_linalg.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
]
100100

101101

102-
def cholesky(a, upper=False):
102+
def cholesky(a, /, *, upper=False):
103103
"""
104104
Cholesky decomposition.
105105
@@ -1062,7 +1062,7 @@ def matrix_power(a, n):
10621062
return dpnp_matrix_power(a, n)
10631063

10641064

1065-
def matrix_rank(A, tol=None, hermitian=False):
1065+
def matrix_rank(A, tol=None, hermitian=False, *, rtol=None):
10661066
"""
10671067
Return matrix rank of array using SVD method.
10681068
@@ -1073,21 +1073,27 @@ def matrix_rank(A, tol=None, hermitian=False):
10731073
----------
10741074
A : {(M,), (..., M, N)} {dpnp.ndarray, usm_ndarray}
10751075
Input vector or stack of matrices.
1076-
tol : (...) {float, dpnp.ndarray, usm_ndarray}, optional
1077-
Threshold below which SVD values are considered zero. If `tol` is
1078-
``None``, and ``S`` is an array with singular values for `M`, and
1079-
``eps`` is the epsilon value for datatype of ``S``, then `tol` is
1080-
set to ``S.max() * max(M.shape) * eps``.
1076+
tol : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional
1077+
Threshold below which SVD values are considered zero. Only `tol` or
1078+
`rtol` can be set at a time. If none of them are provided, defaults
1079+
to ``S.max() * max(M, N) * eps`` where `S` is an array with singular
1080+
values for `A`, and `eps` is the epsilon value for datatype of `S`.
10811081
Default: ``None``.
10821082
hermitian : bool, optional
10831083
If ``True``, `A` is assumed to be Hermitian (symmetric if real-valued),
10841084
enabling a more efficient method for finding singular values.
10851085
Default: ``False``.
1086+
rtol : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional
1087+
Parameter for the relative tolerance component. Only `tol` or `rtol`
1088+
can be set at a time. If none of them are provided, defaults to
1089+
``max(M, N) * eps`` where `eps` is the epsilon value for datatype
1090+
of `S` (an array with singular values for `A`).
1091+
Default: ``None``.
10861092
10871093
Returns
10881094
-------
10891095
rank : (...) dpnp.ndarray
1090-
Rank of A.
1096+
Rank of `A`.
10911097
10921098
See Also
10931099
--------
@@ -1114,8 +1120,12 @@ def matrix_rank(A, tol=None, hermitian=False):
11141120
dpnp.check_supported_arrays_type(
11151121
tol, scalar_type=True, all_scalars=True
11161122
)
1123+
if rtol is not None:
1124+
dpnp.check_supported_arrays_type(
1125+
rtol, scalar_type=True, all_scalars=True
1126+
)
11171127

1118-
return dpnp_matrix_rank(A, tol=tol, hermitian=hermitian)
1128+
return dpnp_matrix_rank(A, tol=tol, hermitian=hermitian, rtol=rtol)
11191129

11201130

11211131
def matrix_transpose(x, /):
@@ -1456,7 +1466,7 @@ def outer(x1, x2, /):
14561466
return dpnp.outer(x1, x2)
14571467

14581468

1459-
def pinv(a, rcond=1e-15, hermitian=False):
1469+
def pinv(a, rcond=None, hermitian=False, *, rtol=None):
14601470
"""
14611471
Compute the (Moore-Penrose) pseudo-inverse of a matrix.
14621472
@@ -1469,20 +1479,27 @@ def pinv(a, rcond=1e-15, hermitian=False):
14691479
----------
14701480
a : (..., M, N) {dpnp.ndarray, usm_ndarray}
14711481
Matrix or stack of matrices to be pseudo-inverted.
1472-
rcond : {float, dpnp.ndarray, usm_ndarray}, optional
1482+
rcond : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional
14731483
Cutoff for small singular values.
14741484
Singular values less than or equal to ``rcond * largest_singular_value``
14751485
are set to zero. Broadcasts against the stack of matrices.
1476-
Default: ``1e-15``.
1486+
Only `rcond` or `rtol` can be set at a time. If none of them are
1487+
provided, defaults to ``max(M, N) * dpnp.finfo(a.dtype).eps``.
1488+
Default: ``None``.
14771489
hermitian : bool, optional
14781490
If ``True``, a is assumed to be Hermitian (symmetric if real-valued),
14791491
enabling a more efficient method for finding singular values.
14801492
Default: ``False``.
1493+
rtol : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional
1494+
Same as `rcond`, but it's an Array API compatible parameter name.
1495+
Only `rcond` or `rtol` can be set at a time. If none of them are
1496+
provided, defaults to ``max(M, N) * dpnp.finfo(a.dtype).eps``.
1497+
Default: ``None``.
14811498
14821499
Returns
14831500
-------
14841501
out : (..., N, M) dpnp.ndarray
1485-
The pseudo-inverse of a.
1502+
The pseudo-inverse of `a`.
14861503
14871504
Examples
14881505
--------
@@ -1493,17 +1510,24 @@ def pinv(a, rcond=1e-15, hermitian=False):
14931510
>>> a = np.random.randn(9, 6)
14941511
>>> B = np.linalg.pinv(a)
14951512
>>> np.allclose(a, np.dot(a, np.dot(B, a)))
1496-
array([ True])
1513+
array(True)
14971514
>>> np.allclose(B, np.dot(B, np.dot(a, B)))
1498-
array([ True])
1515+
array(True)
14991516
15001517
"""
15011518

15021519
dpnp.check_supported_arrays_type(a)
1503-
dpnp.check_supported_arrays_type(rcond, scalar_type=True, all_scalars=True)
1520+
if rcond is not None:
1521+
dpnp.check_supported_arrays_type(
1522+
rcond, scalar_type=True, all_scalars=True
1523+
)
1524+
if rtol is not None:
1525+
dpnp.check_supported_arrays_type(
1526+
rtol, scalar_type=True, all_scalars=True
1527+
)
15041528
assert_stacked_2d(a)
15051529

1506-
return dpnp_pinv(a, rcond=rcond, hermitian=hermitian)
1530+
return dpnp_pinv(a, rcond=rcond, hermitian=hermitian, rtol=rtol)
15071531

15081532

15091533
def qr(a, mode="reduced"):

dpnp/linalg/dpnp_utils_linalg.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,24 +2214,33 @@ def dpnp_matrix_power(a, n):
22142214
return result
22152215

22162216

2217-
def dpnp_matrix_rank(A, tol=None, hermitian=False):
2217+
def dpnp_matrix_rank(A, tol=None, hermitian=False, rtol=None):
22182218
"""
2219-
dpnp_matrix_rank(A, tol=None, hermitian=False)
2219+
dpnp_matrix_rank(A, tol=None, hermitian=False, rtol=None)
22202220
22212221
Return matrix rank of array using SVD method.
22222222
22232223
"""
22242224

2225+
if rtol is not None and tol is not None:
2226+
raise ValueError("`tol` and `rtol` can't be both set.")
2227+
22252228
if A.ndim < 2:
22262229
return (A != 0).any().astype(int)
22272230

22282231
S = dpnp_svd(A, compute_uv=False, hermitian=hermitian)
22292232

22302233
if tol is None:
2231-
rtol = max(A.shape[-2:]) * dpnp.finfo(S.dtype).eps
2234+
if rtol is None:
2235+
rtol = max(A.shape[-2:]) * dpnp.finfo(S.dtype).eps
2236+
elif not dpnp.isscalar(rtol):
2237+
# Add a new axis to make it broadcastable against S
2238+
# needed for S > tol comparison below
2239+
rtol = rtol[..., None]
22322240
tol = S.max(axis=-1, keepdims=True) * rtol
22332241
elif not dpnp.isscalar(tol):
2234-
# Add a new axis to match NumPy's output
2242+
# Add a new axis to make it broadcastable against S,
2243+
# needed for S > tol comparison below
22352244
tol = tol[..., None]
22362245

22372246
return dpnp.count_nonzero(S > tol, axis=-1)
@@ -2320,9 +2329,9 @@ def dpnp_norm(x, ord=None, axis=None, keepdims=False):
23202329
raise ValueError("Improper number of dimensions to norm.")
23212330

23222331

2323-
def dpnp_pinv(a, rcond=1e-15, hermitian=False):
2332+
def dpnp_pinv(a, rcond=None, hermitian=False, rtol=None):
23242333
"""
2325-
dpnp_pinv(a, rcond=1e-15, hermitian=False):
2334+
dpnp_pinv(a, rcond=None, hermitian=False, rtol=None)
23262335
23272336
Compute the Moore-Penrose pseudoinverse of `a` matrix.
23282337
@@ -2331,6 +2340,15 @@ def dpnp_pinv(a, rcond=1e-15, hermitian=False):
23312340
23322341
"""
23332342

2343+
if rcond is None:
2344+
if rtol is None:
2345+
dtype = dpnp.result_type(a.dtype, dpnp.default_float_type(a.device))
2346+
rcond = max(a.shape[-2:]) * dpnp.finfo(dtype).eps
2347+
else:
2348+
rcond = rtol
2349+
elif rtol is not None:
2350+
raise ValueError("`rtol` and `rcond` can't be both set.")
2351+
23342352
if _is_empty_2d(a):
23352353
m, n = a.shape[-2:]
23362354
sh = a.shape[:-2] + (n, m)

tests/test_linalg.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,32 @@ def test_matrix_rank_tolerance(self, high_tol, low_tol):
20972097
)
20982098
assert np_rank_low_tol == dp_rank_low_tol
20992099

2100+
# rtol kwarg was added in numpy 2.0
2101+
@testing.with_requires("numpy>=2.0")
2102+
@pytest.mark.parametrize(
2103+
"tol",
2104+
[0.99e-6, numpy.array(1.01e-6), numpy.ones(4) * [0.99e-6]],
2105+
ids=["float", "0-D array", "1-D array"],
2106+
)
2107+
def test_matrix_rank_tol(self, tol):
2108+
a = numpy.zeros((4, 3, 2))
2109+
a_dp = inp.array(a)
2110+
2111+
if isinstance(tol, numpy.ndarray):
2112+
dp_tol = inp.array(
2113+
tol, usm_type=a_dp.usm_type, sycl_queue=a_dp.sycl_queue
2114+
)
2115+
else:
2116+
dp_tol = tol
2117+
2118+
expected = numpy.linalg.matrix_rank(a, rtol=tol)
2119+
result = inp.linalg.matrix_rank(a_dp, rtol=dp_tol)
2120+
assert_dtype_allclose(result, expected)
2121+
2122+
expected = numpy.linalg.matrix_rank(a, tol=tol)
2123+
result = inp.linalg.matrix_rank(a_dp, tol=dp_tol)
2124+
assert_dtype_allclose(result, expected)
2125+
21002126
def test_matrix_rank_errors(self):
21012127
a_dp = inp.array([[1, 2], [3, 4]], dtype="float32")
21022128

@@ -2121,6 +2147,11 @@ def test_matrix_rank_errors(self):
21212147
tol_dp_q,
21222148
)
21232149

2150+
# both tol and rtol are given
2151+
assert_raises(
2152+
ValueError, inp.linalg.matrix_rank, a_dp, tol=1e-06, rtol=1e-04
2153+
)
2154+
21242155

21252156
# numpy.linalg.matrix_transpose() is available since numpy >= 2.0
21262157
@testing.with_requires("numpy>=2.0")
@@ -3141,6 +3172,16 @@ def test_pinv_hermitian(self, dtype, shape):
31413172
reconstructed = inp.dot(inp.dot(a_dp, B_dp), a_dp)
31423173
assert_allclose(reconstructed, a_dp, rtol=tol, atol=tol)
31433174

3175+
# rtol kwarg was added in numpy 2.0
3176+
@testing.with_requires("numpy>=2.0")
3177+
def test_pinv_rtol(self):
3178+
a = numpy.ones((2, 2))
3179+
a_dp = inp.array(a)
3180+
3181+
expected = numpy.linalg.pinv(a, rtol=1e-15)
3182+
result = inp.linalg.pinv(a_dp, rtol=1e-15)
3183+
assert_dtype_allclose(result, expected)
3184+
31443185
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
31453186
@pytest.mark.parametrize(
31463187
"shape",
@@ -3207,6 +3248,11 @@ def test_pinv_errors(self):
32073248
rcond_dp_q = inp.array([0.5], dtype="float32", sycl_queue=rcond_queue)
32083249
assert_raises(ValueError, inp.linalg.pinv, a_dp_q, rcond_dp_q)
32093250

3251+
# both rcond and rtol are given
3252+
assert_raises(
3253+
ValueError, inp.linalg.pinv, a_dp, rcond=1e-06, rtol=1e-04
3254+
)
3255+
32103256

32113257
class TestTensorinv:
32123258
@pytest.mark.parametrize("dtype", get_all_dtypes())

0 commit comments

Comments
 (0)