Repository: scipy/scipy

Issue:
BUG: scipy.sparse.linalg.gmres fails when provided x0 solves the system
### Describe your issue.

`scipy.sparse.linalg.gmres` fails to solve a linear system when the provided initial solution (`x0`) correctly solves the system. 
Intead, the solver prints several warning messages and returns a vector full of `NaN`s.

The solution without an `x0` works correctly.

### Reproducing Code Example

```python
import numpy as np
import scipy as sp

mat = sp.sparse.eye(2)
rhs = np.array([-1., -1.])

sol, info = sp.sparse.linalg.gmres(mat, rhs)

print(f"Sol: {sol}")
print(f"Info: {info}")


sol, info = sp.sparse.linalg.gmres(mat, rhs, x0=rhs)

print(f"Sol: {sol}")
print(f"Info: {info}")
```


### Error message

```shell
Sol: [-1. -1.]
Info: 0
/usr/lib/python3.11/site-packages/scipy/sparse/linalg/_isolve/iterative.py:770: RuntimeWarning: divide by zero encountered in scalar divide
  v[0, :] *= (1 / tmp)
/usr/lib/python3.11/site-packages/scipy/sparse/linalg/_isolve/iterative.py:770: RuntimeWarning: invalid value encountered in multiply
  v[0, :] *= (1 / tmp)
Sol: [nan nan]
Info: 20
```


### SciPy/NumPy/Python version and system information

```shell
Python version: sys.version_info(major=3, minor=11, micro=6, releaselevel='final', serial=0)
Numpy version: 1.26.3
Scipy version: 1.12.0
Build Dependencies:
  blas:
    detection method: pkgconfig
    found: true
    include directory: /usr/include
    lib directory: /usr/lib
    name: blas
    openblas configuration: unknown
    pc file directory: /usr/lib/pkgconfig
    version: 3.12.0
  lapack:
    detection method: pkgconfig
    found: true
    include directory: /usr/include
    lib directory: /usr/lib
    name: lapack
    openblas configuration: unknown
    pc file directory: /usr/lib/pkgconfig
    version: 3.12.0
  pybind11:
    detection method: pkgconfig
    include directory: /usr/lib/pkgconfig/../..//include
    name: pybind11
    version: 2.11.1
Compilers:
  c:
    args: -march=x86-64, -mtune=generic, -O2, -pipe, -fno-plt, -fexceptions, -Wp,-D_FORTIFY_SOURCE=2,
      -Wformat, -Werror=format-security, -fstack-clash-protection, -fcf-protection,
      -g, -ffile-prefix-map=/build/python-scipy/src=/usr/src/debug/python-scipy, -flto=auto
    commands: cc
    linker: ld.bfd
    linker args: -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now, -flto=auto, -march=x86-64,
      -mtune=generic, -O2, -pipe, -fno-plt, -fexceptions, -Wp,-D_FORTIFY_SOURCE=2,
      -Wformat, -Werror=format-security, -fstack-clash-protection, -fcf-protection,
      -g, -ffile-prefix-map=/build/python-scipy/src=/usr/src/debug/python-scipy, -flto=auto
    name: gcc
    version: 13.2.1
  c++:
    args: -march=x86-64, -mtune=generic, -O2, -pipe, -fno-plt, -fexceptions, -Wp,-D_FORTIFY_SOURCE=2,
      -Wformat, -Werror=format-security, -fstack-clash-protection, -fcf-protection,
      -Wp,-D_GLIBCXX_ASSERTIONS, -g, -ffile-prefix-map=/build/python-scipy/src=/usr/src/debug/python-scipy,
      -flto=auto
    commands: c++
    linker: ld.bfd
    linker args: -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now, -flto=auto, -march=x86-64,
      -mtune=generic, -O2, -pipe, -fno-plt, -fexceptions, -Wp,-D_FORTIFY_SOURCE=2,
      -Wformat, -Werror=format-security, -fstack-clash-protection, -fcf-protection,
      -Wp,-D_GLIBCXX_ASSERTIONS, -g, -ffile-prefix-map=/build/python-scipy/src=/usr/src/debug/python-scipy,
      -flto=auto
    name: gcc
    version: 13.2.1
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 3.0.8
  fortran:
    commands: gfortran
    linker: ld.bfd
    linker args: -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now, -flto=auto
    name: gcc
    version: 13.2.1
  pythran:
    include directory: ../../../../../usr/lib/python3.11/site-packages/pythran
    version: 0.15.0
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: linux
  cross-compiled: false
  host:
    cpu: x86_64
    endian: little
    family: x86_64
    system: linux
Python Information:
  path: /usr/bin/python
  version: '3.11'
```



Discussion:
It is missing the "if already solved, exit" clause, and then dividing with the remaining 0.0 residual. I'll add it. Thank you for reporting @chrhansk 

Resolving patch:
diff --git a/scipy/sparse/linalg/_isolve/iterative.py b/scipy/sparse/linalg/_isolve/iterative.py
--- a/scipy/sparse/linalg/_isolve/iterative.py
+++ b/scipy/sparse/linalg/_isolve/iterative.py
@@ -764,6 +764,8 @@ def gmres(A, b, x0=None, *, tol=_NoValue, restart=None, maxiter=None, M=None,
     for iteration in range(maxiter):
         if iteration == 0:
             r = b - matvec(x) if x.any() else b.copy()
+            if np.linalg.norm(r) < atol:  # Are we done?
+                return postprocess(x), 0
 
         v[0, :] = psolve(r)
         tmp = np.linalg.norm(v[0, :])


Test patch:
diff --git a/scipy/sparse/linalg/_isolve/tests/test_iterative.py b/scipy/sparse/linalg/_isolve/tests/test_iterative.py
--- a/scipy/sparse/linalg/_isolve/tests/test_iterative.py
+++ b/scipy/sparse/linalg/_isolve/tests/test_iterative.py
@@ -521,6 +521,17 @@ def test_x0_equals_Mb(case):
     assert norm(A @ x - b) <= rtol * norm(b)
 
 
+@pytest.mark.parametrize('solver', _SOLVERS)
+def test_x0_solves_problem_exactly(solver):
+    # See gh-19948
+    mat = np.eye(2)
+    rhs = np.array([-1., -1.])
+
+    sol, info = solver(mat, rhs, x0=rhs)
+    assert_allclose(sol, rhs)
+    assert info == 0
+
+
 # Specific tfqmr test
 @pytest.mark.parametrize('case', IterativeParams().cases)
 def test_show(case, capsys):