Repository: yt-project/unyt (python)

Issue:
numpy.clip is now a ufunc which unit doesn't know about
* unyt version: 2.3.0
* Python version: 3.7
* NumPy version: 1.17
* Operating System: macOS

### Description

In NumPy 1.17 they made `numpy.clip` a ufunc which has 3 inputs. Currently, we do not support any ufuncs with 3 inputs. 

Currently this breaks volume rendering tasks which use sigma clipping in yt if NumPy 1.17 is installed. See https://github.com/yt-project/yt/issues/2313

### Code for reproduction

```python
import yt
import numpy as np
a = yt.YTArray(np.random.uniform(size=10), "cm")
a.clip(0.4,  0.6)
```

gives:

```pycon
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-1-335caf0515a5> in <module>
      2 import numpy as np
      3 a = yt.YTArray(np.random.uniform(size=10), "cm")
----> 4 a.clip(0.4,  0.6)

~/miniconda3/lib/python3.7/site-packages/numpy/core/_methods.py in _clip(a, min, max, out, casting, **kwargs)
    130     else:
    131         return _clip_dep_invoke_with_casting(
--> 132             um.clip, a, min, max, out=out, casting=casting, **kwargs)
    133
    134 def _mean(a, axis=None, dtype=None, out=None, keepdims=False):

~/miniconda3/lib/python3.7/site-packages/numpy/core/_methods.py in _clip_dep_invoke_with_casting(ufunc, out, casting, *args, **kwargs)
     83     # try to deal with broken casting rules
     84     try:
---> 85         return ufunc(*args, out=out, **kwargs)
     86     except _exceptions._UFuncOutputCastingError as e:
     87         # Numpy 1.17.0, 2019-02-24

~/Source/unyt/unyt/array.py in __array_ufunc__(self, ufunc, method, *inputs, **kwargs)
   1707             raise RuntimeError(
   1708                 "Support for the %s ufunc with %i inputs has not been"
-> 1709                 "added to unyt_array." % (str(ufunc), len(inputs))
   1710             )
   1711         if unit is None:

RuntimeError: Support for the <ufunc 'clip'> ufunc with 3 inputs has not been added to unyt_array.
```

Pull request description:
add support for the clip ufunc and add testing for np.clip, closes #101
This should probably get backported to yt.units, ping @matthewturk.

Patch:
diff --git a/tox.ini b/tox.ini
index 6350941..56ee095 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ setenv =
     PYTHONPATH = {toxinidir}
 depends = begin
 deps =
-    pytest
+    pytest<5.1
     sympy
     numpy
     h5py
@@ -30,7 +30,7 @@ commands =
 
 [testenv:py35-versions]
 deps =
-    pytest
+    pytest<5.1
     sympy==1.2
     numpy==1.13.3
     h5py==2.6.0
@@ -46,7 +46,7 @@ commands =
 
 [testenv:py35-dependencies]
 deps =
-    pytest
+    pytest<5.1
     sympy
     numpy
     coverage
diff --git a/unyt/array.py b/unyt/array.py
index d601e85..ac7e643 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -106,6 +106,11 @@ from numpy import (
     matmul,
 )
 from numpy.core.umath import _ones_like
+
+try:
+    from numpy.core.umath import clip
+except ImportError:
+    clip = None
 from sympy import Rational
 import warnings
 
@@ -457,6 +462,7 @@ class unyt_array(np.ndarray):
         heaviside: _preserve_units,
         _ones_like: _preserve_units,
         matmul: _multiply_units,
+        clip: _passthrough_unit,
     }
 
     __array_priority__ = 2.0
@@ -1705,10 +1711,21 @@ class unyt_array(np.ndarray):
                         "cannot by multiplied, divide, subtracted or added."
                     )
         else:
-            raise RuntimeError(
-                "Support for the %s ufunc with %i inputs has not been"
-                "added to unyt_array." % (str(ufunc), len(inputs))
-            )
+            if ufunc is clip:
+                inp = (inputs[0].view(np.ndarray), inputs[1], inputs[2])
+                if out is not None:
+                    _out = out.view(np.ndarray)
+                else:
+                    _out = None
+                out_arr = ufunc(*inp, out=_out)
+                unit = inputs[0].units
+                ret_class = type(inputs[0])
+                mul = 1
+            else:
+                raise RuntimeError(
+                    "Support for the %s ufunc with %i inputs has not been "
+                    "added to unyt_array." % (str(ufunc), len(inputs))
+                )
         if unit is None:
             out_arr = np.array(out_arr, copy=False)
         elif ufunc in (modf, divmod_):


Test patch:
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index e588c5a..18b0f09 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -2264,3 +2264,19 @@ def test_input_units_deprecation():
             unyt_array([1, 2, 3], "mile"), unyt_array([1, 2, 3], input_units="mile")
         )
         assert unyt_quantity(3, "mile") == unyt_quantity(3, input_units="mile")
+
+
+def test_clip():
+    from unyt import km
+
+    data = [1, 2, 3, 4, 5, 6] * km
+    answer = [2, 2, 3, 4, 4, 4] * km
+
+    ret = np.clip(data, 2, 4)
+    assert_array_equal(ret, answer)
+    assert ret.units == answer.units
+
+    np.clip(data, 2, 4, out=data)
+
+    assert_array_equal(data, answer)
+    assert data.units == answer.units