Repository: scikit-hep/awkward

Issue:
Positional reducers produce strange results for `axis != -1` on regular types
### Version of Awkward Array

main

### Description and code to reproduce

```python
import awkward as ak
import numpy as np

x = np.arange(2 * 3).reshape(2, 3)

y = ak.from_numpy(x, regulararray=True)
y.show()

result = ak.argmin(y, axis=0, keepdims=True)
result.show()
```


Discussion:

Resolving patch:
diff --git a/src/awkward/contents/listoffsetarray.py b/src/awkward/contents/listoffsetarray.py
--- a/src/awkward/contents/listoffsetarray.py
+++ b/src/awkward/contents/listoffsetarray.py
@@ -1665,7 +1665,6 @@ def _rearrange_prepare_next(self, outlength, parents):
         )
         maxcount = index_nplike.index_as_shape_item(_maxcount[0])
 
-        # A "stable" sort is essential for the subsequent steps.
         nextcarry = Index64.empty(nextlen, nplike=index_nplike)
         nextparents = Index64.empty(nextlen, nplike=index_nplike)
         _maxnextparents = Index64.empty(1, index_nplike)
diff --git a/src/awkward/contents/regulararray.py b/src/awkward/contents/regulararray.py
--- a/src/awkward/contents/regulararray.py
+++ b/src/awkward/contents/regulararray.py
@@ -996,18 +996,6 @@ def _reduce_next(
         branch, depth = self.branch_depth
         nextlen = self._length * self._size
         if not branch and negaxis == depth:
-            if self._size == 0:
-                nextstarts = ak.index.Index64(
-                    index_nplike.zeros(self._length, dtype=np.int64),
-                    nplike=index_nplike,
-                )
-            else:
-                nextstarts = ak.index.Index64(
-                    index_nplike.arange(0, nextlen, self._size),
-                    nplike=index_nplike,
-                )
-            assert nextstarts.length == self._length
-
             nextcarry = ak.index.Index64.empty(nextlen, nplike=index_nplike)
             nextparents = ak.index.Index64.empty(nextlen, nplike=index_nplike)
             assert (
@@ -1026,7 +1014,28 @@ def _reduce_next(
                     nextparents.data,
                     parents.data,
                     self._size,
-                    len(self),
+                    self._length,
+                )
+            )
+            nextstarts = ak.index.Index64.empty(
+                # `starts` must have at least enough elements for the largest `nextparent` to index into
+                # The upper bound for this value is given by `nextlen` (each item in this list belonging
+                # to a distinct reduction), but the length of `starts` should equate to `maxnextparents - 1`.
+                starts.length * self._size,
+                nplike=index_nplike,
+            )
+            assert (
+                nextstarts.nplike is index_nplike and nextparents.nplike is index_nplike
+            )
+            self._handle_error(
+                self._backend[
+                    "awkward_ListOffsetArray_reduce_nonlocal_nextstarts_64",
+                    nextstarts.dtype.type,
+                    nextparents.dtype.type,
+                ](
+                    nextstarts.data,
+                    nextparents.data,
+                    nextlen,
                 )
             )
 
@@ -1456,5 +1465,5 @@ def _to_backend(self, backend: Backend) -> Self:
 
     def _is_equal_to(self, other, index_dtype, numpyarray):
         return self._size == other.size and self._content.is_equal_to(
-            self._content, index_dtype, numpyarray
+            other._content, index_dtype, numpyarray
         )


Test patch:
diff --git a/tests/test_2490_reduce_regulararray_positional.py b/tests/test_2490_reduce_regulararray_positional.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2490_reduce_regulararray_positional.py
@@ -0,0 +1,38 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np
+import pytest  # noqa: F401
+
+import awkward as ak
+
+
+def test():
+    # This example is designed to test a few assumptions about reduction in the regular reduction pathway
+    # The outer list introduces _unique parents_ for local (axis>0) reduction
+    # The child option introduces a missing parent: parents become _non-dense_
+    # The regular array tests our regular reduction pathway
+    layout = ak.contents.ListOffsetArray(
+        ak.index.Index64([0, 3, 4, 6]),
+        ak.contents.ByteMaskedArray(
+            ak.index.Index8([1, 1, 1, 0, 1, 1]),
+            ak.contents.RegularArray(
+                ak.contents.NumpyArray(
+                    np.array(
+                        [0, 2, 1, 3, 1, 0, 1, 0, 3, 9, 1, 7, 7, 6, 5, 7, 4, 8],
+                        dtype=np.int64,
+                    )
+                ),
+                size=3,
+            ),
+            valid_when=True,
+        ),
+    )
+    result = ak.argmax(layout, axis=1, highlevel=False, mask_identity=False)
+    assert result.is_equal_to(
+        ak.contents.RegularArray(
+            ak.contents.NumpyArray(
+                np.array([1, 0, 2, -1, -1, -1, 0, 0, 1], dtype=np.int64)
+            ),
+            size=3,
+        )
+    )