Repository: scikit-hep/awkward

Issue:
Non-contiguous `NumpyArray` calls `toRegularArray()`
### Version of Awkward Array

main

### Description and code to reproduce

```python
>>> array = ak.from_numpy(np.arange(24)[::3])
>>> ak.sum(array, axis=-1)
Traceback (most recent call last):
  File "/home/angus/.mambaforge/envs/awkward-1.0/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 3398, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-26-8650b4044dc1>", line 1, in <cell line: 1>
    ak.sum(array)
  File "/home/angus/Git/awkward-1.0/awkward/operations/ak_sum.py", line 208, in sum
    return _impl(
  File "/home/angus/Git/awkward-1.0/awkward/operations/ak_sum.py", line 279, in _impl
    out = layout.reduce(
  File "/home/angus/Git/awkward-1.0/awkward/contents/content.py", line 855, in reduce
    return self._reduce_axis_none(
  File "/home/angus/Git/awkward-1.0/awkward/contents/content.py", line 830, in _reduce_axis_none
    layout = reducer.apply_many(parts, mask)
  File "/home/angus/Git/awkward-1.0/awkward/reducers.py", line 460, in apply_many
    name = "prod"
  File "/home/angus/Git/awkward-1.0/awkward/reducers.py", line 58, in _apply_many_trivial
    partial_reductions = [
  File "/home/angus/Git/awkward-1.0/awkward/reducers.py", line 59, in <listcomp>
    x.reduce(
  File "/home/angus/Git/awkward-1.0/awkward/contents/content.py", line 894, in reduce
    next = self._reduce_next(
  File "/home/angus/Git/awkward-1.0/awkward/contents/numpyarray.py", line 1090, in _reduce_next
    return self.toRegularArray()._reduce_next(
  File "/home/angus/Git/awkward-1.0/awkward/contents/numpyarray.py", line 1090, in _reduce_next
    return self.toRegularArray()._reduce_next(
  File "/home/angus/Git/awkward-1.0/awkward/contents/numpyarray.py", line 1090, in _reduce_next
    return self.toRegularArray()._reduce_next(
  [Previous line repeated 2970 more times]
  File "/home/angus/Git/awkward-1.0/awkward/contents/numpyarray.py", line 188, in toRegularArray
    out = NumpyArray(self._data.reshape(-1), None, None, self._nplike)
  File "/home/angus/Git/awkward-1.0/awkward/contents/numpyarray.py", line 46, in __init__
    self._data = nplike.asarray(data)
  File "/home/angus/Git/awkward-1.0/awkward/nplikes.py", line 100, in asarray
    return self._module.asarray(*args, **kwargs)
RecursionError: maximum recursion depth exceeded while calling a Python object
```


Discussion:

Resolving patch:
diff --git a/src/awkward/_typetracer.py b/src/awkward/_typetracer.py
--- a/src/awkward/_typetracer.py
+++ b/src/awkward/_typetracer.py
@@ -918,3 +918,6 @@ def datetime_as_string(self, *args, **kwargs):
     @classmethod
     def is_own_array(cls, obj) -> bool:
         return isinstance(obj, TypeTracerArray)
+
+    def is_c_contiguous(self, array) -> bool:
+        return True
diff --git a/src/awkward/contents/numpyarray.py b/src/awkward/contents/numpyarray.py
--- a/src/awkward/contents/numpyarray.py
+++ b/src/awkward/contents/numpyarray.py
@@ -505,24 +505,7 @@ def contiguous(self):
 
     @property
     def is_contiguous(self):
-        if isinstance(self._nplike, ak._typetracer.TypeTracer):
-            return True
-
-        # Alternatively, self._data.flags["C_CONTIGUOUS"], but the following assumes
-        # less of the nplike.
-
-        if isinstance(self.nplike, ak.nplikes.Jax):
-            return True
-
-        x = self._data.dtype.itemsize
-
-        for i in range(len(self._data.shape), 0, -1):
-            if x != self._data.strides[i - 1]:
-                return False
-            else:
-                x = x * self._data.shape[i - 1]
-
-        return True
+        return self._nplike.is_c_contiguous(self._data)
 
     def _subranges_equal(self, starts, stops, length, sorted=True):
         is_equal = ak.index.Index64.zeros(1, self._nplike)
