8000 MAINT: Fix the cutoff value inconsistency for pinv,pinv2,pinvh (#10067) · scipy/scipy@c42462a · GitHub
[go: up one dir, main page]

Skip to content

Commit c42462a

Browse files
authored
MAINT: Fix the cutoff value inconsistency for pinv,pinv2,pinvh (#10067)
* MAINT: Fix the cutoff value inconsistency for pinv family * DOC: Reworded pinv, pinv2, pinvh cond/rcond parameters
1 parent b053854 commit c42462a

File tree

2 files changed

+58
-23
lines changed

2 files changed

+58
-23
lines changed

scipy/linalg/basic.py

+36-23
Original file line numberDiff line numberDiff line change
@@ -1256,11 +1256,16 @@ def pinv(a, cond=None, rcond=None, return_rank=False, check_finite=True):
12561256
a : (M, N) array_like
12571257
Matrix to be pseudo-inverted.
12581258
cond, rcond : float, optional
1259-
Cutoff for 'small' singular values in the least-squares solver.
1260-
Singular values smaller than ``rcond * largest_singular_value``
1261-
are considered zero.
1262-
If None, it is set to ``np.finfo(a.dtype).eps``.
1263-
If `a` is an array of integers, it is set to ``np.finfo('float64').eps``.
1259+
Cutoff factor for 'small' singular values. In `lstsq`,
1260+
singular values less than ``cond*largest_singular_value`` will be
1261+
considered as zero. If both are omitted, the default value
1262+
``max(M, N) * eps`` is passed to `lstsq` where ``eps`` is the
1263+
corresponding machine precision value of the datatype of ``a``.
1264+
1265+
.. versionchanged:: 1.3.0
1266+
Previously the default cutoff value was just `eps` without the
1267+
factor ``max(M, N)``.
1268+
12641269
return_rank : bool, optional
12651270
if True, return the effective rank of the matrix
12661271
check_finite : bool, optional
@@ -1293,9 +1298,13 @@ def pinv(a, cond=None, rcond=None, return_rank=False, check_finite=True):
12931298
"""
12941299
a = _asarray_validated(a, check_finite=check_finite)
12951300
b = np.identity(a.shape[0], dtype=a.dtype)
1301+
12961302
if rcond is not None:
12971303
cond = rcond
12981304

1305+
if cond is None:
1306+
cond = max(a.shape) * np.spacing(a.real.dtype.type(1))
1307+
12991308
x, resids, rank, s = lstsq(a, b, cond=cond, check_finite=False)
13001309

13011310
if return_rank:
@@ -1317,12 +1326,15 @@ def pinv2(a, cond=None, rcond=None, return_rank=False, check_finite=True):
13171326
a : (M, N) array_like
13181327
Matrix to be pseudo-inverted.
13191328
cond, rcond : float or None
1320-
Cutoff for 'small' singular values.
1321-
Singular values smaller than ``rcond*largest_singular_value``
1322-
are considered zero.
1323-
If None and the dtype of `a` is ``np.float32``, it is set to
1324-
``np.finfo('float32').eps * 1e3``.
1325-
Otherwise, it is set to ``np.finfo('float64').eps * 1e6``.
1329+
Cutoff for 'small' singular values; singular values smaller than this
1330+
value are considered as zero. If both are omitted, the default value
1331+
``max(M,N)*largest_singular_value*eps`` is used where ``eps`` is the
1332+
machine precision value of the datatype of ``a``.
1333+
1334+
.. versionchanged:: 1.3.0
1335+
Previously the default cutoff value was just ``eps*f`` where ``f``
1336+
was ``1e3`` for single precision and ``1e6`` for double precision.
1337+
13261338
return_rank : bool, optional
13271339
If True, return the effective rank of the matrix.
13281340
check_finite : bool, optional
@@ -1360,10 +1372,9 @@ def pinv2(a, cond=None, rcond=None, return_rank=False, check_finite=True):
13601372
cond = rcond
13611373
if cond in [None, -1]:
13621374
t = u.dtype.char.lower()
1363-
factor = {'f': 1E3, 'd': 1E6}
1364-
cond = factor[t] * np.finfo(t).eps
1375+
cond = np.max(s) * max(a.shape) * np.finfo(t).eps
13651376

1366-
rank = np.sum(s > cond * np.max(s))
1377+
rank = np.sum(s > cond)
13671378

13681379
u = u[:, :rank]
13691380
u /= s[:rank]
@@ -1389,12 +1400,15 @@ def pinvh(a, cond=None, rcond=None, lower=True, return_rank=False,
13891400
a : (N, N) array_like
13901401
Real symmetric or complex hermetian matrix to be pseudo-inverted
13911402
cond, rcond : float or None
1392-
Cutoff for 'small' singular values.
1393-
Singular values smaller than ``rcond*largest_singular_value``
1394-
are considered zero.
1395-
If None and the dtype of `a` is ``np.float32``, it is set to
1396-
``np.finfo('float32').eps * 1e3``.
1397-
Otherwise, it is set to ``np.finfo('float64').eps * 1e6``.
1403+
Cutoff for 'small' singular values; singular values smaller than this
1404+
value are considered as zero. If both are omitted, the default
1405+
``max(M,N)*largest_eigenvalue*eps`` is used where ``eps`` is the
1406+
machine precision value of the datatype of 6D40 ``a``.
1407+
1408+
.. versionchanged:: 1.3.0
1409+
Previously the default cutoff value was just ``eps*f`` where ``f``
1410+
was ``1e3`` for single precision and ``1e6`` for double precision.
1411+
13981412
lower : bool, optional
13991413
Whether the pertinent array data is taken from the lower or upper
14001414
triangle of `a`. (Default: lower)
@@ -1436,11 +1450,10 @@ def pinvh(a, cond=None, rcond=None, lower=True, return_rank=False,
14361450
cond = rcond
14371451
if cond in [None, -1]:
14381452
t = u.dtype.char.lower()
1439-
factor = {'f': 1E3, 'd': 1E6}
1440-
cond = factor[t] * np.finfo(t).eps
1453+
cond = np.max(np.abs(s)) * max(a.shape) * np.finfo(t).eps
14411454

14421455
# For Hermitian matrices, singular values equal abs(eigenvalues)
1443-
above_cutoff = (abs(s) > cond * np.max(abs(s)))
1456+
above_cutoff = (abs(s) > cond)
14441457
psigma_diag = 1.0 / s[above_cutoff]
14451458
u = u[:, above_cutoff]
14461459

scipy/linalg/tests/test_basic.py

+22
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,28 @@ def test_native_list_argument(self):
12791279
assert_array_almost_equal(np.dot(a, a_pinv), np.eye(3))
12801280

12811281

1282+
def test_pinv_pinv2_comparison(): # As reported in gh-8861
1283+
I_6 = np.eye(6)
1284+
Ts = np.diag([-1] * 4 + [-2], k=-1) + np.diag([-2] + [-1] * 4, k=1)
1285+
T = I_6 + Ts
1286+
A = 25 * (np.kron(I_6, T) + np.kron(Ts, I_6))
1287+
1288+
Ap, Ap2 = pinv(A), pinv2(A)
1289+
1290+
tol = 1e-11
1291+
assert_allclose(A @ Ap @ A - A, A @ Ap2 @ A - A, rtol=0., atol=tol)
1292+
assert_allclose(Ap @ A @ Ap - Ap, Ap2 @ A @ Ap2 - Ap2, rtol=0., atol=tol)
1293+
1294+
1295+
@pytest.mark.parametrize('scale', (1e-20, 1., 1e20))
1296+
@pytest.mark.parametrize('pinv_', (pinv, pinvh, pinv2))
1297+
def test_auto_rcond(scale, pinv_):
1298+
x = np.array([[1, 0], [0, 1e-10]]) * scale
1299+
expected = np.diag(1. / np.diag(x))
1300+
x_inv = pinv_(x)
1301+
assert_allclose(x_inv, expected)
1302+
1303+
12821304
class TestVectorNorms(object):
12831305

12841306
def test_types(self):

0 commit comments

Comments
 (0)
0