Repository: numba/numba

Issue:
np.linalg.pinv Unintended mutation of input
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

Hello all, long time!

The following script (apologies - rather contrived) is an example of a function which does not error when compiled but manages to mutate one of its inputs (not intentionally).

It you call the compiled function with the same inputs many times, you find the mutated input switches from one set of values to another; the 'other' values are very close to the original input values.

Per the note, simply copying the input makes all the assertions pass, as you'd expect.

Just wasn't sure why the input gets modified / why the copy is needed - this might be more of a question than an issue, but it took a while to track down so thought it might be worth posting in case other users find it of interest.

Cheers

Rob


```python
import numpy as np
import pandas as pd
from numba import njit


@njit
def some_fn(alp, v):
    rows, cols = alp.shape
    p = np.empty_like(alp)

    for i in range(rows):
        A_ = v[i]
        b_ = alp[i]

        res = np.linalg.pinv(A_) @ b_

        p[i] = res[:cols]

    return p


if __name__ == '__main__':

    v_data = pd.DataFrame(
        [
            [0.55, -0.21, 0, 0.07],
            [-0.21, 0.48, 0, 0.10],
            [0, 0, 0.31, -0.15],
            [0.07, 0.10, -0.15, 0.51]
        ]
    )

    v_data_original = v_data.copy()

    a_data = pd.DataFrame([[0.1, 0.3, -0.1, 0.05]])

    v = v_data.to_numpy().reshape((1, 4, 4))  # <- using this, the cfunc assertions all fail

    # v = v_data.to_numpy(copy=True).reshape((1, 4, 4))  # <- using this, the cfunc assertions all pass

    a = a_data.values

    # -------- pyfunc

    some_fn.py_func(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # pass

    some_fn.py_func(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # pass

    # -------- cfunc

    some_fn(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # fails (case 1)

    some_fn(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # fails (case 2)

    some_fn(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # fails (case 1)

    some_fn(a, v)
    pd.testing.assert_frame_equal(v_data_original, v_data)  # fails (case 2)
```

* case 1 values are quite different to the original input
* case 2 values are quite similar to the original input





Discussion:

Patch:
diff --git a/numba/np/linalg.py b/numba/np/linalg.py
index d206d8f17..01b6202c8 100644
--- a/numba/np/linalg.py
+++ b/numba/np/linalg.py
@@ -782,6 +782,38 @@ def _check_homogeneous_types(func_name, *types):
             raise TypingError(msg, highlighting=False)
 
 
+def _copy_to_fortran_order():
+    pass
+
+
+@overload(_copy_to_fortran_order)
+def ol_copy_to_fortran_order(a):
+    # This function copies the array 'a' into a new array with fortran order.
+    # This exists because the copy routines don't take order flags yet.
+    F_layout = a.layout == 'F'
+    A_layout = a.layout == 'A'
+    def impl(a):
+        if F_layout:
+            # it's F ordered at compile time, just copy
+            acpy = np.copy(a)
+        elif A_layout:
+            # decide based on runtime value
+            flag_f = a.flags.f_contiguous
+            if flag_f:
+                # it's already F ordered, so copy but in a round about way to
+                # ensure that the copy is also F ordered
+                acpy = np.copy(a.T).T
+            else:
+                # it's something else ordered, so let asfortranarray deal with
+                # copying and making it fortran ordered
+                acpy = np.asfortranarray(a)
+        else:
+            # it's C ordered at compile time, asfortranarray it.
+            acpy = np.asfortranarray(a)
+        return acpy
+    return impl
+
+
 @register_jitable
 def _inv_err_handler(r):
     if r != 0:
@@ -810,8 +842,6 @@ def inv_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "inv"))
 
-    F_layout = a.layout == 'F'
-
     def inv_impl(a):
         n = a.shape[-1]
         if a.shape[-2] != n:
@@ -820,10 +850,7 @@ def inv_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         if n == 0:
             return acpy
@@ -929,8 +956,6 @@ def eig_impl(a):
     JOBVL = ord('N')
     JOBVR = ord('V')
 
-    F_layout = a.layout == 'F'
-
     def real_eig_impl(a):
         """
         eig() implementation for real arrays.
@@ -942,10 +967,7 @@ def eig_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = n
@@ -1001,10 +1023,7 @@ def eig_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = n
@@ -1052,8 +1071,6 @@ def eigvals_impl(a):
     JOBVL = ord('N')
     JOBVR = ord('N')
 
-    F_layout = a.layout == 'F'
-
     def real_eigvals_impl(a):
         """
         eigvals() implementation for real arrays.
