Repository: numba/numba

Issue:
Add support for np.clip
## Feature request

Currently `np.clip(a, a_min, a_max, out=None)` and `a.clip(a_min=None, a_max=None, out=None)` are not supported by `@njit`.

I tried to implement this myself, but am having trouble installing `llvm-6.0` in order to test my development branch locally. So, without knowing what I'm doing (first time looking at `numba` source code), this was my blind attempt implementing `np.clip(...)`:

```python
# File: numba/targets/arrayobj.py

@lower_builtin(np.clip, types.Array)
def array_clip(context, builder, sig, args):
    def np_clip_impl(a, a_min, a_max, out=None):
        if out is None:
            out = np.empty_like(a)
        for i in range(len(a)):
            if a[i] < a_min:
                out[i] = a_min
            elif a[i] > a_max:
                out[i] = a_max
            else:
                out[i] = a[i]
        return out

    return context.compile_internal(builder, np_clip_impl, sig, args)
```

Following the pattern in #3462, I'm guessing that something like this is also needed (for `a.clip(...)`):

```python
# File: numba/targets/arrayobj.py

@overload_method(types.Array, 'clip')
def array_clip(a, a_min, a_max, out=None):
    def impl(a, a_min, a_max, out=None):
        return np.clip(a, a_min, a_max, out)
    return impl
```

**Edit**: The above implementation is incomplete since the signature for `a.clip` is:
```
    a.clip(min=None, max=None, out=None)
```
Note the default values, which are not present for `np.clip`.

Furthermore, `None` can be passed to `np.clip` for `a_min` or `a_max`, in which case the lower or upper bounds are ignored, respectively.


Discussion:
Thanks for the request and starting the implementation, your contribution is appreciated. What you have written is really close to working, perhaps open a pull request and we can iterate on it?

-----------------------
> I tried to implement this myself, but am having trouble installing llvm-6.0 in order to test my development branch locally.

