Repository: scikit-hep/awkward

Issue:
ak.cartesian(..., nested=True) results in behaviour expected from nested=False
### Version of Awkward Array

2.5.1

### Description and code to reproduce

In awkward 2 the behaviour from ak.cartesian is unexpected and deviant from behaviour in v1.

```
>>> one = ak.zip({"pt": [[1, 2], [1]], "eta": [[1.1, 2.1], [1]]})
>>> two = ak.zip({"pt": [[2.1], [2.21, 2.22]], "eta": [[2.11], [2.221, 2.222]]})
>>> ak.cartesian({"foo": one, "bar": two}, nested=False)["foo"].show()
[[{pt: 1, eta: 1.1}, {pt: 2, eta: 2.1}],
 [{pt: 1, eta: 1}, {pt: 1, eta: 1}]]
ak.cartesian({"foo": one, "bar": two}, nested=True)["foo"].show()
[[{pt: 1, eta: 1.1}, {pt: 2, eta: 2.1}],
 [{pt: 1, eta: 1}, {pt: 1, eta: 1}]]
```

In v1 (1.9.0) the behaviour was:
```
>>> for x in ak.cartesian({"foo": one, "bar": two}, nested=True)["foo"]:
>>>     print(x)
[[{pt: 1, eta: 1.1}], [{pt: 2, eta: 2.1}]]
[[{pt: 1, eta: 1}, {pt: 1, eta: 1}]]
>>> for x in ak.cartesian({"foo": one, "bar": two}, nested=False)["foo"]:
>>>     print(x)
[{pt: 1, eta: 1.1}, {pt: 2, eta: 2.1}]
[{pt: 1, eta: 1}, {pt: 1, eta: 1}]
```

Am I misinterpreting what `ak.cartesian(..., nested=True)` should do or is this a bug?

Thanks for considering this comment!


Discussion:
This behaviour has changed in awkward 2.x, but not intentionally; it does behave as described in 2.1.0. This code was recently refactored, I'll take a look and see what was dropped!

