Skip to content

feat: implement array compatibility/interface protocols for virtual array and placeholder array (for better errors)#3839

Merged
ikrommyd merged 16 commits into
scikit-hep:mainfrom
ikrommyd:array-interface-for-virtual-array
Feb 18, 2026
Merged

feat: implement array compatibility/interface protocols for virtual array and placeholder array (for better errors)#3839
ikrommyd merged 16 commits into
scikit-hep:mainfrom
ikrommyd:array-interface-for-virtual-array

Conversation

@ikrommyd

@ikrommyd ikrommyd commented Feb 4, 2026

Copy link
Copy Markdown
Member

This is to fix all the ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all() issues we see with the cuda backend with __setitem__ and __getitem__ between virtual arrays with a cupy nplike and regular cupy arrays. We xfail several tests due to that. This will work with cupy 14 once it comes out and causes no errors otherwise. With cupy 14, the tests we xfail actually pass (tried with cupy installation from v14 pre-release) Once cupy 14 is out, we should remove those xfails too. See cupy/cupy#9089 and cupy/cupy#8352 and #3142 for more info.

Also this is to fix a few more cases similar to the cupy issue but with the numpy backend where if you do y[:] = x and y is a numpy array and x a virtual array, this will use __iter__ and iterate over the virtual array like a python object. Adding the interface fixes that.

@codecov