Some instructions for getting set up for development are [here](http://numba.pydata.org/numba-doc/latest/developer/contributing.html#getting-set-up) and are a great place to start.

Briefly, you can get away without needing LLVM for this sort of development by just using the `llvmlite` package (all the bits of LLVM needed are statically linked into `llvmlite`'s libraries). `llvmlite` is available via conda (use the Numba channel to get a version that is compatible with Numba mainline):
```
$ conda install -c numba llvmlite 
```
it's also available via `pip` using https://anaconda.org, instructions are on this page:
https://anaconda.org/numba/llvmlite

If you *really* want to build from LLVM source up, then http://llvmlite.pydata.org/en/latest/admin-guide/install.html#building-manually is the instructions to get `llvmlite` built, then switch to the Numba build instructions http://numba.pydata.org/numba-doc/latest/developer/contributing.html#building-numba

-----------------------
To the implementation, the [`numba.extending` high level extension API](http://numba.pydata.org/numba-doc/latest/extending/high-level.html) makes it possible to add functionality like this without having to hit the low level API (`@lower_builtin` and friends).

If you use the `@numba.extending.overload` decorator, creating and registering an implementation of `np.clip` with Numba can be done as follows (this is it, it's now ready to be tested etc!):
```python
@extending.overload(np.clip)
def np_clip(a, a_min, a_max, out=None):
    def np_clip_impl(a, a_min, a_max, out=None):
        if out is None:
            out = np.empty_like(a)
        for i in range(len(a)):
            if a[i] < a_min:
                out[i] = a_min
            elif a[i] > a_max:
                out[i] = a_max
            else:
                out[i] = a[i]
        return out
    return np_clip_impl
```
and indeed, following https://github.com/numba/numba/pull/3462 for the overload of the `np.ndarray` method implementation would provide the functionality noted (obviously need to do some small changes to handle the different default values in the kwargs).

Hope this helps.
Thanks for the words of encouragement! I managed to get my dev branch working after using that conda command, and have made a pull request at #3468.
Brilliant, well done! Thanks for opening a PR, one (or more!) of the core devs will help you through getting it working and merged.
Hi! Any hint if this is going to be released at any time?
Hi, any update on this issue?

Resolving patch:
diff --git a/numba/np/arrayobj.py b/numba/np/arrayobj.py
--- a/numba/np/arrayobj.py
+++ b/numba/np/arrayobj.py
@@ -1774,6 +1774,56 @@ def imp(ary):
     return res
 
 
+@overload(np.clip)
+def np_clip(a, a_min, a_max, out=None):
+    if not type_can_asarray(a):
+        raise errors.TypingError('The argument "a" must be array-like')
+
+    if not isinstance(a_min, (types.NoneType, types.Number)):
+        raise errors.TypingError('The argument "a_min" must be a number')
+
+    if not isinstance(a_max, (types.NoneType, types.Number)):
+        raise errors.TypingError('The argument "a_max" must be a number')
+
+    if not isinstance(out, (types.NoneType, types.Array)):
+        msg = 'The argument "out" must be an array if it is provided'
+        raise errors.TypingError(msg)
+
+    # TODO: support scalar a (issue #3469)
+    a_min_is_none = a_min is None or isinstance(a_min, types.NoneType)
+    a_max_is_none = a_max is None or isinstance(a_max, types.NoneType)
+
+    def np_clip_impl(a, a_min, a_max, out=None):
+        if a_min_is_none and a_max_is_none:
+            raise ValueError("array_clip: must set either max or min")
+
+        ret = np.empty_like(a) if out is None else out
+
+        for index, val in np.ndenumerate(a):
+            if a_min_is_none:
+                if val > a_max:
+                    ret[index] = a_max
+                else:
+                    ret[index] = val
+            elif a_max_is_none:
+                if val < a_min:
+                    ret[index] = a_min
+                else:
+                    ret[index] = val
+            else:
+                ret[index] = min(max(val, a_min), a_max)
+        return ret
+
+    return np_clip_impl
+
+
+@overload_method(types.Array, 'clip')
+def array_clip(a, a_min=None, a_max=None, out=None):
+    def impl(a, a_min=None, a_max=None, out=None):
+        return np.clip(a, a_min, a_max, out)
+    return impl
+
+
 def _change_dtype(context, builder, oldty, newty, ary):
     """
     Attempt to fix up *ary* for switching from *oldty* to *newty*.


Test patch:
diff --git a/numba/tests/test_array_methods.py b/numba/tests/test_array_methods.py
--- a/numba/tests/test_array_methods.py
+++ b/numba/tests/test_array_methods.py
@@ -8,7 +8,7 @@
 from numba.core import types
 from numba.core.compiler import compile_isolated
 from numba.core.errors import TypingError, LoweringError
-from numba.np.numpy_support import as_dtype
+from numba.np.numpy_support import as_dtype, numpy_version
 from numba.tests.support import (TestCase, CompilationCache, MemoryLeak,
                                  MemoryLeakMixin, tag, needs_blas)
 import unittest
@@ -232,6 +232,19 @@ def array_real(a):
 def array_imag(a):
     return np.imag(a)
 
+def np_clip(a, a_min, a_max, out=None):
+    return np.clip(a, a_min, a_max, out)
+
+def np_clip_kwargs(a, a_min, a_max, out=None):
+    return np.clip(a, a_min, a_max, out=out)
+
+def array_clip(a, a_min=None, a_max=None, out=None):
+    return a.clip(a_min, a_max, out)
+
+def array_clip_kwargs(a, a_min=None, a_max=None, out=None):
+    return a.clip(a_min, a_max, out=out)
+
+
 def array_conj(a):
     return a.conj()
 
@@ -1329,6 +1342,61 @@ def test_imag(self):
         z = x + 1j*y
         np.testing.assert_equal(pyfunc(z), cfunc(z))
 
+    def test_clip(self):
+        # TODO: scalars are not tested (issue #3469)
+        for a in (np.linspace(-10, 10, 101),
+                  np.linspace(-10, 10, 40).reshape(5, 2, 4)):
+            for pyfunc in [np_clip, np_clip_kwargs, array_clip,
+                           array_clip_kwargs]:
+                cfunc = jit(nopython=True)(pyfunc)
+
+                msg = "array_clip: must set either max or min"
+                with self.assertRaisesRegex(ValueError, msg):
+                    cfunc(a, None, None)
+
+                np.testing.assert_equal(pyfunc(a, 0, None), cfunc(a, 0, None))
+                np.testing.assert_equal(pyfunc(a, None, 0), cfunc(a, None, 0))
+
+                np.testing.assert_equal(pyfunc(a, -5, 5), cfunc(a, -5, 5))
+
+                pyout = np.empty_like(a)
+                cout = np.empty_like(a)
+                np.testing.assert_equal(pyfunc(a, -5, 5, pyout),
+                                        cfunc(a, -5, 5, cout))
+                np.testing.assert_equal(pyout, cout)
+
+                # verifies that type-inference is working on the return value
+                # this used to trigger issue #3489
+                def lower_clip_result(a):
+                    return np.expm1(cfunc(a, -5, 5))
+                np.testing.assert_almost_equal(
+                    lower_clip_result(a),
+                    jit(nopython=True)(lower_clip_result)(a))
+
+    def test_clip_bad_array(self):
+        cfunc = jit(nopython=True)(np_clip)
+        msg = '.*The argument "a" must be array-like.*'
+        with self.assertRaisesRegex(TypingError, msg):
+            cfunc(None, 0, 10)
+
+    def test_clip_bad_min(self):
+        cfunc = jit(nopython=True)(np_clip)
+        msg = '.*The argument "a_min" must be a number.*'
+        with self.assertRaisesRegex(TypingError, msg):
+            cfunc(1, 'a', 10)
+
+    def test_clip_bad_max(self):
+        cfunc = jit(nopython=True)(np_clip)
+        msg = '.*The argument "a_max" must be a number.*'
+        with self.assertRaisesRegex(TypingError, msg):
+            cfunc(1, 1, 'b')
+
+    def test_clip_bad_out(self):
+        cfunc = jit(nopython=True)(np_clip)
+        msg = '.*The argument "out" must be an array if it is provided.*'
+        with self.assertRaisesRegex(TypingError, msg):
+            cfunc(5, 1, 10, out=6)
+
     def test_conj(self):
         for pyfunc in [array_conj, array_conjugate]:
             cfunc = jit(nopython=True)(pyfunc)