@@ -1065,10 +1082,7 @@ def eigvals_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = 1
@@ -1127,10 +1141,7 @@ def eigvals_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldvl = 1
         ldvr = 1
@@ -1171,8 +1182,6 @@ def eigh_impl(a):
 
     _check_linalg_matrix(a, "eigh")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     w_type = getattr(a.dtype, "underlying_float", a.dtype)
     w_dtype = np_support.as_dtype(w_type)
@@ -1193,10 +1202,7 @@ def eigh_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         w = np.empty(n, dtype=w_dtype)
 
@@ -1225,8 +1231,6 @@ def eigvalsh_impl(a):
 
     _check_linalg_matrix(a, "eigvalsh")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     w_type = getattr(a.dtype, "underlying_float", a.dtype)
     w_dtype = np_support.as_dtype(w_type)
@@ -1247,10 +1251,7 @@ def eigvalsh_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         w = np.empty(n, dtype=w_dtype)
 
@@ -1279,8 +1280,6 @@ def svd_impl(a, full_matrices=1):
 
     _check_linalg_matrix(a, "svd")
 
-    F_layout = a.layout == 'F'
-
     # convert typing floats to numpy floats for use in the impl
     s_type = getattr(a.dtype, "underlying_float", a.dtype)
     s_dtype = np_support.as_dtype(s_type)
@@ -1301,10 +1300,7 @@ def svd_impl(a, full_matrices=1):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ldu = m
         minmn = min(m, n)
@@ -1361,8 +1357,6 @@ def qr_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "qr"))
 
-    F_layout = a.layout == 'F'
-
     def qr_impl(a):
         n = a.shape[-1]
         m = a.shape[-2]
@@ -1373,10 +1367,7 @@ def qr_impl(a):
         _check_finite_matrix(a)
 
         # copy A as it will be destroyed
-        if F_layout:
-            q = np.copy(a)
-        else:
-            q = np.asfortranarray(a)
+        q = _copy_to_fortran_order(a)
 
         lda = m
 
@@ -1598,9 +1589,6 @@ def lstsq_impl(a, b, rcond=-1.0):
     # B can be 1D or 2D.
     _check_linalg_1_or_2d_matrix(b, "lstsq")
 
-    a_F_layout = a.layout == 'F'
-    b_F_layout = b.layout == 'F'
-
     _check_homogeneous_types("lstsq", a, b)
 
     np_dt = np_support.as_dtype(a.dtype)
@@ -1640,10 +1628,7 @@ def lstsq_impl(a, b, rcond=-1.0):
         maxmn = max(m, n)
 
         # a is destroyed on exit, copy it
-        if a_F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # b is overwritten on exit with the solution, copy allocate
         bcpy = np.empty((nrhs, maxmn), dtype=np_dt).T
@@ -1717,9 +1702,6 @@ def solve_impl(a, b):
     _check_linalg_matrix(a, "solve")
     _check_linalg_1_or_2d_matrix(b, "solve")
 
-    a_F_layout = a.layout == 'F'
-    b_F_layout = b.layout == 'F'
-
     _check_homogeneous_types("solve", a, b)
 
     np_dt = np_support.as_dtype(a.dtype)
@@ -1742,10 +1724,7 @@ def solve_impl(a, b):
         _system_check_dimensionally_valid(a, b)
 
         # a is destroyed on exit, copy it
-        if a_F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # b is overwritten on exit with the solution, copy allocate
         bcpy = np.empty((nrhs, n), dtype=np_dt).T
@@ -1791,8 +1770,6 @@ def pinv_impl(a, rcond=1.e-15):
 
     numba_xxgemm = _BLAS().numba_xxgemm(a.dtype)
 
-    F_layout = a.layout == 'F'
-
     kind = ord(get_blas_kind(a.dtype, "pinv"))
     JOB = ord('S')
 
@@ -1849,10 +1826,7 @@ def pinv_impl(a, rcond=1.e-15):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         if m == 0 or n == 0:
             return acpy.T.ravel().reshape(a.shape).T
