Repository: scipy/scipy

Issue:
Adding more low-level LAPACK wrappers
I would like to be able to make use of the following LAPACK routines from `scipy.linalg.lapack` (which currently have Cython wrappers provided in `scipy.linalg.cython_lapack`): `geqrt`, `gemqrt`, `tpqrt` and `tpmqrt` for tiled QR factorisation. I suggest the following signatures (with work and info as necessary):

```
?geqrt(nb, a, overwrite_a) -> (a, t)
?gemqrt(side, trans, v, t, c, overwrite_c) -> (c)
?tpqrt(l, nb, a, b, overwrite_a, overwrite_b) -> (a, b, t)
?tpmqrt(side, trans, v, t, a, b, overwrite_a, overwrite_b) -> (a, b)
```

Since QR on F-contiguous is equivalent to LQ on C-contiguous, in principle the LQ factorisation equivalents `gelqt`, `gemlqt`, `tplqt`, `tpmlqt` could be called on C-contiguous arrays to avoid a transposition at the high level. However, these routines do not have the Cython wrappers, which I assume is due to their relatively recent introduction to LAPACK.

I am happy to work on a PR for getting at least the four tiled QR routines going, but I will need some guidance with what's involved to do so.


Discussion:
@AidanGG Thanks for taking the time. 

These routines came with Lapack 3.4 which we only recently assumed as the minimum version to support hence their absence.

For getting acquainted with the wrappers gou can check, say, #8965 on what is involved and which files are needed.

Please let me know if you need any more information. 
Hi @ilayn, that is very helpful. I will try my hand at writing a PR.

Resolving patch:
diff --git a/scipy/linalg/lapack.py b/scipy/linalg/lapack.py
--- a/scipy/linalg/lapack.py
+++ b/scipy/linalg/lapack.py
@@ -597,6 +597,26 @@
    cunmqr
    zunmqr
 
+   sgeqrt
+   dgeqrt
+   cgeqrt
+   zgeqrt
+
+   sgemqrt
+   dgemqrt
+   cgemqrt
+   zgemqrt
+
+   stpqrt
+   dtpqrt
+   ctpqrt
+   ztpqrt
+
+   stpmqrt
+   dtpmqrt
+   ctpmqrt
+   ztpmqrt
+
    cunmrz
    zunmrz
 


Test patch:
diff --git a/scipy/linalg/tests/test_lapack.py b/scipy/linalg/tests/test_lapack.py
--- a/scipy/linalg/tests/test_lapack.py
+++ b/scipy/linalg/tests/test_lapack.py
@@ -1276,3 +1276,154 @@ def test_syconv():
         ldu, ipiv, info = trf(A, lower=0)
         a, e, info = syconv(ldu, ipiv, lower=0)
         assert_allclose(triu(a, 1), triu(U[perm, :], 1), atol=tol, rtol=0.)
