Repository: scipy/scipy

Issue:
BUG: sqrtm dtype
### Describe your issue.

`sqrtm` does not pass through `np.float32` for single precision input.  I think the behavior changed starting at 1.6.

Reporting after a debugging session (the joys of mysteriously mixing dtypes in numerical code, 😂 ).

It's not a terrible bug or anything, but I'd like to know if this was intentional or will be changed.

I have seen this on multiple platforms now that I narrowed it down.

I really appreciate your software.
Thanks!

### Reproducing Code Example

```python
Python 3.7.11 (default, Jul 27 2021, 07:03:16) 
[Clang 10.0.0 ] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy as np
>>> import scipy.linalg as la
>>> la.sqrtm(np.zeros((10,10),dtype=np.float32)).dtype
dtype('float64')
>>>


# Old behavior:
Python 3.6.13 |Anaconda, Inc.| (default, Feb 23 2021, 12:58:59) 
[GCC Clang 10.0.0 ] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import numpy as np
>>> import scipy.linalg as la
>>> la.sqrtm(np.zeros((10,10),dtype=np.float32)).dtype
dtype(‘float32’)
```


### Error message

```shell
See code example for differing dtype.
```


### SciPy/NumPy/Python version information

1.6.1 1.21.2 sys.version_info(major=3, minor=7, micro=11, releaselevel='final', serial=0)


Discussion:
Thanks @garrettwrong. This was added in gh-12474. The underlying Cython code needs the types to be defined and we are casting reals to `np.float64` and complex to `np.complex128`.

https://github.com/scipy/scipy/blob/27dd4b0b3897c867aa0dc31d7cd5841a9194f973/scipy/linalg/_matfuncs_sqrtm.py#L56-L62

Is the type casting posing an issue?
Hi, now that I found the method, it is a tiny patch in my application. We want to support as many versions (of numpy/scipy) as reasonable, so even if changed upstream I would still need the cast in our code...

Mainly I am reporting in case this dtype change is not the expected behavior.  If it is expected, it would be nice to update the docs with the new return types.  I think it is pretty common to expect passthrough in the python scientific stack unless stated otherwise.

Thanks
Ah we are busted :) Thanks @garrettwrong for the report. This is something we briefly discussed way back when and unfortunately went float64-only to ease up the Cythonization effort. But you are right that we need some sort of a unification. As usual I am going waaaay behind of my promises such as #12838 thus the intention is most definitely there but commitments are not in par with them. Sorry about this. I really want to get to this as soon as our work over linalg.solve materializes. 

Resolving patch:
diff --git a/scipy/linalg/_matfuncs_sqrtm.py b/scipy/linalg/_matfuncs_sqrtm.py
--- a/scipy/linalg/_matfuncs_sqrtm.py
+++ b/scipy/linalg/_matfuncs_sqrtm.py
@@ -132,7 +132,11 @@ def sqrtm(A, disp=True, blocksize=64):
     Returns
     -------
     sqrtm : (N, N) ndarray
-        Value of the sqrt function at `A`
+        Value of the sqrt function at `A`. The dtype is float or complex.
+        The precision (data size) is determined based on the precision of
+        input `A`. When the dtype is float, the precision is same as `A`.
+        When the dtype is complex, the precition is double as `A`. The
+        precision might be cliped by each dtype precision range.
 
     errest : float
         (if disp == False)