@@ -1992,8 +1966,6 @@ def slogdet_impl(a):
 
     kind = ord(get_blas_kind(a.dtype, "slogdet"))
 
-    F_layout = a.layout == 'F'
-
     diag_walker = _get_slogdet_diag_walker(a)
 
     ONE = a.dtype(1)
@@ -2010,10 +1982,7 @@ def slogdet_impl(a):
 
         _check_finite_matrix(a)
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         ipiv = np.empty(n, dtype=F_INT_nptype)
 
@@ -2088,8 +2057,6 @@ def _compute_singular_values_impl(a):
     u = np.empty((1, 1), dtype=np_dtype)
     vt = np.empty((1, 1), dtype=np_dtype)
 
-    F_layout = a.layout == 'F'
-
     def sv_function(a):
         """
         Computes singular values.
@@ -2111,10 +2078,7 @@ def _compute_singular_values_impl(a):
         ucol = 1
         ldvt = 1
 
-        if F_layout:
-            acpy = np.copy(a)
-        else:
-            acpy = np.asfortranarray(a)
+        acpy = _copy_to_fortran_order(a)
 
         # u and vt are not referenced however need to be
         # allocated (as done above) for MKL as it


Test patch:
diff --git a/numba/tests/test_linalg.py b/numba/tests/test_linalg.py
index a1198e13f..61cc768e3 100644
--- a/numba/tests/test_linalg.py
+++ b/numba/tests/test_linalg.py
@@ -8,7 +8,7 @@ import platform
 
 import numpy as np
 
-from numba import jit
+from numba import jit, njit, typeof
 from numba.core import errors
 from numba.tests.support import (TestCase, tag, needs_lapack, needs_blas,
                                  _is_armv7l, skip_ppc64le_issue4026)
@@ -772,6 +772,30 @@ class TestLinalgInv(TestLinalgBase):
         # Singular matrix
         self.assert_raise_on_singular(cfunc, (np.zeros((2, 2)),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def ainv(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.inv(X)
+
+        expected = ainv.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = ainv(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 @skip_ppc64le_issue4026
 class TestLinalgCholesky(TestLinalgBase):
@@ -877,17 +901,12 @@ class TestLinalgEigenSystems(TestLinalgBase):
         msg = name + "() argument must not cause a domain change."
         self.assert_error(cfunc, args, msg)
 
-    def checker_for_linalg_eig(
-            self, name, func, expected_res_len, check_for_domain_change=None):
-        """
-        Test np.linalg.eig
-        """
-        n = 10
-        cfunc = jit(nopython=True)(func)
-
-        def check(a):
-            expected = func(a)
-            got = cfunc(a)
+    def _check_worker(self, cfunc, name, expected_res_len,
+                      check_for_domain_change):
+        def check(*args):
+            expected = cfunc.py_func(*args)
+            got = cfunc(*args)
+            a = args[0]
             # check that the returned tuple is same length
             self.assertEqual(len(expected), len(got))
             # and that dimension is correct
@@ -935,7 +954,7 @@ class TestLinalgEigenSystems(TestLinalgBase):
                     if name[-1] == 'h':
                         idxl = np.nonzero(np.eye(a.shape[0], a.shape[1], -1))
                         idxu = np.nonzero(np.eye(a.shape[0], a.shape[1], 1))
-                        cfunc(a)
+                        cfunc(*args)
                         # upper idx must match lower for default uplo="L"
                         # if complex, conjugate
                         a[idxu] = np.conj(a[idxl])
@@ -975,7 +994,19 @@ class TestLinalgEigenSystems(TestLinalgBase):
 
             # Ensure proper resource management
             with self.assertNoNRTLeak():
-                cfunc(a)
+                cfunc(*args)
+        return check
+
+    def checker_for_linalg_eig(
+            self, name, func, expected_res_len, check_for_domain_change=None):
+        """
+        Test np.linalg.eig
+        """
+        n = 10
+        cfunc = jit(nopython=True)(func)
+        check = self._check_worker(cfunc, name, expected_res_len,
+                                   check_for_domain_change)
+
 
         # The main test loop
         for dtype, order in product(self.dtypes, 'FC'):
@@ -1044,6 +1075,48 @@ class TestLinalgEigenSystems(TestLinalgBase):
     def test_linalg_eigvalsh(self):
         self.checker_for_linalg_eig("eigvalsh", eigvalsh_matrix, 1, False)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        # checks inputs are not mutated
+
+        for c in (('eig', 2, True),
+                  ('eigvals', 1, True),
+                  ('eigh', 2, False),
+                  ('eigvalsh', 1, False)):
+
+            m, nout, domain_change = c
+
+            meth = getattr(np.linalg, m)
+
+            @jit(nopython=True)
+            def func(X, test):
+                if test:
+                    # not executed, but necessary to trigger A ordering in X
+                    X = X[1:2, :]
+                return meth(X)
+
+            check = self._check_worker(func, m, nout, domain_change)
+
+            for dtype in (np.float64, np.complex128):
+                with self.subTest(meth=meth, dtype=dtype):
+                    # trivial system, doesn't matter, just checking if it gets
+                    # mutated
+                    X = np.array([[10., 1, 0, 1],
+                                [1, 9, 0, 0],
+                                [0, 0, 8, 0],
+                                [1, 0, 0, 7],
+                                ], order='F', dtype=dtype)
+
+                    X_orig = np.copy(X)
+
+                    expected = func.py_func(X, False)
+                    np.testing.assert_allclose(X, X_orig)
+
+                    got = func(X, False)
+                    np.testing.assert_allclose(X, X_orig)
+
+                    check(X, False)
+
 
 class TestLinalgSvd(TestLinalgBase):
     """
