Repository: numba/numba

Issue:
Ensure empty arrays are handled in np.linalg
The functions in `np.linalg` need to handle empty arrays, and preferably in a consistent manner. Numpy does a variety of things including `LinAlgError`s due to non-squared-ness of the trailing dimensions or due to the emptiness of arrays, `ValueError`s due to reduction operations being invalid on empty arrays, and sometimes returns `0.0`.



Discussion:
Fatal Errors appear:

```
@njit
def nbinv(a):
    return np.linalg.inv(a)
zar = np.empty((0,0))

nbinv(zar)
```

gives

```
Fatal Python error: in Numba-compiled function
RuntimeError: LAPACK Error: Routine "xxgetrf". On input 4
```

this is expected, will need fixing on a case by case basis.


Resolving patch:
diff --git a/numba/targets/linalg.py b/numba/targets/linalg.py
--- a/numba/targets/linalg.py
+++ b/numba/targets/linalg.py
@@ -766,7 +766,7 @@ def _inv_err_handler(r):
 
 @register_jitable
 def _dummy_liveness_func(a):
-    """pass a list of variables to be preseved through dead code elimination"""
+    """pass a list of variables to be preserved through dead code elimination"""
     return a[0]
 
 
@@ -797,6 +797,9 @@ def inv_impl(a):
         else:
             acpy = np.asfortranarray(a)
 
+        if n == 0:
+            return acpy
+
         ipiv = np.empty(n, dtype=F_INT_nptype)
 
         r = numba_xxgetrf(kind, n, n, acpy.ctypes, n, ipiv.ctypes)
@@ -806,8 +809,6 @@ def inv_impl(a):
         _inv_err_handler(r)
 
         # help liveness analysis
-        #acpy.size
-        #ipiv.size
         _dummy_liveness_func([acpy.size, ipiv.size])
         return acpy
 
@@ -863,6 +864,10 @@ def cho_impl(a):
 
             # The output is allocated in C order
             out = a.copy()
+
+            if n == 0:
+                return out
+
             # Pass UP since xxpotrf() operates in F order
             # The semantics ensure this works fine
             # (out is really its Hermitian in F order, but UP instructs
@@ -922,6 +927,9 @@ def real_eig_impl(a):
             vl = np.empty((n, ldvl), dtype=a.dtype)
             vr = np.empty((n, ldvr), dtype=a.dtype)
 
