Repository: scipy/scipy

Issue:
DEP: Execute deprecation of pinv2
From #13831. That PR does not specify a specific removal date, so I think we could sharpen the warnings to indicate removal by 1.10/1.11; on the other hands, two releases between deprecation and removal is also common practice, so would be an option as well IMO. In either case, there's something actionable for 1.9, hence the milestone.

CC @ilayn 


Discussion:

Resolving patch:
diff --git a/scipy/linalg/__init__.py b/scipy/linalg/__init__.py
--- a/scipy/linalg/__init__.py
+++ b/scipy/linalg/__init__.py
@@ -36,7 +36,6 @@
    norm - Matrix and vector norm
    lstsq - Solve a linear least-squares problem
    pinv - Pseudo-inverse (Moore-Penrose) using lstsq
-   pinv2 - Pseudo-inverse using svd
    pinvh - Pseudo-inverse of hermitian matrix
    kron - Kronecker product of two arrays
    khatri_rao - Khatri-Rao product of two arrays
diff --git a/scipy/linalg/_basic.py b/scipy/linalg/_basic.py
--- a/scipy/linalg/_basic.py
+++ b/scipy/linalg/_basic.py
@@ -16,7 +16,7 @@
 
 __all__ = ['solve', 'solve_triangular', 'solveh_banded', 'solve_banded',
            'solve_toeplitz', 'solve_circulant', 'inv', 'det', 'lstsq',
-           'pinv', 'pinv2', 'pinvh', 'matrix_balance', 'matmul_toeplitz']
+           'pinv', 'pinvh', 'matrix_balance', 'matmul_toeplitz']
 
 
 # Linear equations