@@ -1144,6 +1217,31 @@ class TestLinalgSvd(TestLinalgBase):
             args = (np.empty(sz), True)
             self.assert_raise_on_empty(cfunc, args)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.svd(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        for e_a, g_a in zip(expected, got):
+            np.testing.assert_allclose(e_a, g_a)
+
 
 class TestLinalgQr(TestLinalgBase):
     """
@@ -1239,6 +1337,31 @@ class TestLinalgQr(TestLinalgBase):
         for sz in [(0, 1), (1, 0), (0, 0)]:
             self.assert_raise_on_empty(cfunc, (np.empty(sz),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.qr(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        for e_a, g_a in zip(expected, got):
+            np.testing.assert_allclose(e_a, g_a)
+
 
 class TestLinalgSystems(TestLinalgBase):
     """
@@ -1454,8 +1577,8 @@ class TestLinalgLstsq(TestLinalgSystems):
         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 
+        [(1, 1), (0,)], # valid A, empty 1D b
+        [(1, 1), (1, 0)],  # valid A, empty 2D b
         ]
 
         for A, b in empties:
@@ -1504,6 +1627,28 @@ class TestLinalgLstsq(TestLinalgSystems):
         self.assert_dimensionally_invalid(cfunc, (ok, bad1D))
         self.assert_dimensionally_invalid(cfunc, (ok, bad2D))
 
