Repository: scipy/scipy

Issue:
DEP: add deprecation warnings to kwargs `turbo` / `eigvals` of `linalg.eigh`
Deprecated in documentation only since https://github.com/scipy/scipy/pull/11304

CC @ilayn 


Discussion:

Resolving patch:
diff --git a/scipy/linalg/_decomp.py b/scipy/linalg/_decomp.py
--- a/scipy/linalg/_decomp.py
+++ b/scipy/linalg/_decomp.py
@@ -17,6 +17,8 @@
            'eig_banded', 'eigvals_banded',
            'eigh_tridiagonal', 'eigvalsh_tridiagonal', 'hessenberg', 'cdf2rdf']
 
+import warnings
+
 import numpy
 from numpy import (array, isfinite, inexact, nonzero, iscomplexobj, cast,
                    flatnonzero, conj, asarray, argsort, empty,
@@ -266,7 +268,7 @@ def eig(a, b=None, left=False, right=True, overwrite_a=False,
 
 
 def eigh(a, b=None, lower=True, eigvals_only=False, overwrite_a=False,
-         overwrite_b=False, turbo=True, eigvals=None, type=1,
+         overwrite_b=False, turbo=False, eigvals=None, type=1,
          check_finite=True, subset_by_index=None, subset_by_value=None,
          driver=None):
     """
@@ -336,17 +338,16 @@ def eigh(a, b=None, lower=True, eigvals_only=False, overwrite_a=False,
         Whether to check that the input matrices contain 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.
-    turbo : bool, optional
-        *Deprecated since v1.5.0, use ``driver=gvd`` keyword instead*.
-        Use divide and conquer algorithm (faster but expensive in memory, only
-        for generalized eigenvalue problem and if full set of eigenvalues are
-        requested.). Has no significant effect if eigenvectors are not
-        requested.
-    eigvals : tuple (lo, hi), optional
-        *Deprecated since v1.5.0, use ``subset_by_index`` keyword instead*.
-        Indexes of the smallest and largest (in ascending order) eigenvalues
-        and corresponding eigenvectors to be returned: 0 <= lo <= hi <= M-1.
-        If omitted, all eigenvalues and eigenvectors are returned.
+    turbo : bool, optional, deprecated
+            .. deprecated:: 1.5.0
+                `eigh` keyword argument `turbo` is deprecated in favour of
+                ``driver=gvd`` keyword instead and will be removed in SciPy
+                1.12.0.
+    eigvals : tuple (lo, hi), optional, deprecated
+            .. deprecated:: 1.5.0
+                `eigh` keyword argument `eigvals` is deprecated in favour of
+                `subset_by_index` keyword instead and will be removed in SciPy
+                1.12.0.
 
     Returns
     -------
@@ -436,6 +437,17 @@ def eigh(a, b=None, lower=True, eigvals_only=False, overwrite_a=False,
     (5, 1)
 
     """
+    if turbo:
+        warnings.warn("Keyword argument 'turbo' is deprecated in favour of '"
+                      "driver=gvd' keyword instead and will be removed in "
+                      "SciPy 1.12.0.",
+                      DeprecationWarning, stacklevel=2)
+    if eigvals:
+        warnings.warn("Keyword argument 'eigvals' is deprecated in favour of "
+                      "'subset_by_index' keyword instead and will be removed "
+                      "in SciPy 1.12.0.",
+                      DeprecationWarning, stacklevel=2)
+
     # set lower
     uplo = 'L' if lower else 'U'
     # Set job for Fortran routines
@@ -887,7 +899,7 @@ def eigvals(a, b=None, overwrite_a=False, check_finite=True,
 
 
 def eigvalsh(a, b=None, lower=True, overwrite_a=False,
-             overwrite_b=False, turbo=True, eigvals=None, type=1,
+             overwrite_b=False, turbo=False, eigvals=None, type=1,
              check_finite=True, subset_by_index=None, subset_by_value=None,
              driver=None):
     """
@@ -952,19 +964,15 @@ def eigvalsh(a, b=None, lower=True, overwrite_a=False,
         "evd", "evr", "evx" for standard problems and "gv", "gvd", "gvx" for
         generalized (where b is not None) problems. See the Notes section of
         `scipy.linalg.eigh`.
-    turbo : bool, optional
-        *Deprecated by ``driver=gvd`` option*. Has no significant effect for
-        eigenvalue computations since no eigenvectors are requested.
-
+    turbo : bool, optional, deprecated
         .. deprecated:: 1.5.0
+            'eigvalsh' keyword argument `turbo` is deprecated in favor of
+            ``driver=gvd`` option and will be removed in SciPy 1.12.0.
 
     eigvals : tuple (lo, hi), optional
-        *Deprecated by ``subset_by_index`` keyword*. Indexes of the smallest
-        and largest (in ascending order) eigenvalues and corresponding
-        eigenvectors to be returned: 0 <= lo <= hi <= M-1. If omitted, all
-        eigenvalues and eigenvectors are returned.
-
         .. deprecated:: 1.5.0
+            'eigvalsh' keyword argument `eigvals` is deprecated in favor of
+            `subset_by_index` option and will be removed in SciPy 1.12.0.
 
     Returns
     -------
diff --git a/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py b/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
--- a/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
+++ b/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
@@ -465,7 +465,7 @@ def lobpcg(
         try:
             vals, vecs = eigh(A,
                               B,
-                              eigvals=eigvals,
+                              subset_by_index=eigvals,
                               check_finite=False)
             if largest:
                 # Reverse order to be compatible with eigs() in 'LM' mode.


Test patch:
diff --git a/scipy/linalg/tests/test_decomp.py b/scipy/linalg/tests/test_decomp.py
--- a/scipy/linalg/tests/test_decomp.py
+++ b/scipy/linalg/tests/test_decomp.py
@@ -791,16 +791,32 @@ def test_wrong_inputs(self):
                       type=4)
         # Both value and index subsets requested
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
-                      subset_by_value=[1, 2], eigvals=[2, 4])
+                      subset_by_value=[1, 2], subset_by_index=[2, 4])
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'eigvals")
+            assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
+                          subset_by_value=[1, 2], eigvals=[2, 4])
         # Invalid upper index spec
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
-                      eigvals=[0, 4])
+                      subset_by_index=[0, 4])
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'eigvals")
+            assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
+                          eigvals=[0, 4])
         # Invalid lower index
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
-                      eigvals=[-2, 2])
+                      subset_by_index=[-2, 2])
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'eigvals")
+            assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
+                          eigvals=[-2, 2])
         # Invalid index spec #2
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
-                      eigvals=[2, 0])
+                      subset_by_index=[2, 0])
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'eigvals")
+            assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
+                          subset_by_index=[2, 0])
         # Invalid value spec
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
                       subset_by_value=[2, 0])
@@ -813,7 +829,11 @@ def test_wrong_inputs(self):
                       driver='evr', turbo=False)
         # Subset request from invalid driver
         assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
-                      driver='gvd', eigvals=[1, 2], turbo=False)
+                      driver='gvd', subset_by_index=[1, 2], turbo=False)
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "'eigh' keyword argument 'eigvals")
+            assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]),
+                          driver='gvd', subset_by_index=[1, 2], turbo=False)
 
     def test_nonpositive_b(self):
         assert_raises(LinAlgError, eigh, np.ones([3, 3]), np.ones([3, 3]))
@@ -869,36 +889,9 @@ def test_various_drivers_generalized(self, driver, type):
         else:
             assert_allclose(b @ a @ v - v * w, 0., atol=atol, rtol=0.)
 
-    # Old eigh tests kept for backwards compatibility
-    @pytest.mark.parametrize('eigvals', (None, (2, 4)))
-    @pytest.mark.parametrize('turbo', (True, False))
-    @pytest.mark.parametrize('lower', (True, False))
-    @pytest.mark.parametrize('overwrite', (True, False))
-    @pytest.mark.parametrize('dtype_', ('f', 'd', 'F', 'D'))
-    @pytest.mark.parametrize('dim', (6,))
-    def test_eigh(self, dim, dtype_, overwrite, lower, turbo, eigvals):
-        atol = 1e-11 if dtype_ in ('dD') else 1e-4
-        a = _random_hermitian_matrix(n=dim, dtype=dtype_)
-        w, z = eigh(a, overwrite_a=overwrite, lower=lower, eigvals=eigvals)
-        assert_dtype_equal(z.dtype, dtype_)
-        w = w.astype(dtype_)
-        diag_ = diag(z.T.conj() @ a @ z).real
-        assert_allclose(diag_, w, rtol=0., atol=atol)
-
-        a = _random_hermitian_matrix(n=dim, dtype=dtype_)
-        b = _random_hermitian_matrix(n=dim, dtype=dtype_, posdef=True)
-        w, z = eigh(a, b, overwrite_a=overwrite, lower=lower,
-                    overwrite_b=overwrite, turbo=turbo, eigvals=eigvals)
-        assert_dtype_equal(z.dtype, dtype_)
-        w = w.astype(dtype_)
-        diag1_ = diag(z.T.conj() @ a @ z).real
-        assert_allclose(diag1_, w, rtol=0., atol=atol)
-        diag2_ = diag(z.T.conj() @ b @ z).real
-        assert_allclose(diag2_, ones(diag2_.shape[0]), rtol=0., atol=atol)
-
     def test_eigvalsh_new_args(self):
         a = _random_hermitian_matrix(5)
-        w = eigvalsh(a, eigvals=[1, 2])
+        w = eigvalsh(a, subset_by_index=[1, 2])
         assert_equal(len(w), 2)
 
         w2 = eigvalsh(a, subset_by_index=[1, 2])
@@ -910,6 +903,35 @@ def test_eigvalsh_new_args(self):
         assert_equal(len(w3), 2)
         assert_allclose(w3, np.array([1.2, 1.3]))
 
+    @pytest.mark.parametrize("method", [eigh, eigvalsh])
+    def test_deprecation_warnings(self, method):
+        with pytest.warns(DeprecationWarning,
+                          match="Keyword argument 'turbo'"):
+            method(np.zeros((2, 2)), turbo=True)
+        with pytest.warns(DeprecationWarning,
+                          match="Keyword argument 'eigvals'"):
+            method(np.zeros((2, 2)), eigvals=[0, 1])
+
+    def test_deprecation_results(self):
+        a = _random_hermitian_matrix(3)
+        b = _random_hermitian_matrix(3, posdef=True)
+
+        # check turbo gives same result as driver='gvd'
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'turbo'")
+            w_dep, v_dep = eigh(a, b, turbo=True)
+        w, v = eigh(a, b, driver='gvd')
+        assert_allclose(w_dep, w)
+        assert_allclose(v_dep, v)
+
+        # check eigvals gives the same result as subset_by_index
+        with np.testing.suppress_warnings() as sup:
+            sup.filter(DeprecationWarning, "Keyword argument 'eigvals'")
+            w_dep, v_dep = eigh(a, eigvals=[0, 1])
+        w, v = eigh(a, subset_by_index=[0, 1])
+        assert_allclose(w_dep, w)
+        assert_allclose(v_dep, v)
+
 
 class TestLU:
     def setup_method(self):