@@ -1334,61 +1334,6 @@ def pinv(a, atol=None, rtol=None, return_rank=False, check_finite=True,
         return B
 
 
-def pinv2(a, cond=None, rcond=None, return_rank=False, check_finite=True):
-    """
-    Compute the (Moore-Penrose) pseudo-inverse of a matrix.
-
-    `scipy.linalg.pinv2` is deprecated since SciPy 1.7.0, use
-    `scipy.linalg.pinv` instead for better tolerance control.
-
-    Calculate a generalized inverse of a matrix using its
-    singular-value decomposition and including all 'large' singular
-    values.
-
-    Parameters
-    ----------
-    a : (M, N) array_like
-        Matrix to be pseudo-inverted.
-    cond, rcond : float or None
-        Cutoff for 'small' singular values; singular values smaller than this
-        value are considered as zero. If both are omitted, the default value
-        ``max(M,N)*largest_singular_value*eps`` is used where ``eps`` is the
-        machine precision value of the datatype of ``a``.
-
-        .. versionchanged:: 1.3.0
-            Previously the default cutoff value was just ``eps*f`` where ``f``
-            was ``1e3`` for single precision and ``1e6`` for double precision.
-
-    return_rank : bool, optional
-        If True, return the effective rank of the matrix.
-    check_finite : bool, optional
-        Whether to check that the input matrix contains only finite numbers.
-        Disabling may give a performance gain, but may result in problems
-        (crashes, non-termination) if the inputs do contain infinities or NaNs.
-
-    Returns
-    -------
-    B : (N, M) ndarray
-        The pseudo-inverse of matrix `a`.
-    rank : int
-        The effective rank of the matrix. Returned if `return_rank` is True.
-
-    Raises
-    ------
-    LinAlgError
-        If SVD computation does not converge.
-
-    """
-    # SciPy 1.7.0 2021-04-10
-    warn('scipy.linalg.pinv2 is deprecated since SciPy 1.7.0, use '
-         'scipy.linalg.pinv instead', DeprecationWarning, stacklevel=2)
-    if rcond is not None:
-        cond = rcond
-
-    return pinv(a=a, atol=cond, rtol=None, return_rank=return_rank,
-                check_finite=check_finite)
-
-
 def pinvh(a, atol=None, rtol=None, lower=True, return_rank=False,
           check_finite=True, cond=None, rcond=None):
     """
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -8,7 +8,7 @@
 __all__ = [  # noqa: F822
     'solve', 'solve_triangular', 'solveh_banded', 'solve_banded',
     'solve_toeplitz', 'solve_circulant', 'inv', 'det', 'lstsq',
-    'pinv', 'pinv2', 'pinvh', 'matrix_balance', 'matmul_toeplitz',
+    'pinv', 'pinvh', 'matrix_balance', 'matmul_toeplitz',
     'atleast_1d', 'atleast_2d', 'get_flinalg_funcs', 'get_lapack_funcs',
     'LinAlgError', 'LinAlgWarning', 'levinson'
 ]


Test patch:
diff --git a/scipy/linalg/tests/test_basic.py b/scipy/linalg/tests/test_basic.py
--- a/scipy/linalg/tests/test_basic.py
+++ b/scipy/linalg/tests/test_basic.py
@@ -13,7 +13,7 @@
 import pytest
 from pytest import raises as assert_raises
 
-from scipy.linalg import (solve, inv, det, lstsq, pinv, pinv2, pinvh, norm,
+from scipy.linalg import (solve, inv, det, lstsq, pinv, pinvh, norm,
                           solve_banded, solveh_banded, solve_triangular,
                           solve_circulant, circulant, LinAlgError, block_diag,
                           matrix_balance, qr, LinAlgWarning)
@@ -1277,8 +1277,6 @@ def test_simple_real(self):
         a = array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], dtype=float)
         a_pinv = pinv(a)
         assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
-        a_pinv = pinv2(a)
-        assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
 
     def test_simple_complex(self):
         a = (array([[1, 2, 3], [4, 5, 6], [7, 8, 10]],
@@ -1286,39 +1284,42 @@ def test_simple_complex(self):
                                        dtype=float))
         a_pinv = pinv(a)
         assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
-        a_pinv = pinv2(a)
-        assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
 
     def test_simple_singular(self):
         a = array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=float)
         a_pinv = pinv(a)
-        a_pinv2 = pinv2(a)
-        assert_array_almost_equal(a_pinv, a_pinv2)
+        expected = array([[-6.38888889e-01, -1.66666667e-01, 3.05555556e-01],
+                          [-5.55555556e-02, 1.30136518e-16, 5.55555556e-02],
+                          [5.27777778e-01, 1.66666667e-01, -1.94444444e-01]])
+        assert_array_almost_equal(a_pinv, expected)
 
     def test_simple_cols(self):
         a = array([[1, 2, 3], [4, 5, 6]], dtype=float)
         a_pinv = pinv(a)
-        a_pinv2 = pinv2(a)
-        assert_array_almost_equal(a_pinv, a_pinv2)
+        expected = array([[-0.94444444, 0.44444444],
+                          [-0.11111111, 0.11111111],
+                          [0.72222222, -0.22222222]])
+        assert_array_almost_equal(a_pinv, expected)
 
     def test_simple_rows(self):
         a = array([[1, 2], [3, 4], [5, 6]], dtype=float)
         a_pinv = pinv(a)
-        a_pinv2 = pinv2(a)
-        assert_array_almost_equal(a_pinv, a_pinv2)
+        expected = array([[-1.33333333, -0.33333333, 0.66666667],
+                          [1.08333333, 0.33333333, -0.41666667]])
+        assert_array_almost_equal(a_pinv, expected)
 
     def test_check_finite(self):
         a = array([[1, 2, 3], [4, 5, 6.], [7, 8, 10]])
         a_pinv = pinv(a, check_finite=False)
         assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
-        a_pinv = pinv2(a, check_finite=False)
-        assert_array_almost_equal(dot(a, a_pinv), np.eye(3))
 
     def test_native_list_argument(self):
         a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
         a_pinv = pinv(a)
-        a_pinv2 = pinv2(a)
-        assert_array_almost_equal(a_pinv, a_pinv2)
+        expected = array([[-6.38888889e-01, -1.66666667e-01, 3.05555556e-01],
+                          [-5.55555556e-02, 1.30136518e-16, 5.55555556e-02],
+                          [5.27777778e-01, 1.66666667e-01, -1.94444444e-01]])
+        assert_array_almost_equal(a_pinv, expected)
 
     def test_atol_rtol(self):
         n = 12
@@ -1348,7 +1349,6 @@ def test_atol_rtol(self):
         assert_allclose(np.linalg.norm(adiff2), 4.233, rtol=0.01)
 
 
-@pytest.mark.filterwarnings('ignore::DeprecationWarning')
 class TestPinvSymmetric:
 
     def setup_method(self):
@@ -1366,7 +1366,7 @@ def test_nonpositive(self):
         u, s, vt = np.linalg.svd(a)
         s[0] *= -1
         a = np.dot(u * s, vt)  # a is now symmetric non-positive and singular
-        a_pinv = pinv2(a)
+        a_pinv = pinv(a)
         a_pinvh = pinvh(a)
         assert_array_almost_equal(a_pinv, a_pinvh)
 
@@ -1412,9 +1412,8 @@ def test_atol_rtol(self):
         assert_allclose(norm(adiff2), 1e-4, rtol=0.1)
 
 
-@pytest.mark.filterwarnings('ignore::DeprecationWarning')
 @pytest.mark.parametrize('scale', (1e-20, 1., 1e20))
-@pytest.mark.parametrize('pinv_', (pinv, pinvh, pinv2))
+@pytest.mark.parametrize('pinv_', (pinv, pinvh))
 def test_auto_rcond(scale, pinv_):
     x = np.array([[1, 0], [0, 1e-10]]) * scale
     expected = np.diag(1. / np.diag(x))
@@ -1555,10 +1554,6 @@ def test_lstsq(self):
     def test_pinv(self):
         assert_no_overwrite(pinv, [(3, 3)])
 
-    @pytest.mark.filterwarnings('ignore::DeprecationWarning')
-    def test_pinv2(self):
-        assert_no_overwrite(pinv2, [(3, 3)])
-
     def test_pinvh(self):
         assert_no_overwrite(pinvh, [(3, 3)])