Repository: cupy/cupy

Issue:
cupy.linalg.qr() of complex matrix gives wrong answer
cupy.linalg.qr() works well for real matrix but returns wrong result for complex ones.  

```py
>>> import cupy as cp
>>> x = cp.random.randn(3,2)+cp.random.randn(3,2)*1.0j
>>> x
array([[-0.24066196+1.51554653j,  0.78608315-0.07083773j],
       [-0.22975259-0.27401681j, -0.94060383-1.28354323j],
       [ 0.28654721-0.34910445j, -2.5263385 -0.25618918j]])
>>> q,r=cp.linalg.qr(x)
>>> q
array([[-0.15510171+0.9767387j ,  0.78608315-0.07083773j],
       [-0.14807084-0.57906488j, -0.94060383-1.28354323j],
       [-0.21132318-0.78741754j, -2.5263385 -0.25618918j]])
>>> r
array([[ 1.55163969-0.84558675j,  0.78608315-0.07083773j],
       [ 0.        +0.j        , -0.94060383-1.28354323j]])
>>> xp = cp.einsum('ij,jk->ik',q,r)
>>> xp
array([[ 0.58525534+1.64669848j, -0.883049  -0.16355659j],
       [-0.71940218-0.77329331j, -0.9201633 +1.96990723j],
       [-0.99372727-1.04309622j,  1.82555733+2.87963121j]])
>>> cp.linalg.norm(xp-x)
array(6.7425088)
```

In addition, both q and r are different from that of numpy.linalg.qr() for the same matrix.

* Conditions
  - CuPy version: 5.4.0
  - OS/Platform: linux
  - CUDA version: 10.0.130
  - cuDNN/NCCL version (if applicable): 7.3.1




Discussion:
Current implementation of `cupy.linalg.qr` does not support complex, but it fails to emit error when complex array is given. It seems it's easy to support complex in `qr` as cusolver already supports it.

Resolving patch:
diff --git a/cupy/linalg/decomposition.py b/cupy/linalg/decomposition.py
--- a/cupy/linalg/decomposition.py
+++ b/cupy/linalg/decomposition.py
@@ -107,8 +107,8 @@ def qr(a, mode='reduced'):
         else:
             raise ValueError('Unrecognized mode \'{}\''.format(mode))
 
-    # Cast to float32 or float64
-    if a.dtype.char == 'f' or a.dtype.char == 'd':
+    # support float32, float64, complex64, and complex128
+    if a.dtype.char in 'fdFD':
         dtype = a.dtype.char
     else:
         dtype = numpy.find_common_type((a.dtype.char, 'f'), ()).char
@@ -118,21 +118,29 @@ def qr(a, mode='reduced'):
     mn = min(m, n)
     handle = device.get_cusolver_handle()
     dev_info = cupy.empty(1, dtype=numpy.int32)
-    # compute working space of geqrf and ormqr, and solve R
+    # compute working space of geqrf and orgqr, and solve R
     if dtype == 'f':
