Repository: scipy/scipy

Issue:
ENH: Expose NoConvergence error class in the scipy.optimize namespace
### Is your feature request related to a problem? Please describe.

The various nonlinear solvers in scipy.optimize will raise a NoConvergence exception upon reaching ```maxiter``` if it is specified. Previously, this error could be caught via ```except scipy.optimize.nonlin.NoConvergence:```. Once the ```scipy.optimize.nonlin``` namespace is deprecated, this will no longer be possible. Hence, there will soon be no way to limit the iteration count in a non-fatal, handleable manner.

### Describe the solution you'd like.

Expose the NoConvergence exception in scipy.optimize by adding it to [\_\_all\_\_](https://github.com/scipy/scipy/blob/5d29b06e48ff5dd6a3d6b7fe2190e74ee540ff74/scipy/optimize/_nonlin.py#L16).

### Describe alternatives you've considered.

Expose the ```raise_exception``` kwarg in ```nonlin_solve```. This is likely not ideal since signaling that termination was due to nonconvergence seems to require either API compatibility breakage or inconsistency in output types, since at the moment the solvers return a ```ndarray``` directly.

### Additional context (e.g. screenshots, GIFs)

_No response_


Discussion:

Resolving patch:
diff --git a/scipy/optimize/__init__.py b/scipy/optimize/__init__.py
--- a/scipy/optimize/__init__.py
+++ b/scipy/optimize/__init__.py
@@ -385,6 +385,7 @@
    fsolve - Non-linear multivariable equation solver.
    broyden1 - Broyden's first method.
    broyden2 - Broyden's second method.
+   NoConvergence -  Exception raised when nonlinear solver does not converge.
 
 Large-scale nonlinear solvers:
 
diff --git a/scipy/optimize/_nonlin.py b/scipy/optimize/_nonlin.py
--- a/scipy/optimize/_nonlin.py
+++ b/scipy/optimize/_nonlin.py
@@ -16,7 +16,7 @@
 __all__ = [
     'broyden1', 'broyden2', 'anderson', 'linearmixing',
     'diagbroyden', 'excitingmixing', 'newton_krylov',
-    'BroydenFirst', 'KrylovJacobian', 'InverseJacobian']
+    'BroydenFirst', 'KrylovJacobian', 'InverseJacobian', 'NoConvergence']
 
 #------------------------------------------------------------------------------
 # Utility functions
@@ -24,6 +24,8 @@
 
 
 class NoConvergence(Exception):
+    """Exception raised when nonlinear solver fails to converge within the specified
+    `maxiter`."""
     pass
 
 
diff --git a/scipy/optimize/_root.py b/scipy/optimize/_root.py
--- a/scipy/optimize/_root.py
+++ b/scipy/optimize/_root.py
@@ -363,8 +363,7 @@ def _root_broyden1_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -441,8 +440,7 @@ def _root_broyden2_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -506,8 +504,7 @@ def _root_anderson_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -548,8 +545,7 @@ def _root_linearmixing_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, ``NoConvergence`` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -585,8 +581,7 @@ def _root_diagbroyden_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -622,8 +617,7 @@ def _root_excitingmixing_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional
@@ -662,8 +656,7 @@ def _root_krylov_doc():
     disp : bool, optional
         Print status to stdout on every iteration.
     maxiter : int, optional
-        Maximum number of iterations to make. If more are needed to
-        meet convergence, `NoConvergence` is raised.
+        Maximum number of iterations to make.
     ftol : float, optional
         Relative tolerance for the residual. If omitted, not used.
     fatol : float, optional


Test patch:
diff --git a/scipy/optimize/tests/test_nonlin.py b/scipy/optimize/tests/test_nonlin.py
--- a/scipy/optimize/tests/test_nonlin.py
+++ b/scipy/optimize/tests/test_nonlin.py
@@ -10,6 +10,7 @@
 from numpy import diag, dot
 from numpy.linalg import inv
 import numpy as np
+import scipy
 
 from .test_minpack import pressure_network
 
@@ -208,6 +209,13 @@ def test_problem_root(self):
                     continue
                 self._check_root(f, meth)
 
+    def test_no_convergence(self):
+        def wont_converge(x):
+            return 1e3 + x
+        
+        with pytest.raises(scipy.optimize.NoConvergence):
+            nonlin.newton_krylov(wont_converge, xin=[0], maxiter=1)
+
 
 class TestSecant:
     """Check that some Jacobian approximations satisfy the secant condition"""