Resolving patch:
diff --git a/src/awkward/operations/ak_cartesian.py b/src/awkward/operations/ak_cartesian.py
--- a/src/awkward/operations/ak_cartesian.py
+++ b/src/awkward/operations/ak_cartesian.py
@@ -290,8 +290,11 @@ def _impl(arrays, axis, nested, parameters, with_name, highlevel, behavior, attr
 
     backend = next((layout.backend for layout in layouts), cpu)
     if posaxis == 0:
+        # Translate nested field names to nested field indices
         if fields is not None:
-            nested = [i for i, name in enumerate(fields) if name in nested]
+            nested_as_index = [i for i, name in enumerate(fields) if name in nested]
+        else:
+            nested_as_index = nested
 
         indexes = [
             ak.index.Index64(backend.index_nplike.reshape(x, (-1,)))
@@ -309,10 +312,15 @@ def _impl(arrays, axis, nested, parameters, with_name, highlevel, behavior, attr
 
         result = ak.contents.RecordArray(outs, fields, parameters=parameters)
         for i in range(len(array_layouts))[::-1]:
-            if i in nested:
+            if i in nested_as_index:
                 result = ak.contents.RegularArray(result, layouts[i + 1].length, 0)
 
     else:
+        # Translate nested field names to nested field indices
+        if fields is not None:
+            nested_as_index = [i for i, name in enumerate(fields) if name in nested]
+        else:
+            nested_as_index = nested
 
         def add_outer_dimensions(
             layout: ak.contents.Content, n: int
@@ -369,7 +377,7 @@ def apply_pad_inner_list_at_axis(layout, depth, lateral_context, **kwargs):
         axes_to_flatten = [
             posaxis + i + 1
             for i, _ in enumerate(array_layouts)
-            if i < len(array_layouts) - 1 and i not in nested
+            if i < len(array_layouts) - 1 and i not in nested_as_index
         ]
         # This list *must* be sorted in reverse order
         axes_to_flatten.reverse()


Test patch:
diff --git a/tests/test_2329_cartesian_broadcasting_fixes.py b/tests/test_2329_cartesian_broadcasting_fixes.py
--- a/tests/test_2329_cartesian_broadcasting_fixes.py
+++ b/tests/test_2329_cartesian_broadcasting_fixes.py
@@ -8,7 +8,7 @@
 import awkward as ak
 
 
-def test_nested_exis_0():
+def test_nested_axis_0():
     arrays = {"x": np.arange(4), "y": ["this", "that", "foo", "bar!"]}
 
     result = ak.cartesian(arrays, nested=True, axis=0)
diff --git a/tests/test_2928_nested_cartesian_record.py b/tests/test_2928_nested_cartesian_record.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2928_nested_cartesian_record.py
@@ -0,0 +1,94 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward/blob/main/LICENSE
+
+from __future__ import annotations
+
+import numpy as np
+import pytest
+
+import awkward as ak
+
+pa = pytest.importorskip("pyarrow")
+
+
+def test():
+    one = ak.zip({"pt": [[1, 2], [1]], "eta": [[1.1, 2.1], [1]]})
+    two = ak.zip({"pt": [[2.1], [2.21, 2.22]], "eta": [[2.11], [2.221, 2.222]]})
+    assert ak.cartesian({"foo": one, "bar": two}, nested=False).layout.is_equal_to(
+        ak.contents.ListOffsetArray(
+            ak.index.Index64([0, 2, 4]),
+            ak.contents.RecordArray(
+                [
+                    ak.contents.IndexedArray(
+                        ak.index.Index64([0, 1, 2, 2]),
+                        ak.contents.RecordArray(
+                            [
+                                ak.contents.NumpyArray(
+                                    np.array([1, 2, 1], dtype=np.int64)
+                                ),
+                                ak.contents.NumpyArray(
+                                    np.array([1.1, 2.1, 1.0], dtype=np.float64)
+                                ),
+                            ],
+                            ["pt", "eta"],
+                        ),
+                    ),
+                    ak.contents.IndexedArray(
+                        ak.index.Index64([0, 0, 1, 2]),
+                        ak.contents.RecordArray(
+                            [
+                                ak.contents.NumpyArray(
+                                    np.array([2.1, 2.21, 2.22], dtype=np.float64)
+                                ),
+                                ak.contents.NumpyArray(
+                                    np.array([2.11, 2.221, 2.222], dtype=np.float64)
+                                ),
+                            ],
+                            ["pt", "eta"],
+                        ),
+                    ),
+                ],
+                ["foo", "bar"],
+            ),
+        )
+    )
+    assert ak.cartesian({"foo": one, "bar": two}, nested=True).layout.is_equal_to(
+        ak.contents.ListOffsetArray(
+            ak.index.Index64([0, 2, 3]),
+            ak.contents.ListOffsetArray(
+                ak.index.Index64([0, 1, 2, 4]),
+                ak.contents.RecordArray(
+                    [
+                        ak.contents.IndexedArray(
+                            ak.index.Index64([0, 1, 2, 2]),
+                            ak.contents.RecordArray(
+                                [
+                                    ak.contents.NumpyArray(
+                                        np.array([1, 2, 1], dtype=np.int64)
+                                    ),
+                                    ak.contents.NumpyArray(
+                                        np.array([1.1, 2.1, 1.0], dtype=np.float64)
+                                    ),
+                                ],
+                                ["pt", "eta"],
+                            ),
+                        ),
+                        ak.contents.IndexedArray(
+                            ak.index.Index64([0, 0, 1, 2]),
+                            ak.contents.RecordArray(
+                                [
+                                    ak.contents.NumpyArray(
+                                        np.array([2.1, 2.21, 2.22], dtype=np.float64)
+                                    ),
+                                    ak.contents.NumpyArray(
+                                        np.array([2.11, 2.221, 2.222], dtype=np.float64)
+                                    ),
+                                ],
+                                ["pt", "eta"],
+                            ),
+                        ),
+                    ],
+                    ["foo", "bar"],
+                ),
+            ),
+        )
+    )