-        buffersize = cusolver.sgeqrf_bufferSize(handle, m, n, x.data.ptr, n)
-        workspace = cupy.empty(buffersize, dtype=numpy.float32)
-        tau = cupy.empty(mn, dtype=numpy.float32)
-        cusolver.sgeqrf(
-            handle, m, n, x.data.ptr, m,
-            tau.data.ptr, workspace.data.ptr, buffersize, dev_info.data.ptr)
-    else:  # dtype == 'd'
-        buffersize = cusolver.dgeqrf_bufferSize(handle, n, m, x.data.ptr, n)
-        workspace = cupy.empty(buffersize, dtype=numpy.float64)
-        tau = cupy.empty(mn, dtype=numpy.float64)
-        cusolver.dgeqrf(
-            handle, m, n, x.data.ptr, m,
-            tau.data.ptr, workspace.data.ptr, buffersize, dev_info.data.ptr)
+        geqrf_bufferSize = cusolver.sgeqrf_bufferSize
+        geqrf = cusolver.sgeqrf
+    elif dtype == 'd':
+        geqrf_bufferSize = cusolver.dgeqrf_bufferSize
+        geqrf = cusolver.dgeqrf
+    elif dtype == 'F':
+        geqrf_bufferSize = cusolver.cgeqrf_bufferSize
+        geqrf = cusolver.cgeqrf
+    elif dtype == 'D':
+        geqrf_bufferSize = cusolver.zgeqrf_bufferSize
+        geqrf = cusolver.zgeqrf
+    else:
+        msg = ('dtype must be float32, float64, complex64 or complex128'
+               ' (actual: {})'.format(a.dtype))
+        raise ValueError(msg)
+    buffersize = geqrf_bufferSize(handle, m, n, x.data.ptr, n)
+    workspace = cupy.empty(buffersize, dtype=dtype)
+    tau = cupy.empty(mn, dtype=dtype)
+    geqrf(handle, m, n, x.data.ptr, m,
+          tau.data.ptr, workspace.data.ptr, buffersize, dev_info.data.ptr)
+
     status = int(dev_info[0])
     if status < 0:
         raise linalg.LinAlgError(
@@ -149,6 +157,9 @@ def qr(a, mode='reduced'):
             # following code would be inappropriate, however, in this time
             # we explicitly convert them to float64 for compatibility.
             return x.astype(numpy.float64), tau.astype(numpy.float64)
+        elif a.dtype.char == 'F':
+            # The same applies to complex64
+            return x.astype(numpy.complex128), tau.astype(numpy.complex128)
         return x, tau
 
     if mode == 'complete' and m > n:
@@ -161,19 +172,22 @@ def qr(a, mode='reduced'):
 
     # solve Q
     if dtype == 'f':
-        buffersize = cusolver.sorgqr_bufferSize(
-            handle, m, mc, mn, q.data.ptr, m, tau.data.ptr)
-        workspace = cupy.empty(buffersize, dtype=numpy.float32)
-        cusolver.sorgqr(
-            handle, m, mc, mn, q.data.ptr, m, tau.data.ptr,
-            workspace.data.ptr, buffersize, dev_info.data.ptr)
-    else:
-        buffersize = cusolver.dorgqr_bufferSize(
-            handle, m, mc, mn, q.data.ptr, m, tau.data.ptr)
-        workspace = cupy.empty(buffersize, dtype=numpy.float64)
-        cusolver.dorgqr(
-            handle, m, mc, mn, q.data.ptr, m, tau.data.ptr,
-            workspace.data.ptr, buffersize, dev_info.data.ptr)
+        orgqr_bufferSize = cusolver.sorgqr_bufferSize
+        orgqr = cusolver.sorgqr
+    elif dtype == 'd':
+        orgqr_bufferSize = cusolver.dorgqr_bufferSize
+        orgqr = cusolver.dorgqr
+    elif dtype == 'F':
+        orgqr_bufferSize = cusolver.cungqr_bufferSize
+        orgqr = cusolver.cungqr
+    elif dtype == 'D':
+        orgqr_bufferSize = cusolver.zungqr_bufferSize
+        orgqr = cusolver.zungqr
+    buffersize = orgqr_bufferSize(handle, m, mc, mn, q.data.ptr, m,
+                                  tau.data.ptr)
+    workspace = cupy.empty(buffersize, dtype=dtype)
+    orgqr(handle, m, mc, mn, q.data.ptr, m, tau.data.ptr,
+          workspace.data.ptr, buffersize, dev_info.data.ptr)
 
     q = q[:mc].transpose()
     r = x[:, :mc].transpose()


Test patch:
diff --git a/tests/cupy_tests/linalg_tests/test_decomposition.py b/tests/cupy_tests/linalg_tests/test_decomposition.py
--- a/tests/cupy_tests/linalg_tests/test_decomposition.py
+++ b/tests/cupy_tests/linalg_tests/test_decomposition.py
@@ -72,7 +72,7 @@ def test_decomposition(self, dtype):
 @testing.gpu
 class TestQRDecomposition(unittest.TestCase):
 
-    @testing.for_float_dtypes(no_float16=True)
+    @testing.for_dtypes('fdFD')
     def check_mode(self, array, mode, dtype):
         a_cpu = numpy.asarray(array, dtype=dtype)
         a_gpu = cupy.asarray(array, dtype=dtype)