+    @needs_lapack
+    def test_issue3368(self):
+        X = np.array([[1., 7.54, 6.52],
+                      [1., 2.70, 4.00],
+                      [1., 2.50, 3.80],
+                      [1., 1.15, 5.64],
+                      [1., 4.22, 3.27],
+                      [1., 1.41, 5.70],], order='F')
+
+        X_orig = np.copy(X)
+        y = np.array([1., 2., 3., 4., 5., 6.])
+
+        @jit(nopython=True)
+        def f2(X, y, test):
+            if test:
+                # never executed, but necessary to trigger the bug
+                X = X[1:2, :]
+            return np.linalg.lstsq(X, y)
+
+        f2(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+
 
 class TestLinalgSolve(TestLinalgSystems):
     """
@@ -1627,6 +1772,34 @@ class TestLinalgSolve(TestLinalgSystems):
         bad2D = self.specific_sample_matrix((2, 2), np.float64, 'C', rank=1)
         self.assert_raise_on_singular(cfunc, (bad2D, ok))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 1, 1, 1],
+                      [0., 1, 1, 1],
+                      [0., 0, 1, 1],
+                      [1., 0, 0, 1],], order='F')
+
+        X_orig = np.copy(X)
+        y = np.array([1., 2., 3., 4])
+        y_orig = np.copy(y)
+
+        @jit(nopython=True)
+        def func(X, y, test):
+            if test:
+                # not executed, triggers A order in X
+                X = X[1:2, :]
+            return np.linalg.solve(X, y)
+
+        expected = func.py_func(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+        np.testing.assert_allclose(y, y_orig)
+
+        got = func(X, y, False)
+        np.testing.assert_allclose(X, X_orig)
+        np.testing.assert_allclose(y, y_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgPinv(TestLinalgBase):
     """
@@ -1749,6 +1922,29 @@ class TestLinalgPinv(TestLinalgBase):
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
 
+    @needs_lapack
+    def test_issue5870(self):
+        # testing for mutation of input matrix
+        @jit(nopython=True)
+        def some_fn(v):
+            return np.linalg.pinv(v[0])
+
+        v_data = np.array([[1., 3, 2, 7,],
+                           [-5, 4, 2, 3,],
+                           [9, -3, 1, 1,],
+                           [2, -2, 2, 8,]], order='F')
+
+        v_orig = np.copy(v_data)
+        reshaped_v = v_data.reshape((1, 4, 4))
+
+        expected = some_fn.py_func(reshaped_v)
+        np.testing.assert_allclose(v_data, v_orig)
+
+        got = some_fn(reshaped_v)
+        np.testing.assert_allclose(v_data, v_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgDetAndSlogdet(TestLinalgBase):
     """
@@ -1849,6 +2045,30 @@ class TestLinalgDetAndSlogdet(TestLinalgBase):
         cfunc = jit(nopython=True)(slogdet_matrix)
         self.do_test("slogdet", self.check_slogdet, cfunc)
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.slogdet(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 # Use TestLinalgSystems as a base to get access to additional
 # testing for 1 and 2D inputs.
 
@@ -2148,6 +2368,34 @@ class TestLinalgMatrixRank(TestLinalgSystems):
                                   (np.array([[1., 2., ], [np.inf, np.nan]],
                                             dtype=np.float64),))
 
+    @needs_lapack
+    def test_no_input_mutation(self):
+        # this is here to test no input mutation by
+        # numba.np.linalg._compute_singular_values
+        # which is the workhorse for norm with 2d input, rank and cond.
+
+        X = np.array([[1., 3, 2, 7,],
+                      [-5, 4, 2, 3,],
+                      [9, -3, 1, 1,],
+                      [2, -2, 2, 8,]], order='F')
+
+        X_orig = np.copy(X)
+
+        @jit(nopython=True)
+        def func(X, test):
+            if test:
+                # not executed, but necessary to trigger A ordering in X
+                X = X[1:2, :]
+            return np.linalg.matrix_rank(X)
+
+        expected = func.py_func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        got = func(X, False)
+        np.testing.assert_allclose(X, X_orig)
+
+        np.testing.assert_allclose(expected, got)
+
 
 class TestLinalgMatrixPower(TestLinalgBase):
     """
@@ -2398,5 +2646,46 @@ class TestBasics(TestLinalgSystems):  # TestLinalgSystems for 1d test
         self.assert_error(cfunc, args, msg, err=errors.TypingError)
 
 
+class TestHelpers(TestCase):
+    def test_copy_to_fortran_order(self):
+        from numba.np.linalg import _copy_to_fortran_order
+
+        def check(udt, expectfn, shapes, dtypes, orders):
+            for shape, dtype, order in product(shapes, dtypes, orders):
+                a = np.arange(np.prod(shape)).reshape(shape, order=order)
+
+                r = udt(a)
+                # check correct operation
+                self.assertPreciseEqual(expectfn(a), r)
+                # check new copy has made
+                self.assertNotEqual(a.ctypes.data, r.ctypes.data)
+
+        @njit
+        def direct_call(a):
+            return _copy_to_fortran_order(a)
+
+        shapes = [(3, 4), (3, 2, 5)]
+        dtypes = [np.intp]
+        orders = ['C', 'F']
+        check(direct_call, np.asfortranarray, shapes, dtypes, orders)
+
+
+        @njit
+        def slice_to_any(a):
+            # make a 'any' layout slice
+            sliced = a[::2][0]
+            return _copy_to_fortran_order(sliced)
+
+        shapes = [(3, 3, 4), (3, 3, 2, 5)]
+        dtypes = [np.intp]
+        orders = ['C', 'F']
+
+        def expected_slice_to_any(a):
+            # make a 'any' layout slice
+            sliced = a[::2][0]
+            return np.asfortranarray(sliced)
+
+        check(slice_to_any, expected_slice_to_any, shapes, dtypes, orders)
+
 if __name__ == '__main__':
     unittest.main()