+
+class TestBlockedQR(object):
+    """
+    Tests for the blocked QR factorization, namely through geqrt, gemqrt, tpqrt
+    and tpmqr.
+    """
+
+    def test_geqrt_gemqrt(self):
+        seed(1234)
+        for ind, dtype in enumerate(DTYPES):
+            n = 20
+
+            if ind > 1:
+                A = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+            else:
+                A = (rand(n, n)).astype(dtype)
+
+            tol = 100*np.spacing(dtype(1.0).real)
+            geqrt, gemqrt = get_lapack_funcs(('geqrt', 'gemqrt'), dtype=dtype)
+
+            a, t, info = geqrt(n, A)
+            assert(info == 0)
+
+            # Extract elementary reflectors from lower triangle, adding the main
+            # diagonal of ones.
+            v = np.tril(a, -1) + np.eye(n, dtype=dtype)
+            # Generate the block Householder transform I - VTV^H
+            Q = np.eye(n, dtype=dtype) - v @ t @ v.T.conj()
+            R = np.triu(a)
+
+            # Test columns of Q are orthogonal
+            assert_allclose(Q.T.conj() @ Q, np.eye(n, dtype=dtype), atol=tol,
+                            rtol=0.)
+            assert_allclose(Q @ R, A, atol=tol, rtol=0.)
+
+            if ind > 1:
+                C = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+                transpose = 'C'
+            else:
+                C = (rand(n, n)).astype(dtype)
+                transpose = 'T'
+
+            for side in ('L', 'R'):
+                for trans in ('N', transpose):
+                    c, info = gemqrt(a, t, C, side=side, trans=trans)
+                    assert(info == 0)
+
+                    if trans == transpose:
+                        q = Q.T.conj()
+                    else:
+                        q = Q
+
+                    if side == 'L':
+                        qC = q @ C
+                    else:
+                        qC = C @ q
+
+                    assert_allclose(c, qC, atol=tol, rtol=0.)
+
+                    # Test default arguments
+                    if (side, trans) == ('L', 'N'):
+                        c_default, info = gemqrt(a, t, C)
+                        assert(info == 0)
+                        assert_equal(c_default, c)
+
+            # Test invalid side/trans
+            assert_raises(Exception, gemqrt, a, t, C, side='A')
+            assert_raises(Exception, gemqrt, a, t, C, trans='A')
+
+    def test_tpqrt_tpmqrt(self):
+        seed(1234)
+        for ind, dtype in enumerate(DTYPES):
+            n = 20
+
+            if ind > 1:
+                A = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+                B = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+            else:
+                A = (rand(n, n)).astype(dtype)
+                B = (rand(n, n)).astype(dtype)
+
+            tol = 100*np.spacing(dtype(1.0).real)
+            tpqrt, tpmqrt = get_lapack_funcs(('tpqrt', 'tpmqrt'), dtype=dtype)
+
+            # Test for the range of pentagonal B, from square to upper
+            # triangular
+            for l in (0, n // 2, n):
+                a, b, t, info = tpqrt(l, n, A, B)
+                assert(info == 0)
+
+                # Check that lower triangular portion of A has not been modified
+                assert_equal(np.tril(a, -1), np.tril(A, -1))
+                # Check that elements not part of the pentagonal portion of B
+                # have not been modified.
+                assert_equal(np.tril(b, l - n - 1), np.tril(B, l - n - 1))
+
+                # Extract pentagonal portion of B
+                B_pent, b_pent = np.triu(B, l - n), np.triu(b, l - n)
+
+                # Generate elementary reflectors
+                v = np.concatenate((np.eye(n, dtype=dtype), b_pent))
+                # Generate the block Householder transform I - VTV^H
+                Q = np.eye(2 * n, dtype=dtype) - v @ t @ v.T.conj()
+                R = np.concatenate((np.triu(a), np.zeros_like(a)))
+
+                # Test columns of Q are orthogonal
+                assert_allclose(Q.T.conj() @ Q, np.eye(2 * n, dtype=dtype),
+                                atol=tol, rtol=0.)
+                assert_allclose(Q @ R, np.concatenate((np.triu(A), B_pent)),
+                                atol=tol, rtol=0.)
+
+                if ind > 1:
+                    C = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+                    D = (rand(n, n) + rand(n, n)*1j).astype(dtype)
+                    transpose = 'C'
+                else:
+                    C = (rand(n, n)).astype(dtype)
+                    D = (rand(n, n)).astype(dtype)
+                    transpose = 'T'
+
+                for side in ('L', 'R'):
+                    for trans in ('N', transpose):
+                        c, d, info = tpmqrt(l, b, t, C, D, side=side,
+                                            trans=trans)
+                        assert(info == 0)
+
+                        if trans == transpose:
+                            q = Q.T.conj()
+                        else:
+                            q = Q
+
+                        if side == 'L':
+                            cd = np.concatenate((c, d), axis=0)
+                            CD = np.concatenate((C, D), axis=0)
+                            qCD = q @ CD
+                        else:
+                            cd = np.concatenate((c, d), axis=1)
+                            CD = np.concatenate((C, D), axis=1)
+                            qCD = CD @ q
+
+                        assert_allclose(cd, qCD, atol=tol, rtol=0.)
+
+                        if (side, trans) == ('L', 'N'):
+                            c_default, d_default, info = tpmqrt(l, b, t, C, D)
+                            assert(info == 0)
+                            assert_equal(c_default, c)
+                            assert_equal(d_default, d)
+
+                # Test invalid side/trans
+                assert_raises(Exception, tpmqrt, l, b, t, C, D, side='A')
+                assert_raises(Exception, tpmqrt, l, b, t, C, D, trans='A')