@@ -1071,7 +1054,7 @@ def _reduce_next(
         keepdims,
         behavior,
     ):
-        if len(self._data.shape) != 1 or not self.is_contiguous:
+        if self._data.ndim > 1:
             return self.toRegularArray()._reduce_next(
                 reducer,
                 negaxis,
@@ -1083,6 +1066,22 @@ def _reduce_next(
                 keepdims,
                 behavior,
             )
+        elif not self.is_contiguous:
+            return self.contiguous()._reduce_next(
+                reducer,
+                negaxis,
+                starts,
+                shifts,
+                parents,
+                outlength,
+                mask,
+                keepdims,
+                behavior,
+            )
+
+        # Yes, we've just tested these, but we need to be explicit that they are invariants
+        assert self.is_contiguous
+        assert self._data.ndim == 1
 
         if isinstance(self.nplike, ak.nplikes.Jax):
             from awkward._connect.jax.reducers import get_jax_reducer  # noqa: F401
diff --git a/src/awkward/nplikes.py b/src/awkward/nplikes.py
--- a/src/awkward/nplikes.py
+++ b/src/awkward/nplikes.py
@@ -374,6 +374,9 @@ def is_own_array(cls, obj) -> bool:
         """
         raise NotImplementedError
 
+    def is_c_contiguous(self, array) -> bool:
+        raise ak._errors.wrap_error(NotImplementedError)
+
 
 class NumpyKernel:
     def __init__(self, kernel, name_and_types):
@@ -541,6 +544,9 @@ def is_own_array(cls, obj) -> bool:
         """
         return isinstance(obj, numpy.ndarray)
 
+    def is_c_contiguous(self, array) -> bool:
+        return array.flags["C_CONTIGUOUS"]
+
 
 class Cupy(NumpyLike):
     is_eager = False
@@ -754,6 +760,9 @@ def is_own_array(cls, obj) -> bool:
         module, _, suffix = type(obj).__module__.partition(".")
         return module == "cupy"
 
+    def is_c_contiguous(self, array) -> bool:
+        return array.flags["C_CONTIGUOUS"]
+
 
 class Jax(NumpyLike):
     @property
@@ -926,6 +935,9 @@ def is_tracer(cls, obj) -> bool:
         module, _, suffix = type(obj).__module__.partition(".")
         return module == "jax"
 
+    def is_c_contiguous(self, array) -> bool:
+        return True
+
 
 # Temporary sentinel marking "argument not given"
 _UNSET = object()


Test patch:
diff --git a/tests/test_1847-numpy-array-contiguous.py b/tests/test_1847-numpy-array-contiguous.py
new file mode 100644
--- /dev/null
+++ b/tests/test_1847-numpy-array-contiguous.py
@@ -0,0 +1,39 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np  # noqa: F401
+import pytest  # noqa: F401
+
+import awkward as ak  # noqa: F401
+
+
+def test_reduce_1d_strided():
+    non_contiguous_array = np.arange(64, dtype=np.int64)[::3]
+    layout = ak.contents.NumpyArray(
+        non_contiguous_array,
+    )
+    assert not layout.is_contiguous
+    assert ak.sum(layout, axis=-1) == np.sum(non_contiguous_array, axis=-1)
+
+
+def test_reduce_transpose_2d():
+    non_contiguous_array = np.arange(6 * 8, dtype=np.int64).reshape(6, 8).T
+    layout = ak.contents.NumpyArray(
+        non_contiguous_array,
+    )
+    assert not layout.is_contiguous
+    assert (
+        ak.sum(layout, axis=-1).to_list()
+        == np.sum(non_contiguous_array, axis=-1).tolist()
+    )
+
+
+def test_reduce_2d():
+    non_contiguous_array = np.arange(6 * 8, dtype=np.int64).reshape(6, 8)
+    layout = ak.contents.NumpyArray(
+        non_contiguous_array,
+    )
+    assert layout.is_contiguous
+    assert (
+        ak.sum(layout, axis=-1).to_list()
+        == np.sum(non_contiguous_array, axis=-1).tolist()
+    )