@@ -159,6 +163,7 @@ def sqrtm(A, disp=True, blocksize=64):
            [ 1.,  4.]])
 
     """
+    byte_size = np.asarray(A).dtype.itemsize
     A = _asarray_validated(A, check_finite=True, as_inexact=True)
     if len(A.shape) != 2:
         raise ValueError("Non-matrix input to matrix function.")
@@ -176,6 +181,16 @@ def sqrtm(A, disp=True, blocksize=64):
         R = _sqrtm_triu(T, blocksize=blocksize)
         ZH = np.conjugate(Z).T
         X = Z.dot(R).dot(ZH)
+        if not np.iscomplexobj(X):
+            # float byte size range: f2 ~ f16
+            X = X.astype(f"f{np.clip(byte_size, 2, 16)}", copy=False)
+        else:
+            # complex byte size range: c8 ~ c32.
+            # c32(complex256) might not be supported in some environments.
+            if hasattr(np, 'complex256'):
+                X = X.astype(f"c{np.clip(byte_size*2, 8, 32)}", copy=False)
+            else:
+                X = X.astype(f"c{np.clip(byte_size*2, 8, 16)}", copy=False)
     except SqrtmError:
         failflag = True
         X = np.empty_like(A)


Test patch:
diff --git a/scipy/linalg/tests/test_matfuncs.py b/scipy/linalg/tests/test_matfuncs.py
--- a/scipy/linalg/tests/test_matfuncs.py
+++ b/scipy/linalg/tests/test_matfuncs.py
@@ -410,6 +410,72 @@ def test_gh7839(self):
         assert_allclose(np.dot(R, R), M, atol=1e-14)
         assert_allclose(sqrtm(M), R, atol=1e-14)
 
+    def test_data_size_preservation_uint_in_float_out(self):
+        M = np.zeros((10, 10), dtype=np.uint8)
+        # input bit size is 8, but minimum float bit size is 16
+        assert sqrtm(M).dtype == np.float16
+        M = np.zeros((10, 10), dtype=np.uint16)
+        assert sqrtm(M).dtype == np.float16
+        M = np.zeros((10, 10), dtype=np.uint32)
+        assert sqrtm(M).dtype == np.float32
+        M = np.zeros((10, 10), dtype=np.uint64)
+        assert sqrtm(M).dtype == np.float64
+
+    def test_data_size_preservation_int_in_float_out(self):
+        M = np.zeros((10, 10), dtype=np.int8)
+        # input bit size is 8, but minimum float bit size is 16
+        assert sqrtm(M).dtype == np.float16
+        M = np.zeros((10, 10), dtype=np.int16)
+        assert sqrtm(M).dtype == np.float16
+        M = np.zeros((10, 10), dtype=np.int32)
+        assert sqrtm(M).dtype == np.float32
+        M = np.zeros((10, 10), dtype=np.int64)
+        assert sqrtm(M).dtype == np.float64
+
+    def test_data_size_preservation_int_in_comp_out(self):
+        M = np.array([[2, 4], [0, -2]], dtype=np.int8)
+        # input bit size is 8, but minimum complex bit size is 64
+        assert sqrtm(M).dtype == np.complex64
+        M = np.array([[2, 4], [0, -2]], dtype=np.int16)
+        # input bit size is 16, but minimum complex bit size is 64
+        assert sqrtm(M).dtype == np.complex64
+        M = np.array([[2, 4], [0, -2]], dtype=np.int32)
+        assert sqrtm(M).dtype == np.complex64
+        M = np.array([[2, 4], [0, -2]], dtype=np.int64)
+        assert sqrtm(M).dtype == np.complex128
+
+    def test_data_size_preservation_float_in_float_out(self):
+        M = np.zeros((10, 10), dtype=np.float16)
+        assert sqrtm(M).dtype == np.float16
+        M = np.zeros((10, 10), dtype=np.float32)
+        assert sqrtm(M).dtype == np.float32
+        M = np.zeros((10, 10), dtype=np.float64)
+        assert sqrtm(M).dtype == np.float64
+        if hasattr(np, 'float128'):
+            M = np.zeros((10, 10), dtype=np.float128)
+            assert sqrtm(M).dtype == np.float128
+
+    def test_data_size_preservation_float_in_comp_out(self):
+        M = np.array([[2, 4], [0, -2]], dtype=np.float16)
+        # input bit size is 16, but minimum complex bit size is 64
+        assert sqrtm(M).dtype == np.complex64
+        M = np.array([[2, 4], [0, -2]], dtype=np.float32)
+        assert sqrtm(M).dtype == np.complex64
+        M = np.array([[2, 4], [0, -2]], dtype=np.float64)
+        assert sqrtm(M).dtype == np.complex128
+        if hasattr(np, 'float128') and hasattr(np, 'complex256'):
+            M = np.array([[2, 4], [0, -2]], dtype=np.float128)
+            assert sqrtm(M).dtype == np.complex256
+
+    def test_data_size_preservation_comp_in_comp_out(self):
+        M = np.array([[2j, 4], [0, -2j]], dtype=np.complex64)
+        assert sqrtm(M).dtype == np.complex128
+        if hasattr(np, 'complex256'):
+            M = np.array([[2j, 4], [0, -2j]], dtype=np.complex128)
+            assert sqrtm(M).dtype == np.complex256
+            M = np.array([[2j, 4], [0, -2j]], dtype=np.complex256)
+            assert sqrtm(M).dtype == np.complex256
+
 
 class TestFractionalMatrixPower:
     def test_round_trip_random_complex(self):