Repository: scipy/scipy

Issue:
linalg.solve: Unclear error when using assume_a='her' with real matrix
My issue is about an obscure error that happens when using `scipy.linalg.solve(a,b, assume_a='her')` with a being a real matrix.
While raising an exception seems logical, the one that is raised makes it harder to diagnose the problem in user code.
(personally, when I first encountered the error, I went to check if the lapack/blas libraries were correctly installed/linked)

#### Reproducing code example:
<!-- 
If you place your code between the triple backticks below, 
it will be rendered as a code block. 
-->

```
import numpy as np
import scipy.linalg as spl
a = np.array([[0,1,2],[1,3,4],[2,4,6]])
a2 = np.asarray(a, dtype=complex)
b = np.array([7,8,9])

spl.solve(a,b, assume_a='sym')   # success: expected behavior
spl.solve(a2,b, assume_a='her')  # success: expected behavior
spl.solve(a2,b, assume_a='sym')  # success: expected behavior: this is a different assumption
spl.solve(a,b, assume_a='her')  # error, see below
```

#### Observed behavior (Error message) :
<!-- If any, paste the *full* error message inside a code block
as above (starting from line Traceback)
-->
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "*path*/python3.9/site-packages/scipy/linalg/basic.py", line 221, in solve
    hecon, hesv, hesv_lw = get_lapack_funcs(('hecon', 'hesv',
  File "*path*/python3.9/site-packages/scipy/linalg/blas.py", line 399, in getter
    value = func(names, arrays, dtype, ilp64)
  File "*path*/python3.9/site-packages/scipy/linalg/lapack.py", line 964, in get_lapack_funcs
    return _get_funcs(names, arrays, dtype,
  File "*path*/python3.9/site-packages/scipy/linalg/blas.py", line 358, in _get_funcs
    raise ValueError(
ValueError: LAPACK function dhecon could not be found
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  ...
```

#### "Expected" behavior(s)
The exception that is raised is (to me, at least) not clear enough about what the user's blunder was.
I see two (mutually exclusive) possibilities:
- `assume_a` is silently replaced by 'sym', because it is the same assumption as 'her' in the case of of a real matrix.
- an exception is raised earlier, clearly pointing out the dtype mismatch

I attached two (draft-state) patches, one for each possibility (at the end of this report)

(personally, I would go with the former solution, which feels more coherent with the documentation of the `spl.pinv` and `spl.pinvh` functions, although I do not know what would truly be preferred for the project)

#### Scipy/Numpy/Python version information:

scipy : master branch, d98e31ec32b293da156d2c41fc8c09eda345ecff   (same behavior at version 1.6.1).
numpy: `1.20.1`
python: `3.9.4 (releaselevel='final", serial=0)`
blas/lapack linkage info (in case it's relevant):

```
lapack_mkl_info:
    libraries = ['mkl_rt', 'pthread']
    library_dirs = ['/opt/intel/mkl/lib/intel64']
    define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
    include_dirs = ['/opt/intel/mkl', '/opt/intel/mkl/include', '/opt/intel/mkl/lib']
lapack_opt_info:
    libraries = ['mkl_rt', 'pthread']
    library_dirs = ['/opt/intel/mkl/lib/intel64']
    define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
    include_dirs = ['/opt/intel/mkl', '/opt/intel/mkl/include', '/opt/intel/mkl/lib']
blas_mkl_info:
    libraries = ['mkl_rt', 'pthread']
    library_dirs = ['/opt/intel/mkl/lib/intel64']
    define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
    include_dirs = ['/opt/intel/mkl', '/opt/intel/mkl/include', '/opt/intel/mkl/lib']
blas_opt_info:
    libraries = ['mkl_rt', 'pthread']
    library_dirs = ['/opt/intel/mkl/lib/intel64']
    define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
    include_dirs = ['/opt/intel/mkl', '/opt/intel/mkl/include', '/opt/intel/mkl/lib']
```

#### notes
- both patches were tested with the minimal example given
- I did not (yet) check other functions for similar problems

#### two (mutually exclusive) patches:
patch 1 (draft): silently correct the user's mistake
```
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
index d673d823c..db0a9ee93 100644
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -169,6 +169,10 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
     if assume_a not in ('gen', 'sym', 'her', 'pos'):
         raise ValueError('{} is not a recognized matrix structure'
                          ''.format(assume_a))
+
+    # for a real matrix, describe it as "symmetric", not "hermitian" (lapack doesn't know what to do with real hermitian matrices)
+    if assume_a == 'her' and not np.iscomplexobj(a1):
+        assume_a = 'sym'

     # Deprecate keyword "debug"
     if debug is not None:
```

patch 2 (draft): explicitly raise a meaningful exception
```
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
index d673d823c..6db5c4b1e 100644
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -218,6 +218,10 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
         rcond, info = gecon(lu, anorm, norm=norm)
     # Hermitian case 'hesv'
     elif assume_a == 'her':
+        if not np.iscomplexobj(a1):
+            raise ValueError('input a, containing real values, '
+                             'cannot be assumed to be hermitian ("her"). '
+                             'did you mean to assume it is symmetric ("sym")?')
         hecon, hesv, hesv_lw = get_lapack_funcs(('hecon', 'hesv',
                                                  'hesv_lwork'), (a1, b1))
         lwork = _compute_lwork(hesv_lw, n, lower)
```



Discussion:
I was hoping to get to this before it bites anyone but apparently not. Indeed patch 1 should be the behavior but if I can find enough time for finishing #12824 then probably nobody would bother specifying structure. A PR would be much appreciated if you have the time. 
sorry I didn't see your answer immediately.
I will send a PR after I've slept, if that's OK with you.

Resolving patch:
diff --git a/scipy/linalg/basic.py b/scipy/linalg/basic.py
--- a/scipy/linalg/basic.py
+++ b/scipy/linalg/basic.py
@@ -170,6 +170,11 @@ def solve(a, b, sym_pos=False, lower=False, overwrite_a=False,
         raise ValueError('{} is not a recognized matrix structure'
                          ''.format(assume_a))
 
+    # for a real matrix, describe it as "symmetric", not "hermitian"
+    # (lapack doesn't know what to do with real hermitian matrices)
+    if assume_a == 'her' and not np.iscomplexobj(a1):
+        assume_a = 'sym'
+
     # Deprecate keyword "debug"
     if debug is not None:
         warn('Use of the "debug" keyword is deprecated '
@@ -1234,7 +1239,7 @@ def lstsq(a, b, cond=None, overwrite_a=False, overwrite_b=False,
 
 
 def pinv(a, atol=None, rtol=None, return_rank=False, check_finite=True,
-          cond=None, rcond=None):
+         cond=None, rcond=None):
     """
     Compute the (Moore-Penrose) pseudo-inverse of a matrix.
 


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
@@ -508,38 +508,88 @@ def test_20Feb04_bug(self):
 
     def test_simple(self):
         a = [[1, 20], [-30, 4]]
-        for b in ([[1, 0], [0, 1]], [1, 0],
-                  [[2, 1], [-30, 4]]):
+        for b in ([[1, 0], [0, 1]],
+                  [1, 0],
+                  [[2, 1], [-30, 4]]
+                  ):
             x = solve(a, b)
             assert_array_almost_equal(dot(a, x), b)
 
-    def test_simple_sym(self):
+    def test_simple_complex(self):
+        a = array([[5, 2], [2j, 4]], 'D')
+        for b in ([1j, 0],
+                  [[1j, 1j], [0, 2]],
+                  [1, 0j],
+                  array([1, 0], 'D'),
+                  ):
+            x = solve(a, b)
+            assert_array_almost_equal(dot(a, x), b)
+
+    def test_simple_pos(self):
         a = [[2, 3], [3, 5]]
         for lower in [0, 1]:
-            for b in ([[1, 0], [0, 1]], [1, 0]):
-                x = solve(a, b, sym_pos=1, lower=lower)
+            for b in ([[1, 0], [0, 1]],
+                      [1, 0]
+                      ):
+                x = solve(a, b, assume_a='pos', lower=lower)
                 assert_array_almost_equal(dot(a, x), b)
 
-    def test_simple_sym_complex(self):
+    def test_simple_pos_complexb(self):
         a = [[5, 2], [2, 4]]
-        for b in [[1j, 0],
-                  [[1j, 1j],
-                   [0, 2]],
-                  ]:
-            x = solve(a, b, sym_pos=1)
+        for b in ([1j, 0],
+                  [[1j, 1j], [0, 2]],
+                  ):
+            x = solve(a, b, assume_a='pos')
             assert_array_almost_equal(dot(a, x), b)
 
-    def test_simple_complex(self):
-        a = array([[5, 2], [2j, 4]], 'D')
-        for b in [[1j, 0],
-                  [[1j, 1j],
-                   [0, 2]],
-                  [1, 0j],
-                  array([1, 0], 'D'),
-                  ]:
-            x = solve(a, b)
+    def test_simple_sym(self):
+        a = [[2, 3], [3, -5]]
+        for lower in [0, 1]:
+            for b in ([[1, 0], [0, 1]],
+                      [1, 0]
+                      ):
+                x = solve(a, b, assume_a='sym', lower=lower)
+                assert_array_almost_equal(dot(a, x), b)
+
+    def test_simple_sym_complexb(self):
+        a = [[5, 2], [2, -4]]
+        for b in ([1j, 0],
+                  [[1j, 1j],[0, 2]]
+                  ):
+            x = solve(a, b, assume_a='sym')
             assert_array_almost_equal(dot(a, x), b)
 
+    def test_simple_sym_complex(self):
+        a = [[5, 2+1j], [2+1j, -4]]
+        for b in ([1j, 0],
+                  [1, 0],
+                  [[1j, 1j], [0, 2]]
+                  ):
+            x = solve(a, b, assume_a='sym')
+            assert_array_almost_equal(dot(a, x), b)
+
+    def test_simple_her_actuallysym(self):
+        a = [[2, 3], [3, -5]]
+        for lower in [0, 1]:
+            for b in ([[1, 0], [0, 1]],
+                      [1, 0],
+                      [1j, 0],
+                      ):
+                x = solve(a, b, assume_a='her', lower=lower)
+                assert_array_almost_equal(dot(a, x), b)
+
+            
+    def test_simple_her(self):
+        a = [[5, 2+1j], [2-1j, -4]]
+        for b in ([1j, 0],
+                  [1, 0],
+                  [[1j, 1j], [0, 2]]
+                  ):
+            x = solve(a, b, assume_a='her')
+            assert_array_almost_equal(dot(a, x), b)
+
+            
+
     def test_nils_20Feb04(self):
         n = 2
         A = random([n, n])+random([n, n])*1j