+            if n == 0:
+                return (wr, vr.T)
+
             r = numba_ez_rgeev(kind,
                                JOBVL,
                                JOBVR,
@@ -952,11 +960,6 @@ def real_eig_impl(a):
 
             # put these in to help with liveness analysis,
             # `.ctypes` doesn't keep the vars alive
-            #acpy.size
-            #vl.size
-            #vr.size
-            #wr.size
-            #wi.size
             _dummy_liveness_func([acpy.size, vl.size, vr.size, wr.size, wi.size])
             return (wr, vr.T)
 
@@ -982,6 +985,9 @@ def cmplx_eig_impl(a):
             vl = np.empty((n, ldvl), dtype=a.dtype)
             vr = np.empty((n, ldvr), dtype=a.dtype)
 
+            if n == 0:
+                return (w, vr.T)
+
             r = numba_ez_cgeev(kind,
                                JOBVL,
                                JOBVR,
@@ -997,10 +1003,6 @@ def cmplx_eig_impl(a):
 
             # put these in to help with liveness analysis,
             # `.ctypes` doesn't keep the vars alive
-            #acpy.size
-            #vl.size
-            #vr.size
-            #w.size
             _dummy_liveness_func([acpy.size, vl.size, vr.size, w.size])
             return (w, vr.T)
 
@@ -1044,6 +1046,10 @@ def real_eigvals_impl(a):
             ldvl = 1
             ldvr = 1
             wr = np.empty(n, dtype=a.dtype)
+
+            if n == 0:
+                return wr
+
             wi = np.empty(n, dtype=a.dtype)
 
             # not referenced but need setting for MKL null check
@@ -1080,11 +1086,6 @@ def real_eigvals_impl(a):
 
             # put these in to help with liveness analysis,
             # `.ctypes` doesn't keep the vars alive
-            #acpy.size
-            #vl.size
-            #vr.size
-            #wr.size
-            #wi.size
             _dummy_liveness_func([acpy.size, vl.size, vr.size, wr.size, wi.size])
             return wr
 
@@ -1107,6 +1108,10 @@ def cmplx_eigvals_impl(a):
             ldvl = 1
             ldvr = 1
             w = np.empty(n, dtype=a.dtype)
+
+            if n == 0:
+                return w
+
             vl = np.empty((1), dtype=a.dtype)
             vr = np.empty((1), dtype=a.dtype)
 
@@ -1125,10 +1130,6 @@ def cmplx_eigvals_impl(a):
 
             # put these in to help with liveness analysis,
             # `.ctypes` doesn't keep the vars alive
-            #acpy.size
-            #vl.size
-            #vr.size
-            #w.size
             _dummy_liveness_func([acpy.size, vl.size, vr.size, w.size])
             return w
 
@@ -1172,6 +1173,9 @@ def eigh_impl(a):
 
             w = np.empty(n, dtype=w_dtype)
 
+            if n == 0:
+                return (w, acpy)
+
             r = numba_ez_xxxevd(kind,  # kind
                                 JOBZ,  # jobz
                                 UPLO,  # uplo
@@ -1183,8 +1187,6 @@ def eigh_impl(a):
             _handle_err_maybe_convergence_problem(r)
 
             # help liveness analysis
-            #acpy.size
-            #w.size
             _dummy_liveness_func([acpy.size, w.size])
             return (w, acpy)
 
@@ -1225,6 +1227,9 @@ def eigvalsh_impl(a):
 
             w = np.empty(n, dtype=w_dtype)
 
+            if n == 0:
+                return w
+
             r = numba_ez_xxxevd(kind,  # kind
                                 JOBZ,  # jobz
                                 UPLO,  # uplo
@@ -1236,8 +1241,6 @@ def eigvalsh_impl(a):
             _handle_err_maybe_convergence_problem(r)
 
             # help liveness analysis
-            #acpy.size
-            #w.size
             _dummy_liveness_func([acpy.size, w.size])
             return w
 
@@ -1266,6 +1269,9 @@ def svd_impl(a, full_matrices=1):
             n = a.shape[-1]
             m = a.shape[-2]
 
+            if n == 0 or m == 0:
+                raise np.linalg.LinAlgError("Arrays cannot be empty")
+
             _check_finite_matrix(a)
 
             if F_layout:
@@ -1305,10 +1311,6 @@ def svd_impl(a, full_matrices=1):
             _handle_err_maybe_convergence_problem(r)
 
             # help liveness analysis
-            #acpy.size
-            #vt.size
-            #u.size
-            #s.size
             _dummy_liveness_func([acpy.size, vt.size, u.size, s.size])
             return (u.T, s, vt.T)
 
@@ -1338,6 +1340,9 @@ def qr_impl(a):
         n = a.shape[-1]
         m = a.shape[-2]
 
+        if n == 0 or m == 0:
+            raise np.linalg.LinAlgError("Arrays cannot be empty")
+
         _check_finite_matrix(a)
 
         # copy A as it will be destroyed
@@ -1388,8 +1393,6 @@ def qr_impl(a):
         _handle_err_maybe_convergence_problem(ret)
 
         # help liveness analysis
-        #tau.size
-        #q.size
         _dummy_liveness_func([tau.size, q.size])
         return (q[:, :minmn], r)
 
@@ -1466,6 +1469,35 @@ def twoD_impl(a, b):
         return twoD_impl
 
 
+def _system_check_non_empty(a, b):
+    """
+    Check that AX=B style system input is not empty.
+    """
+    raise NotImplementedError
+
+
+@overload(_system_check_non_empty)
+def _system_check_non_empty_impl(a, b):
+    ndim = b.ndim
+    if ndim == 1:
+        def oneD_impl(a, b):
+            am = a.shape[-2]
+            an = a.shape[-1]
+            bm = b.shape[-1]
+            if am == 0 or bm == 0 or an == 0:
+                raise np.linalg.LinAlgError('Arrays cannot be empty')
+        return oneD_impl
+    else:
+        def twoD_impl(a, b):
+            am = a.shape[-2]
+            an = a.shape[-1]
+            bm = b.shape[-2]
+            bn = b.shape[-1]
+            if am == 0 or bm == 0 or an == 0 or bn == 0:
+                raise np.linalg.LinAlgError('Arrays cannot be empty')
+        return twoD_impl
+
+
 def _lstsq_residual(b, n, rhs):
     """
     Compute the residual from the 'b' scratch space.
@@ -1571,7 +1603,10 @@ def lstsq_impl(a, b, rcond=-1.0):
         _check_finite_matrix(a)
         _check_finite_matrix(b)
 
-        # check the systems is dimensionally valid
+        # check the system is not empty
+        _system_check_non_empty(a, b)
+
+        # check the systems are dimensionally valid
         _system_check_dimensionally_valid(a, b)
 
         minmn = min(m, n)
@@ -1622,10 +1657,6 @@ def lstsq_impl(a, b, rcond=-1.0):
         x = _lstsq_solution(b, bcpy, n)
 
         # help liveness analysis
-        #acpy.size
-        #bcpy.size
-        #s.size
-        #rank_ptr.size
         _dummy_liveness_func([acpy.size, bcpy.size, s.size, rank_ptr.size])
         return (x, res, rank, s[:minmn])
 
@@ -1691,6 +1722,9 @@ def solve_impl(a, b):
 
         # b is overwritten on exit with the solution, copy allocate
         bcpy = np.empty((nrhs, n), dtype=np_dt).T
+        if n == 0:
+            return _solve_compute_return(b, bcpy)
+
         # specialised copy in due to b being 1 or 2D
         _system_copy_in_b(bcpy, b, nrhs)
 
@@ -1710,9 +1744,6 @@ def solve_impl(a, b):
         _inv_err_handler(r)
 
         # help liveness analysis
-        #acpy.size
-        #bcpy.size
-        #ipiv.size
         _dummy_liveness_func([acpy.size, bcpy.size, ipiv.size])
         return _solve_compute_return(b, bcpy)
 
@@ -1796,6 +1827,9 @@ def pinv_impl(a, rcond=1.e-15):
         else:
             acpy = np.asfortranarray(a)
 
+        if m == 0 or n == 0:
+            return acpy.T.ravel().reshape(a.shape).T
+
         minmn = min(m, n)
 
         u = np.empty((minmn, m), dtype=a.dtype)
@@ -1935,12 +1969,18 @@ def slogdet_impl(a):
 
     diag_walker = _get_slogdet_diag_walker(a)
 
+    ONE = a.dtype(1)
+    ZERO = getattr(a.dtype, "underlying_float", a.dtype)(0)
+
     def slogdet_impl(a):
         n = a.shape[-1]
         if a.shape[-2] != n:
             msg = "Last 2 dimensions of the array must be square."
             raise np.linalg.LinAlgError(msg)
 
+        if n == 0:
+            return (ONE, ZERO)
+
         _check_finite_matrix(a)
 
         if F_layout:
@@ -1973,7 +2013,6 @@ def slogdet_impl(a):
             sgn = -1
 
         # help liveness analysis
-        #ipiv.size
         _dummy_liveness_func([ipiv.size])
         return diag_walker(n, acpy, sgn)
 
@@ -2017,7 +2056,7 @@ def _compute_singular_values_impl(a):
     np_ret_type = np_support.as_dtype(nb_ret_type)
     np_dtype = np_support.as_dtype(a.dtype)
 
-    # This are not referenced in the computation but must be set
+    # These are not referenced in the computation but must be set
     # for MKL.
     u = np.empty((1, 1), dtype=np_dtype)
     vt = np.empty((1, 1), dtype=np_dtype)
@@ -2032,9 +2071,12 @@ def sv_function(a):
         # call LAPACK to shortcut doing the "reconstruct
         # singular vectors from reflectors" step and just
         # get back the singular values.
-        _check_finite_matrix(a)
         n = a.shape[-1]
         m = a.shape[-2]
+        if m == 0 or n == 0:
+            raise np.linalg.LinAlgError('Arrays cannot be empty')
+        _check_finite_matrix(a)
+
         ldu = m
         minmn = min(m, n)
 
@@ -2042,8 +2084,6 @@ def sv_function(a):
         ucol = 1
         ldvt = 1
 
-        _check_finite_matrix(a)
-
         if F_layout:
             acpy = np.copy(a)
         else:
@@ -2070,10 +2110,6 @@ def sv_function(a):
         _handle_err_maybe_convergence_problem(r)
 
         # help liveness analysis
-        #acpy.size
-        #vt.size
-        #u.size
-        #s.size
         _dummy_liveness_func([acpy.size, vt.size, u.size, s.size])
         return s
 
@@ -2483,6 +2519,14 @@ def matrix_power_impl(a, n):
                 A[k, k] = 1.
             return A
 
+        am, an = a.shape[-1], a.shape[-2]
+        if am != an:
+            raise ValueError('input must be a square array')
+
+        # empty, return a copy
+        if am == 0:
+            return a.copy()
+
         # note: to be consistent over contiguousness, C order is
         # returned as that is what dot() produces and the most common
         # paths through matrix_power will involve that. Therefore


Test patch:
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -499,6 +499,20 @@ def specific_sample_matrix(
 
         return Q
 
+    def shape_with_0_input(self, *args):
+        """
+        returns True if an input argument has a dimension that is zero
+        and Numpy version is < 1.13, else False. This is due to behaviour
+        changes in handling dimension zero arrays:
+        https://github.com/numpy/numpy/issues/10573
+        """
+        if numpy_version < (1, 13):
+            for x in args:
+                if isinstance(x, np.ndarray):
+                    if 0 in x.shape:
+                        return True
+        return False
+
     def assert_error(self, cfunc, args, msg, err=ValueError):
         with self.assertRaises(err) as raises:
             cfunc(*args)
@@ -585,6 +599,10 @@ def assert_invalid_norm_kind(self, cfunc, args):
         msg = "Invalid norm order for matrices."
         self.assert_error(cfunc, args, msg, ValueError)
 
+    def assert_raise_on_empty(self, cfunc, args):
+        msg = 'Arrays cannot be empty'
+        self.assert_error(cfunc, args, msg, np.linalg.LinAlgError)
+
 
 class TestTestLinalgBase(TestCase):
     """
@@ -733,6 +751,9 @@ def check(a, **kwargs):
             a = self.specific_sample_matrix((n, n), dtype, order)
             check(a)
 
+        # 0 dimensioned matrix
+        check(np.empty((0, 0)))
+
         # Non square matrix
         self.assert_non_square(cfunc, (np.ones((2, 3)),))
 
@@ -777,6 +798,11 @@ def test_linalg_cholesky(self):
         cfunc = jit(nopython=True)(cholesky_matrix)
 
         def check(a):
+            if self.shape_with_0_input(a):
+                # has shape with 0 on input, numpy will fail,
+                # just make sure Numba runs without error
+                cfunc(a)
+                return
             expected = cholesky_matrix(a)
             got = cfunc(a)
             use_reconstruction = False
@@ -810,6 +836,9 @@ def check(a):
             a = self.sample_matrix(n, dtype, order)
             check(a)
 
+        # 0 dimensioned matrix
+        check(np.empty((0, 0)))
+
         rn = "cholesky"
         # Non square matrices
         self.assert_non_square(cfunc, (np.ones((2, 3), dtype=np.float64),))
@@ -856,6 +885,11 @@ def checker_for_linalg_eig(
         cfunc = jit(nopython=True)(func)
 
         def check(a):
+            if self.shape_with_0_input(a):
+                # has shape with 0 on input, numpy will fail,
+                # just make sure Numba runs without error
+                cfunc(a)
+                return
             expected = func(a)
             got = cfunc(a)
             # check that the returned tuple is same length
@@ -954,6 +988,10 @@ def check(a):
 
         # Test both a real and complex type as the impls are different
         for ty in [np.float32, np.complex64]:
+
+            # 0 dimensioned matrix
+            check(np.empty((0, 0), dtype=ty))
+
             # Non square matrices
             self.assert_non_square(cfunc, (np.ones((2, 3), dtype=ty),))
 
@@ -1105,6 +1143,10 @@ def check(a, **kwargs):
         self.assert_no_nan_or_inf(cfunc,
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
+        # empty
+        for sz in [(0, 1), (1, 0), (0, 0)]:
+            args = (np.empty(sz), True)
+            self.assert_raise_on_empty(cfunc, args)
 
 
 class TestLinalgQr(TestLinalgBase):
@@ -1197,6 +1239,10 @@ def check(a, **kwargs):
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
 
+        # empty
+        for sz in [(0, 1), (1, 0), (0, 0)]:
+            self.assert_raise_on_empty(cfunc, (np.empty(sz),))
+
 
 class TestLinalgSystems(TestLinalgBase):
     """
@@ -1408,6 +1454,18 @@ def inner_test_loop_fn(A, dt, **kwargs):
                 inner_test_loop_fn(A, dt,
                                    rcond=approx_half_rank_rcond)
 
+        # check empty arrays
+        empties = [
+        [(0, 1), (1,)], # empty A, valid b
+        [(1, 0), (1,)], # empty A, valid b
+        [(1, 1), (0,)], # valid A, empty 1D b 
+        [(1, 1), (1, 0)],  # valid A, empty 2D b 
+        ]
+
+        for A, b in empties:
+            args = (np.empty(A), np.empty(b))
+            self.assert_raise_on_empty(cfunc, args)
+
         # Test input validation
         ok = np.array([[1., 2.], [3., 4.]], dtype=np.float64)
 
@@ -1525,6 +1583,9 @@ def check(a, b, **kwargs):
                 tmp = B[:, 0].copy(order=b_order)
                 check(A, tmp)
 
+        # check empty
+        cfunc(np.empty((0, 0)), np.empty((0,)))
+
         # Test input validation
         ok = np.array([[1., 0.], [0., 1.]], dtype=np.float64)
 
@@ -1584,6 +1645,11 @@ def test_linalg_pinv(self):
         cfunc = jit(nopython=True)(pinv_matrix)
 
         def check(a, **kwargs):
+            if self.shape_with_0_input(a):
+                # has shape with 0 on input, numpy will fail,
+                # just make sure Numba runs without error
+                cfunc(a, **kwargs)
+                return
             expected = pinv_matrix(a, **kwargs)
             got = cfunc(a, **kwargs)
 
@@ -1673,6 +1739,10 @@ def check(a, **kwargs):
                 approx_half_rank_rcond = minmn * rcond
                 check(a, rcond=approx_half_rank_rcond)
 
+        # check empty
+        for sz in [(0, 1), (1, 0)]:
+            check(np.empty(sz))
+
         rn = "pinv"
 
         # Wrong dtype
@@ -1698,6 +1768,11 @@ class TestLinalgDetAndSlogdet(TestLinalgBase):
     """
 
     def check_det(self, cfunc, a, **kwargs):
+        if self.shape_with_0_input(a):
+            # has shape with 0 on input, numpy will fail,
+            # just make sure Numba runs without error
+            cfunc(a, **kwargs)
+            return
         expected = det_matrix(a, **kwargs)
         got = cfunc(a, **kwargs)
 
@@ -1711,6 +1786,11 @@ def check_det(self, cfunc, a, **kwargs):
             cfunc(a, **kwargs)
 
     def check_slogdet(self, cfunc, a, **kwargs):
+        if self.shape_with_0_input(a):
+            # has shape with 0 on input, numpy will fail,
+            # just make sure Numba runs without error
+            cfunc(a, **kwargs)
+            return
         expected = slogdet_matrix(a, **kwargs)
         got = cfunc(a, **kwargs)
 
@@ -1762,6 +1842,9 @@ def do_test(self, rn, check, cfunc):
             a = np.zeros((3, 3), dtype=dtype)
             check(cfunc, a)
 
+        # check empty
+        check(cfunc, np.empty((0, 0)))
+
         # Wrong dtype
         self.assert_wrong_dtype(rn, cfunc,
                                 (np.ones((2, 2), dtype=np.int32),))
@@ -1833,13 +1916,6 @@ def check(a, **kwargs):
             a = self.sample_vector(10, dtype)[::3]
             check(a, ord=nrm_type)
 
-        # check that numba returns zero for empty arrays. Numpy returns zero
-        # for most norm types and raises ValueError for +/-np.inf.
-        for dtype, nrm_type, order in \
-                product(self.dtypes, nrm_types, 'FC'):
-            a = np.array([], dtype=dtype, order=order)
-            self.assertEqual(cfunc(a, nrm_type), 0.0)
-
         # Check 2D inputs:
         # test: column vector, tall, wide, square, row vector
         # prime sizes
@@ -1869,10 +1945,14 @@ def check(a, **kwargs):
             check(a[1, 4::3], ord=nrm_type)
 
         # check that numba returns zero for empty arrays. Numpy returns zero
-        # for some norm types and raises a variety of errors for others.
+        # for most norm types and raises ValueError for +/-np.inf.
+        # there is not a great deal of consistency in Numpy's response so
+        # it is not being emulated in Numba
         for dtype, nrm_type, order in \
                 product(self.dtypes, nrm_types, 'FC'):
-            a = np.array([[]], dtype=dtype, order=order)
+            a = np.empty((0,), dtype=dtype, order=order)
+            self.assertEqual(cfunc(a, nrm_type), 0.0)
+            a = np.empty((0, 0), dtype=dtype, order=order)
             self.assertEqual(cfunc(a, nrm_type), 0.0)
 
         rn = "norm"
@@ -1942,6 +2022,10 @@ def check(a, **kwargs):
             a = self.specific_sample_matrix(size, dtype, order)
             check(a)
 
+        # empty
+        for sz in [(0, 1), (1, 0), (0, 0)]:
+            self.assert_raise_on_empty(cfunc, (np.empty(sz),))
+
         # try an ill-conditioned system with 2-norm, make sure np raises an
         # overflow warning as the result is `+inf` and that the result from
         # numba matches.
@@ -2058,6 +2142,11 @@ def check(a, **kwargs):
             self.assertEqual(cfunc(a), 1)
             check(a)
 
+        # empty
+        for sz in [(0, 1), (1, 0), (0, 0)]:
+            for tol in [None, 1e-13]:
+                self.assert_raise_on_empty(cfunc, (np.empty(sz), tol))
+
         rn = "matrix_rank"
 
         # Wrong dtype
@@ -2113,6 +2202,8 @@ def check(a, pwr):
                 product(sizes, powers, self.dtypes, 'FC'):
             a = self.specific_sample_matrix(size, dtype, order)
             check(a, pwr)
+            a = np.empty((0, 0), dtype=dtype, order=order)
+            check(a, pwr)
 
         rn = "matrix_power"
 
@@ -2124,6 +2215,11 @@ def check(a, pwr):
         self.assert_wrong_dtype(rn, cfunc,
                                 (np.ones((2, 2), dtype=np.int32), 1))
 
+        # non square system
+        args = (np.ones((3, 5)), 1)
+        msg = 'input must be a square array'
+        self.assert_error(cfunc, args, msg)
+
         # Dimension issue
         self.assert_wrong_dimensions(rn, cfunc,
                                      (np.ones(10, dtype=np.float64), 1))
@@ -2185,6 +2281,10 @@ def check(a, **kwargs):
             check(a, offset=offset)
             if offset == 0:
                 check(a)
+            a = np.empty((0, 0), dtype=dtype, order=order)
+            check(a, offset=offset)
+            if offset == 0:
+                check(a)
 
         rn = "trace"