codecov Bot commented Feb 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.64%. Comparing base (bb827d0) to head (8eb7fd5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/awkward/_nplikes/placeholder.py 57.14% 6 Missing ⚠️
src/awkward/_nplikes/virtual.py 76.92% 3 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
src/awkward/_nplikes/virtual.py 91.16% <76.92%> (-0.96%) ⬇️
src/awkward/_nplikes/placeholder.py 65.21% <57.14%> (-1.19%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Feb 4, 2026

Copy link
Copy Markdown

The documentation preview is ready to be viewed at http://preview.awkward-array.org.s3-website.us-east-1.amazonaws.com/PR3839

@ikrommyd
ikrommyd marked this pull request as ready for review February 4, 2026 07:31
@ikrommyd

ikrommyd commented Feb 4, 2026

Copy link
Copy Markdown
Member Author

@pfackeldey we've been iterating over numpy arrays this whole time in certain edge cases. If you do y[:] = x and y is a numpy array and x a virtual array, this uses __iter__ and iterates over the virtual array like a python object. Adding the interface makes numpy able to convert the virtual array into a numpy array instead of seeing it as an iterable.

@ikrommyd
ikrommyd marked this pull request as draft February 4, 2026 15:44
@ikrommyd ikrommyd changed the title feat: implement array interface protocol for virtual array feat: implement array compatibility/interface protocols for virtual array Feb 4, 2026
Comment thread src/awkward/_nplikes/virtual.py Outdated
@ikrommyd
ikrommyd marked this pull request as ready for review February 10, 2026 19:44
@ikrommyd
ikrommyd requested a review from pfackeldey February 10, 2026 19:44
@ikrommyd

ikrommyd commented Feb 10, 2026

Copy link
Copy Markdown
Member Author

This is actually way more critical than I thought. I added the following code to catch and raise an error when a module like numpy cupy or jax try to iterate over an array because they don't find the appopriate way to convert it.

diff --git a/src/awkward/_nplikes/typetracer.py b/src/awkward/_nplikes/typetracer.py
index 7f8da1ec..6eb45a5a 100644
--- a/src/awkward/_nplikes/typetracer.py
+++ b/src/awkward/_nplikes/typetracer.py
@@ -405,6 +405,32 @@ class TypeTracerArray(NDArrayOperatorsMixin, ArrayLike):
             "Bug in Awkward Array: cannot convert TypeTracerArray into a concrete array"
         )
 
+    _cycler = 0
+
+    def __getattr__(self, name):
+        if name == "__array_struct__":
+            self._cycler = 1
+            raise AttributeError(name)
+        elif name == "__array_interface__":
+            if self._cycler == 1:
+                self._cycler = 2
+            else:
+                self._cycler = 0
+            return self._data.__array_interface__
+        elif name == "__array__":
+            if self._cycler == 2:
+                self._cycler = 3
+                raise Exception(
+                    "Module is coming to the conclusion that VirtualNDArray is a Python iterable"
+                )
+            else:
+                self._cycler = 0
+            raise AttributeError(name)
+        elif name in self.__dict__:
+            return self.__dict__[name]
+        else:
+            raise AttributeError(name)
+
     class _CTypes:
         data = 0
 
diff --git a/src/awkward/_nplikes/virtual.py b/src/awkward/_nplikes/virtual.py
index 0c517423..6eafe6db 100644
--- a/src/awkward/_nplikes/virtual.py
+++ b/src/awkward/_nplikes/virtual.py
@@ -413,3 +413,29 @@ class VirtualNDArray(NDArrayOperatorsMixin, MaterializableArray):
 
     def __reduce__(self):
         return self.materialize().__reduce__()
+
+    _cycler = 0
+
+    def __getattr__(self, name):
+        if name == "__array_struct__":
+            self._cycler = 1
+            raise AttributeError(name)
+        elif name == "__array_interface__":
+            if self._cycler == 1:
+                self._cycler = 2
+            else:
+                self._cycler = 0
+            return self._data.__array_interface__
+        elif name == "__array__":
+            if self._cycler == 2:
+                self._cycler = 3
+                raise Exception(
+                    "Module is coming to the conclusion that VirtualNDArray is a Python iterable"
+                )
+            else:
+                self._cycler = 0
+            raise AttributeError(name)
+        elif name in self.__dict__:
+            return self.__dict__[name]
+        else:
+            raise AttributeError(name)
diff --git a/src/awkward/contents/content.py b/src/awkward/contents/content.py
index 2148e31f..801149f1 100644
--- a/src/awkward/contents/content.py
+++ b/src/awkward/contents/content.py
@@ -1406,6 +1406,32 @@ class Content(Meta):
     def _arrow_needs_option_type(cls):
         return cls.is_option  # is_option is a class property of Meta
 
+    _cycler = 0
+
+    def __getattr__(self, name):
+        if name == "__array_struct__":
+            self._cycler = 1
+            raise AttributeError(name)
+        elif name == "__array_interface__":
+            if self._cycler == 1:
+                self._cycler = 2
+            else:
+                self._cycler = 0
+            return self._data.__array_interface__
+        elif name == "__array__":
+            if self._cycler == 2:
+                self._cycler = 3
+                raise Exception(
+                    "Module is coming to the conclusion that NumpyArray is a Python iterable"
+                )
+            else:
+                self._cycler = 0
+            raise AttributeError(name)
+        elif name in self.__dict__:
+            return self.__dict__[name]
+        else:
+            raise AttributeError(name)
+
 
 @register_backend_lookup_factory
 def find_content_backend(obj: type):
diff --git a/src/awkward/index.py b/src/awkward/index.py
index f709930d..1014c5e3 100644
--- a/src/awkward/index.py
+++ b/src/awkward/index.py
@@ -177,6 +177,32 @@ class Index:
     def __len__(self) -> int:
         return int(self.length)
 
+    _cycler = 0
+
+    def __getattr__(self, name):
+        if name == "__array_struct__":
+            self._cycler = 1
+            raise AttributeError(name)
+        elif name == "__array_interface__":
+            if self._cycler == 1:
+                self._cycler = 2
+            else:
+                self._cycler = 0
+            return self._data.__array_interface__
+        elif name == "__array__":
+            if self._cycler == 2:
+                self._cycler = 3
+                raise Exception(
+                    "Module is coming to the conclusion that Index is a Python iterable"
+                )
+            else:
+                self._cycler = 0
+            raise AttributeError(name)
+        elif name in self.__dict__:
+            return self.__dict__[name]
+        else:
+            raise AttributeError(name)
+
     @property
     def __cuda_array_interface__(self):
         return self._data.__cuda_array_interface__  # type: ignore[attr-defined]

And if I run all the tests, we get this:

FAILED tests/test_3364_virtualarray.py::test_float_array_rounding - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_float_array_nan - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_nested_virtual_arrays - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_to_numpy - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_flatten - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_unflatten - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_count - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_count_nonzero - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_sum - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_prod - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_any - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_all - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_min - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_max - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_argmin - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_argmax - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_sort - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_argsort - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3364_virtualarray.py::test_listarray_run_lengths - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_to_numpy - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_flatten - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_unflatten - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_count - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_count_nonzero - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_sum - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_prod - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_any - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_all - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_min - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_max - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_argmin - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_argmax - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_sort - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_argsort - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3451_virtualarray_with_jax.py::test_listarray_run_lengths - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_float_array_rounding[None] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_float_array_rounding[use_shape_generator] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_float_array_nan[None] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_float_array_nan[use_shape_generator] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_nested_virtual_arrays[None] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3475_virtualarray_unknown_length.py::test_nested_virtual_arrays[use_shape_generator] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable
FAILED tests/test_3612_virtualarray_to_packed_and_pickling.py::test_list_array_to_packed[False] - Exception: Module is coming to the conclusion that VirtualNDArray is a Python iterable

So it looks like we iterate over virtual arrays pretty often by mistake. We should fix this with this PR ASAP. There are no failures with this PR.

cc @ianna

@pfackeldey pfackeldey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah! Good catch @ikrommyd and thanks for the checks with the test suite.

Yes, that is important to get in!

It looks good to me, I'm not familiar with the cupy protocols, but the numpy and jax one look correct to me 👍 (I haven't seen __array_interface__, what's that used for?)

@pfackeldey

Copy link
Copy Markdown
Collaborator

Are there actually cases where we want __iter__ to work on VirtualNDArrays? Would it make sense to error on __iter__ to catch those kind of things in the future?

@pfackeldey pfackeldey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I need to request changes (withdraw my approval for now)...

I thought about it a bit more and I think we shouldn't add any of those protocols to VirtualNDArray. Instead, we need to trace down the cases where we forgot to call maybe_materialize (e.g. by erroring on __iter__).
What do you think about this?

@ikrommyd

ikrommyd commented Feb 11, 2026

Copy link
Copy Markdown
Member Author

Sorry, I need to request changes (withdraw my approval for now)...

I thought about it a bit more and I think we shouldn't add any of those protocols to VirtualNDArray. Instead, we need to trace down the cases where we forgot to call maybe_materialize (e.g. by erroring on __iter__). What do you think about this?

I don't think this needs to be manual. In the same way virtual array + 1 automatically just works, I'd want ndarray[virtual array] and ndarray[something] = virtual array to work too. virtual array should be interoperable with array libraries in that sense. I don't want to leave these and only these cases out.

Are there actually cases where we want iter to work on VirtualNDArrays? Would it make sense to error on iter to catch those kind of things in the future?

Well it's a buffer so yeah, if you want to manually iterate over a buffer, who are we to stop it. I'm not saying one should be iterating over arrays but yeah I'd rather allow it to be possible.

@ikrommyd

Copy link
Copy Markdown
Member Author

Now that you mention that, I actually think the placeholder should implement these and throw the "good" error message that we have too.

@ikrommyd ikrommyd changed the title feat: implement array compatibility/interface protocols for virtual array feat: implement array compatibility/interface protocols for virtual array and placeholder array Feb 11, 2026
@ikrommyd ikrommyd changed the title feat: implement array compatibility/interface protocols for virtual array and placeholder array feat: implement array compatibility/interface protocols for virtual array and placeholder array (for better errors) Feb 11, 2026
@ikrommyd

ikrommyd commented Feb 11, 2026

Copy link
Copy Markdown
Member Author

FWIW, I think It's actually not good that we have to add maybe_materialize in a few and only a few places manually just because we lack interoperability
maybe_materialize should be called as few times as possible manually but it's not, just because we lacked interporability (or the other packages did like cupy where we reportred issues for setitem and getitem).
We should try to remove maybe_materialize explicit calls not try to add more of them. I am strong believer that automatic interporability is better than manual case by case handling.

@pfackeldey pfackeldey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked this through offline in detail:

These protocols are good to add, better than manual maybe_materialize. In the long run we can get rid of many of these maybe_materialize calls as well, which helps in maintaining this code. So all in all, I'm happy with these changes. Also, my concern of potential over materialization is not valid: we would've hit those over materializations already now through __iter__.

So, everything good from my side 👍

@ikrommyd

Copy link
Copy Markdown
Member Author

Thanks! Yeah I'll remove as many maybe_materialize as I can in a separate PR. I was able to do most of them actually locally while testing this but it requires more thorough testing to ensure that we don't end up with a virtual array in the wrong places.

@ikrommyd
ikrommyd merged commit 9b88bab into scikit-hep:main Feb 18, 2026
75 of 83 checks passed
@ikrommyd
ikrommyd deleted the array-interface-for-virtual-array branch February 18, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants