Repository: scikit-hep/awkward

Issue:
Define wrappers for pure NumPy functions that don't automatically call '__array__'
We can override any NumPy function using our own internal `@ak._connect._numpy.implements` decorator, and this has been used to give NumPy functions Awkward generalizations. However, there are still a lot of NumPy functions that fail for Awkward Arrays, even flat ones, because NumPy isn't calling calling `__array__` on the `ak.Array`. Some NumPy functions do, others don't.

To make this more predictable, someone (me?) should do a survey of all functions in the `numpy` namespace find the ones that don't automatically call `__array__`, and define them in a submodule of their own, like this:

```python
@ak._connect._numpy.implements("whatever")
def whatever(array_arg, another_array_arg, non_array_arg):
    nplike = ak.nplike.of(array_arg, another_array_arg)._module
    return nplike.whatever(nplike.asarray(array_arg), nplike.asarray(another_array_arg), non_array_arg)
```

This could be done more systematically with `*args` and `**kwargs`, but we still need to know which arguments are supposed to be arrays to pass them through `nplike.asarray`. (Here, I'm using the direct `._module`, rather than going through the full [NumpyLike](https://github.com/scikit-hep/awkward-1.0/blob/68087850eabafd39f0c0de37ed96487fa44cc69f/src/awkward/nplike.py#L120) interface because I don't want to verify NumPy-CuPy agreement/lack-of-agreement for every one of these. The `nplike.asarray` function raises an exception if the Awkward Array isn't a rectilinear, numerical array, so this isn't generalizing NumPy's behavior so much as making Awkward-brand arrays usable as NumPy arrays in exactly the cases that NumPy itself covers (except `dtype="O"` and strings).

These functions will _not_ be used in the main Awkward codebase, they will _not_ be exposed to the top-level `awkward` namespace, and they will _not_ be documented on ReadTheDocs. Among them are some functions that we might want to generalize someday, and that would mean moving them from this submodule to one of the standard places and possibly narrowing the argument list.

This submodule should probably be called `ak._connect._numpy_compatibility` or something. It should be imported by `__init__.py` after `ak._connect._numpy`.


Discussion:

Resolving patch:
diff --git a/src/awkward/_connect/_numpy.py b/src/awkward/_connect/_numpy.py
--- a/src/awkward/_connect/_numpy.py
+++ b/src/awkward/_connect/_numpy.py
@@ -20,10 +20,36 @@ def convert_to_array(layout, args, kwargs):
 implemented = {}
 
 
+def _to_rectilinear(arg):
+    if isinstance(
+        arg,
+        (
+            ak.Array,
+            ak.Record,
+            ak.ArrayBuilder,
+            ak.layout.Content,
+            ak.layout.Record,
+            ak.layout.ArrayBuilder,
+            ak.layout.LayoutBuilder,
+        ),
+    ):
+        nplike = ak.nplike.of(arg)
+        return nplike.to_rectilinear(arg)
+    else:
+        return arg
+
+
 def array_function(func, types, args, kwargs):
     function = implemented.get(func)
     if function is None:
-        return NotImplemented
+        args = tuple(_to_rectilinear(x) for x in args)
+        kwargs = dict((k, _to_rectilinear(v)) for k, v in kwargs.items())
+        out = func(*args, **kwargs)
+        nplike = ak.nplike.of(out)
+        if isinstance(out, nplike.ndarray) and len(out.shape) != 0:
+            return ak.Array(out)
+        else:
+            return out
     else:
         return function(*args, **kwargs)
 
diff --git a/src/awkward/nplike.py b/src/awkward/nplike.py
--- a/src/awkward/nplike.py
+++ b/src/awkward/nplike.py
@@ -358,6 +358,9 @@ def __call__(self, *args):
 
 
 class Numpy(NumpyLike):
+    def to_rectilinear(self, array, *args, **kwargs):
+        return ak.operations.convert.to_numpy(array, *args, **kwargs)
+
     def __getitem__(self, args):
         return NumpyKernel(ak._cpu_kernels.kernel[args])
 
@@ -372,8 +375,15 @@ def ma(self):
     def char(self):
         return self._module.char
 
+    @property
+    def ndarray(self):
+        return self._module.ndarray
+
 
 class Cupy(NumpyLike):
+    def to_rectilinear(self, array, *args, **kwargs):
+        return ak.operations.convert.to_cupy(array, *args, **kwargs)
+
     def __getitem__(self, args):
         raise NotImplementedError("no CUDA in v2 yet")
 


Test patch:
diff --git a/tests/test_0595-0630-default-for-nep18.py b/tests/test_0595-0630-default-for-nep18.py
new file mode 100644
--- /dev/null
+++ b/tests/test_0595-0630-default-for-nep18.py
@@ -0,0 +1,19 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+from __future__ import absolute_import
+
+import pytest  # noqa: F401
+import numpy as np  # noqa: F401
+import awkward as ak  # noqa: F401
+
+
+@pytest.mark.skipif(
+    ak._util.py27 or ak._util.py35,
+    reason="SOMETIMES doesn't work under Python 2.7 and 3.5",
+)
+def test():
+    out = np.digitize(ak.Array([1.1, 2.2, 3.3, 4.4, 5.5]), ak.Array([0, 1, 3, 6]))
+    assert isinstance(out, ak.Array)
+    assert out.tolist() == [2, 2, 3, 3, 3]
+
+    assert np.median(ak.Array([1.1, 2.2, 3.3, 4.4, 5.5])) == 3.3