diff --git a/numpy/core/code_generators/generate_umath.py b/numpy/core/code_generators/generate_umath.py index 2241618f7c4e..16149bb8d922 100644 --- a/numpy/core/code_generators/generate_umath.py +++ b/numpy/core/code_generators/generate_umath.py @@ -877,6 +877,12 @@ def english_upper(s): TypeDescription('d', None, 'd', 'di'), TypeDescription('g', None, 'g', 'gi'), ], + ), +'clip' : + Ufunc(3, 1, None, + docstrings.get('numpy.core.umath.clip'), + 'PyUFunc_ClipTypeResolver', + TD(noobj), ) } @@ -910,6 +916,17 @@ def indent(st, spaces): 'G': 'GG_G', 'O': 'OO_O', 'P': 'OO_O_method'} + +chartotype3 = {'e': 'eee_e', + 'f': 'fff_f', + 'd': 'ddd_d', + 'g': 'ggg_g', + 'F': 'FFF_F', + 'D': 'DDD_D', + 'G': 'GGG_G', + 'O': 'OOO_O', + 'P': 'OOO_O_method'} + #for each name # 1) create functions, data, and signature # 2) fill in functions and data in InitOperators @@ -930,9 +947,11 @@ def make_arrays(funcdict): k = 0 sub = 0 - if uf.nin > 1: - assert uf.nin == 2 - thedict = chartotype2 # two inputs and one output + if uf.nin > 2: + assert uf.nin == 3 + thedict = chartotype3 # three inputs and one output + elif uf.nin == 2: + thedict = chartotype2 else: thedict = chartotype1 # one input and one output diff --git a/numpy/core/code_generators/ufunc_docstrings.py b/numpy/core/code_generators/ufunc_docstrings.py index c5172f6a8bcf..a99e65d1a4ac 100644 --- a/numpy/core/code_generators/ufunc_docstrings.py +++ b/numpy/core/code_generators/ufunc_docstrings.py @@ -3669,3 +3669,54 @@ def add_newdoc(place, name, doc): array([ 0., 1., 2., 3., 4., 5.]) """) + +add_newdoc('numpy.core.umath', 'clip', + """ + Clip (limit) the values in an array. + + Given an interval, values outside the interval are clipped to + the interval edges. For example, if an interval of ``[0, 1]`` + is specified, values smaller than 0 become 0, and values larger + than 1 become 1. + + Parameters + ---------- + a : array_like + Array containing elements to clip. + a_min : scalar or array_like + Minimum value. + a_max : scalar or array_like + Maximum value. If `a_min` or `a_max` are array_like, then the + three arrays will be broadcasted to match their shapes. + out : ndarray, optional + The results will be placed in this array. It may be the input + array for in-place clipping. `out` must be of the right shape + to hold the output. Its type is preserved. + + Returns + ------- + clipped_array : ndarray + An array with the elements of `a`, but where values + < `a_min` are replaced with `a_min`, and those > `a_max` + with `a_max`. + + See Also + -------- + numpy.doc.ufuncs : Section "Output arguments" + + Examples + -------- + >>> a = np.arange(10) + >>> np.clip(a, 1, 8) + array([1, 1, 2, 3, 4, 5, 6, 7, 8, 8]) + >>> a + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> np.clip(a, 3, 6, out=a) + array([3, 3, 3, 3, 4, 5, 6, 6, 6, 6]) + >>> a = np.arange(10) + >>> a + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> np.clip(a, [3, 4, 1, 1, 1, 4, 4, 4, 4, 4], 8) + array([3, 4, 2, 3, 4, 5, 6, 7, 8, 8]) + + """) diff --git a/numpy/core/fromnumeric.py b/numpy/core/fromnumeric.py index a8c2fd2fb088..4a6e8cec17e3 100644 --- a/numpy/core/fromnumeric.py +++ b/numpy/core/fromnumeric.py @@ -20,7 +20,7 @@ # functions that are methods __all__ = [ 'alen', 'all', 'alltrue', 'amax', 'amin', 'any', 'argmax', - 'argmin', 'argpartition', 'argsort', 'around', 'choose', 'clip', + 'argmin', 'argpartition', 'argsort', 'around', 'choose', 'compress', 'cumprod', 'cumproduct', 'cumsum', 'diagonal', 'mean', 'ndim', 'nonzero', 'partition', 'prod', 'product', 'ptp', 'put', 'rank', 'ravel', 'repeat', 'reshape', 'resize', 'round_', @@ -1670,63 +1670,6 @@ def compress(condition, a, axis=None, out=None): return _wrapfunc(a, 'compress', condition, axis=axis, out=out) -def clip(a, a_min, a_max, out=None): - """ - Clip (limit) the values in an array. - - Given an interval, values outside the interval are clipped to - the interval edges. For example, if an interval of ``[0, 1]`` - is specified, values smaller than 0 become 0, and values larger - than 1 become 1. - - Parameters - ---------- - a : array_like - Array containing elements to clip. - a_min : scalar or array_like or `None` - Minimum value. If `None`, clipping is not performed on lower - interval edge. Not more than one of `a_min` and `a_max` may be - `None`. - a_max : scalar or array_like or `None` - Maximum value. If `None`, clipping is not performed on upper - interval edge. Not more than one of `a_min` and `a_max` may be - `None`. If `a_min` or `a_max` are array_like, then the three - arrays will be broadcasted to match their shapes. - out : ndarray, optional - The results will be placed in this array. It may be the input - array for in-place clipping. `out` must be of the right shape - to hold the output. Its type is preserved. - - Returns - ------- - clipped_array : ndarray - An array with the elements of `a`, but where values - < `a_min` are replaced with `a_min`, and those > `a_max` - with `a_max`. - - See Also - -------- - numpy.doc.ufuncs : Section "Output arguments" - - Examples - -------- - >>> a = np.arange(10) - >>> np.clip(a, 1, 8) - array([1, 1, 2, 3, 4, 5, 6, 7, 8, 8]) - >>> a - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - >>> np.clip(a, 3, 6, out=a) - array([3, 3, 3, 3, 4, 5, 6, 6, 6, 6]) - >>> a = np.arange(10) - >>> a - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - >>> np.clip(a, [3, 4, 1, 1, 1, 4, 4, 4, 4, 4], 8) - array([3, 4, 2, 3, 4, 5, 6, 7, 8, 8]) - - """ - return _wrapfunc(a, 'clip', a_min, a_max, out=out) - - def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue): """ Sum of array elements over a given axis. diff --git a/numpy/core/src/multiarray/calculation.c b/numpy/core/src/multiarray/calculation.c index 379e5c3d26c6..58a8c0e9f2bf 100644 --- a/numpy/core/src/multiarray/calculation.c +++ b/numpy/core/src/multiarray/calculation.c @@ -891,14 +891,7 @@ _slow_array_clip(PyArrayObject *self, PyObject *min, PyObject *max, PyArrayObjec NPY_NO_EXPORT PyObject * PyArray_Clip(PyArrayObject *self, PyObject *min, PyObject *max, PyArrayObject *out) { - PyArray_FastClipFunc *func; - int outgood = 0, ingood = 0; - PyArrayObject *maxa = NULL; - PyArrayObject *mina = NULL; - PyArrayObject *newout = NULL, *newin = NULL; - PyArray_Descr *indescr = NULL, *newdescr = NULL; - char *max_data, *min_data; - PyObject *zero; + PyObject *res = NULL; /* Treat None the same as NULL */ if (min == Py_None) { @@ -908,262 +901,23 @@ PyArray_Clip(PyArrayObject *self, PyObject *min, PyObject *max, PyArrayObject *o max = NULL; } - if ((max == NULL) && (min == NULL)) { - PyErr_SetString(PyExc_ValueError, - "array_clip: must set either max or min"); - return NULL; - } - - func = PyArray_DESCR(self)->f->fastclip; - if (func == NULL - || (min != NULL && !PyArray_CheckAnyScalar(min)) - || (max != NULL && !PyArray_CheckAnyScalar(max)) - || PyArray_ISBYTESWAPPED(self) - || (out && PyArray_ISBYTESWAPPED(out))) { - return _slow_array_clip(self, min, max, out); - } - /* Use the fast scalar clip function */ - - /* First we need to figure out the correct type */ - if (min != NULL) { - indescr = PyArray_DescrFromObject(min, NULL); - if (indescr == NULL) { - goto fail; - } - } - if (max != NULL) { - newdescr = PyArray_DescrFromObject(max, indescr); - Py_XDECREF(indescr); - indescr = NULL; - if (newdescr == NULL) { - goto fail; - } - } - else { - /* Steal the reference */ - newdescr = indescr; - indescr = NULL; - } - - - /* - * Use the scalar descriptor only if it is of a bigger - * KIND than the input array (and then find the - * type that matches both). - */ - if (PyArray_ScalarKind(newdescr->type_num, NULL) > - PyArray_ScalarKind(PyArray_DESCR(self)->type_num, NULL)) { - indescr = PyArray_PromoteTypes(newdescr, PyArray_DESCR(self)); - if (indescr == NULL) { - goto fail; - } - func = indescr->f->fastclip; - if (func == NULL) { - Py_DECREF(indescr); - return _slow_array_clip(self, min, max, out); - } - } - else { - indescr = PyArray_DESCR(self); - Py_INCREF(indescr); - } - Py_DECREF(newdescr); - newdescr = NULL; - - if (!PyDataType_ISNOTSWAPPED(indescr)) { - PyArray_Descr *descr2; - descr2 = PyArray_DescrNewByteorder(indescr, '='); - Py_DECREF(indescr); - indescr = NULL; - if (descr2 == NULL) { - goto fail; - } - indescr = descr2; - } - - /* Convert max to an array */ - if (max != NULL) { - Py_INCREF(indescr); - maxa = (PyArrayObject *)PyArray_FromAny(max, indescr, 0, 0, - NPY_ARRAY_DEFAULT, NULL); - if (maxa == NULL) { - goto fail; - } - } - - /* - * If we are unsigned, then make sure min is not < 0 - * This is to match the behavior of _slow_array_clip - * - * We allow min and max to go beyond the limits - * for other data-types in which case they - * are interpreted as their modular counterparts. - */ - if (min != NULL) { - if (PyArray_ISUNSIGNED(self)) { - int cmp; - zero = PyInt_FromLong(0); - cmp = PyObject_RichCompareBool(min, zero, Py_LT); - if (cmp == -1) { - Py_DECREF(zero); - goto fail; - } - if (cmp == 1) { - min = zero; - } - else { - Py_DECREF(zero); - Py_INCREF(min); - } - } - else { - Py_INCREF(min); - } - - /* Convert min to an array */ - Py_INCREF(indescr); - mina = (PyArrayObject *)PyArray_FromAny(min, indescr, 0, 0, - NPY_ARRAY_DEFAULT, NULL); - Py_DECREF(min); - if (mina == NULL) { - goto fail; - } - } - - /* - * Check to see if input is single-segment, aligned, - * and in native byteorder - */ - if (PyArray_ISONESEGMENT(self) && - PyArray_CHKFLAGS(self, NPY_ARRAY_ALIGNED) && - PyArray_ISNOTSWAPPED(self) && - (PyArray_DESCR(self) == indescr)) { - ingood = 1; + /* Handle missing arguments */ + if (min == NULL && max == NULL) { + res = (PyObject *)self; + Py_INCREF(res); + return res; } - if (!ingood) { - int flags; - - if (PyArray_ISFORTRAN(self)) { - flags = NPY_ARRAY_FARRAY; - } - else { - flags = NPY_ARRAY_CARRAY; - } - Py_INCREF(indescr); - newin = (PyArrayObject *)PyArray_FromArray(self, indescr, flags); - if (newin == NULL) { - goto fail; - } + if (min == NULL) { + return _GenericBinaryOutFunction(self, max, out, n_ops.minimum); } - else { - newin = self; - Py_INCREF(newin); + if (max == NULL) { + return _GenericBinaryOutFunction(self, min, out, n_ops.maximum); } - /* - * At this point, newin is a single-segment, aligned, and correct - * byte-order array of the correct type - * - * if ingood == 0, then it is a copy, otherwise, - * it is the original input. - */ - - /* - * If we have already made a copy of the data, then use - * that as the output array - */ - if (out == NULL && !ingood) { - out = newin; - } - - /* - * Now, we know newin is a usable array for fastclip, - * we need to make sure the output array is available - * and usable - */ if (out == NULL) { - Py_INCREF(indescr); - out = (PyArrayObject*)PyArray_NewFromDescr(Py_TYPE(self), - indescr, PyArray_NDIM(self), - PyArray_DIMS(self), - NULL, NULL, - PyArray_ISFORTRAN(self), - (PyObject *)self); - if (out == NULL) { - goto fail; - } - - outgood = 1; - } - else Py_INCREF(out); - /* Input is good at this point */ - if (out == newin) { - outgood = 1; - } - if (!outgood && PyArray_ISONESEGMENT(out) && - PyArray_CHKFLAGS(out, NPY_ARRAY_ALIGNED) && - PyArray_ISNOTSWAPPED(out) && - PyArray_EquivTypes(PyArray_DESCR(out), indescr)) { - outgood = 1; - } - - /* - * Do we still not have a suitable output array? - * Create one, now - */ - if (!outgood) { - int oflags; - if (PyArray_ISFORTRAN(out)) - oflags = NPY_ARRAY_FARRAY; - else - oflags = NPY_ARRAY_CARRAY; - oflags |= NPY_ARRAY_UPDATEIFCOPY | NPY_ARRAY_FORCECAST; - Py_INCREF(indescr); - newout = (PyArrayObject*)PyArray_FromArray(out, indescr, oflags); - if (newout == NULL) { - goto fail; - } - } - else { - newout = out; - Py_INCREF(newout); - } - - /* make sure the shape of the output array is the same */ - if (!PyArray_SAMESHAPE(newin, newout)) { - PyErr_SetString(PyExc_ValueError, "clip: Output array must have the" - "same shape as the input."); - goto fail; - } - - /* Now we can call the fast-clip function */ - min_data = max_data = NULL; - if (mina != NULL) { - min_data = PyArray_DATA(mina); + return PyObject_CallFunction(n_ops.clip, "OOO", self, min, max); } - if (maxa != NULL) { - max_data = PyArray_DATA(maxa); - } - func(PyArray_DATA(newin), PyArray_SIZE(newin), min_data, max_data, PyArray_DATA(newout)); - - /* Clean up temporary variables */ - Py_XDECREF(indescr); - Py_XDECREF(newdescr); - Py_XDECREF(mina); - Py_XDECREF(maxa); - Py_DECREF(newin); - /* Copy back into out if out was not already a nice array. */ - Py_DECREF(newout); - return (PyObject *)out; - - fail: - Py_XDECREF(indescr); - Py_XDECREF(newdescr); - Py_XDECREF(maxa); - Py_XDECREF(mina); - Py_XDECREF(newin); - PyArray_XDECREF_ERR(newout); - return NULL; + return PyObject_CallFunction(n_ops.clip, "OOOO", self, min, max, out); } diff --git a/numpy/core/src/multiarray/number.c b/numpy/core/src/multiarray/number.c index 9c1343497db3..99df096fe8d6 100644 --- a/numpy/core/src/multiarray/number.c +++ b/numpy/core/src/multiarray/number.c @@ -115,6 +115,7 @@ PyArray_SetNumericOps(PyObject *dict) SET(minimum); SET(rint); SET(conjugate); + SET(clip); return 0; } @@ -168,6 +169,7 @@ PyArray_GetNumericOps(void) GET(minimum); GET(rint); GET(conjugate); + GET(clip); return dict; fail: diff --git a/numpy/core/src/multiarray/number.h b/numpy/core/src/multiarray/number.h index 99a2a722b6b9..869a03b972f1 100644 --- a/numpy/core/src/multiarray/number.h +++ b/numpy/core/src/multiarray/number.h @@ -39,6 +39,7 @@ typedef struct { PyObject *minimum; PyObject *rint; PyObject *conjugate; + PyObject *clip; } NumericOps; extern NPY_NO_EXPORT NumericOps n_ops; diff --git a/numpy/core/src/umath/loops.c.src b/numpy/core/src/umath/loops.c.src index 40ebc119aefe..c37987dbad6b 100644 --- a/numpy/core/src/umath/loops.c.src +++ b/numpy/core/src/umath/loops.c.src @@ -2874,3 +2874,131 @@ OBJECT_sign(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED ** END LOOPS ** ***************************************************************************** */ + + + +/* + ***************************************************************************** + ** CLIP LOOPS ** + ***************************************************************************** + */ + + + +#define TERNARY_LOOP\ + char *ip1 = args[0], *ip2 = args[1], *ip3 = args[2], *op1 = args[3];\ + npy_intp is1 = steps[0], is2 = steps[1], is3 = steps[2], os1 = steps[3];\ + npy_intp n = dimensions[0];\ + npy_intp i;\ + for(i = 0; i < n; i++, ip1 += is1, ip2 += is2, ip3 += is3, op1 += os1) + +#define _LESS_THAN(a, b) ((a) < (b)) +#define _GREATER_THAN(a, b) ((a) > (b)) + +/* + * In fastclip, 'b' was already checked for NaN, so the half comparison + * only needs to check 'a' for NaN. + */ + +#define _HALF_LESS_THAN(a, b) (!npy_half_isnan(a) && npy_half_lt_nonan(a, b)) +#define _HALF_GREATER_THAN(a, b) (!npy_half_isnan(a) && npy_half_lt_nonan(b, a)) + +/**begin repeat + * + * #TYPE = BOOL, + * BYTE, UBYTE, SHORT, USHORT, INT, UINT, + * LONG, ULONG, LONGLONG, ULONGLONG, + * HALF, FLOAT, DOUBLE, LONGDOUBLE, + * DATETIME, TIMEDELTA# + * #type = npy_bool, + * npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint, + * npy_long, npy_ulong, npy_longlong, npy_ulonglong, + * npy_half, npy_float, npy_double, npy_longdouble, + * npy_datetime, npy_timedelta# + * #isfloat = 0*11, 1*4, 0*2# + * #isnan = nop*11, npy_half_isnan, npy_isnan*3, nop*2# + * #lt = _LESS_THAN*11, _HALF_LESS_THAN, _LESS_THAN*5# + * #gt = _GREATER_THAN*11, _HALF_GREATER_THAN, _GREATER_THAN*5# + */ + +NPY_NO_EXPORT void +@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)) +{ + TERNARY_LOOP { + const @type@ in = *((@type@ *)ip1); + const @type@ min = *((@type@ *)ip2); + const @type@ max = *((@type@ *)ip3); + if (@lt@(in, min)) { + *((@type@ *)op1) = min; + } + else if (@gt@(in, max)) { + *((@type@ *)op1) = max; + } + else { + *((@type@ *)op1) = in; + } + } +} + +/**end repeat**/ + +#undef _LESS_THAN +#undef _GREATER_THAN +#undef _HALF_LESS_THAN +#undef _HALF_GREATER_THAN + + +#define CGE(xr,xi,yr,yi) ((xr > yr && !npy_isnan(xi) && !npy_isnan(yi)) \ + || (xr == yr && xi >= yi)) +#define CLE(xr,xi,yr,yi) ((xr < yr && !npy_isnan(xi) && !npy_isnan(yi)) \ + || (xr == yr && xi <= yi)) +#define CGT(xr,xi,yr,yi) ((xr > yr && !npy_isnan(xi) && !npy_isnan(yi)) \ + || (xr == yr && xi > yi)) +#define CLT(xr,xi,yr,yi) ((xr < yr && !npy_isnan(xi) && !npy_isnan(yi)) \ + || (xr == yr && xi < yi)) +#define CEQ(xr,xi,yr,yi) (xr == yr && xi == yi) +#define CNE(xr,xi,yr,yi) (xr != yr || xi != yi) + + +/**begin repeat + * complex types + * #TYPE = CFLOAT, CDOUBLE, CLONGDOUBLE# + * #ftype = npy_float, npy_double, npy_longdouble# + * #c = f, , l# + * #C = F, , L# + */ + +NPY_NO_EXPORT void +@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)) +{ + TERNARY_LOOP { + const @ftype@ inr = ((@ftype@ *)ip1)[0]; + const @ftype@ ini = ((@ftype@ *)ip1)[1]; + const @ftype@ minr = ((@ftype@ *)ip2)[0]; + const @ftype@ mini = ((@ftype@ *)ip2)[1]; + const @ftype@ maxr = ((@ftype@ *)ip3)[0]; + const @ftype@ maxi = ((@ftype@ *)ip3)[1]; + + if (CLT(inr, ini, minr, mini)) { + ((@ftype@ *)op1)[0] = minr; + ((@ftype@ *)op1)[1] = mini; + } + else if (CGT(inr, ini, maxr, maxi)) { + ((@ftype@ *)op1)[0] = maxr; + ((@ftype@ *)op1)[1] = maxi; + } + else { + ((@ftype@ *)op1)[0] = inr; + ((@ftype@ *)op1)[1] = ini; + } + } +} + + +/**end repeat**/ +#undef CGE +#undef CLE +#undef CGT +#undef CLT +#undef CEQ +#undef CNE diff --git a/numpy/core/src/umath/loops.h.src b/numpy/core/src/umath/loops.h.src index 4243c6522683..e264aa2b6e4f 100644 --- a/numpy/core/src/umath/loops.h.src +++ b/numpy/core/src/umath/loops.h.src @@ -29,7 +29,7 @@ /**begin repeat * #kind = equal, not_equal, greater, greater_equal, less, less_equal, - * logical_and, logical_or, absolute, logical_not# + * logical_and, logical_or, absolute, logical_not, clip# **/ NPY_NO_EXPORT void BOOL_@kind@(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); @@ -143,6 +143,9 @@ NPY_NO_EXPORT void NPY_NO_EXPORT void @S@@TYPE@_divmod(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); +NPY_NO_EXPORT void +@S@@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); + /**end repeat1**/ /**end repeat**/ @@ -262,6 +265,9 @@ NPY_NO_EXPORT void NPY_NO_EXPORT void @TYPE@_ldexp_long(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); +NPY_NO_EXPORT void +@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); + #define @TYPE@_true_divide @TYPE@_divide /**end repeat**/ @@ -374,6 +380,10 @@ NPY_NO_EXPORT void C@TYPE@_@kind@(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); /**end repeat1**/ + +NPY_NO_EXPORT void +C@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); + #define C@TYPE@_true_divide C@TYPE@_divide /**end repeat**/ @@ -429,6 +439,8 @@ NPY_NO_EXPORT void @TYPE@_@kind@(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); /**end repeat1**/ +NPY_NO_EXPORT void +@TYPE@_clip(char **args, npy_intp *dimensions, npy_intp *steps, void *NPY_UNUSED(func)); /**end repeat**/ NPY_NO_EXPORT void diff --git a/numpy/core/src/umath/ufunc_type_resolution.c b/numpy/core/src/umath/ufunc_type_resolution.c index 0fd3c45c5d36..66dba3fac191 100644 --- a/numpy/core/src/umath/ufunc_type_resolution.c +++ b/numpy/core/src/umath/ufunc_type_resolution.c @@ -511,6 +511,132 @@ PyUFunc_SimpleBinaryOperationTypeResolver(PyUFuncObject *ufunc, return 0; } + + +/* + * This function applies special type resolution rules for the case + * where all the functions have the pattern XXX->X, using + * PyArray_ResultType instead of a linear search to get the best + * loop. + * + * Note that a simpler linear search through the functions loop + * is still done, but switching to a simple array lookup for + * built-in types would be better at some point. + * + * Returns 0 on success, -1 on error. + */ +NPY_NO_EXPORT int +PyUFunc_SimpleTernaryOperationTypeResolver(PyUFuncObject *ufunc, + NPY_CASTING casting, + PyArrayObject **operands, + PyObject *type_tup, + PyArray_Descr **out_dtypes) +{ + int i, type_num1, type_num2, type_num3; + const char *ufunc_name; + + ufunc_name = ufunc->name ? ufunc->name : ""; + + if (ufunc->nin != 3 || ufunc->nout != 1) { + PyErr_Format(PyExc_RuntimeError, "ufunc %s is configured " + "to use ternary operation type resolution but has " + "the wrong number of inputs or outputs", + ufunc_name); + return -1; + } + + /* + * Use the default type resolution if there's a custom data type + * or object arrays. + */ + type_num1 = PyArray_DESCR(operands[0])->type_num; + type_num2 = PyArray_DESCR(operands[1])->type_num; + type_num3 = PyArray_DESCR(operands[2])->type_num; + if (type_num1 >= NPY_NTYPES || type_num2 >= NPY_NTYPES || + type_num3 >= NPY_NTYPES || type_num1 == NPY_OBJECT || + type_num2 == NPY_OBJECT || type_num3 == NPY_OBJECT) { + return PyUFunc_DefaultTypeResolver(ufunc, casting, operands, + type_tup, out_dtypes); + } + + if (type_tup == NULL) { + /* Input types are the result type */ + out_dtypes[0] = PyArray_ResultType(3, operands, 0, NULL); + if (out_dtypes[0] == NULL) { + return -1; + } + out_dtypes[1] = out_dtypes[0]; + Py_INCREF(out_dtypes[1]); + out_dtypes[2] = out_dtypes[0]; + Py_INCREF(out_dtypes[2]); + out_dtypes[3] = out_dtypes[0]; + Py_INCREF(out_dtypes[3]); + } + else { + PyObject *item; + PyArray_Descr *dtype = NULL; + + /* + * If the type tuple isn't a single-element tuple, let the + * default type resolution handle this one. + */ + if (!PyTuple_Check(type_tup) || PyTuple_GET_SIZE(type_tup) != 1) { + return PyUFunc_DefaultTypeResolver(ufunc, casting, + operands, type_tup, out_dtypes); + } + + item = PyTuple_GET_ITEM(type_tup, 0); + + if (item == Py_None) { + PyErr_SetString(PyExc_ValueError, + "require data type in the type tuple"); + return -1; + } + else if (!PyArray_DescrConverter(item, &dtype)) { + return -1; + } + + out_dtypes[0] = ensure_dtype_nbo(dtype); + if (out_dtypes[0] == NULL) { + return -1; + } + out_dtypes[1] = out_dtypes[0]; + Py_INCREF(out_dtypes[1]); + out_dtypes[2] = out_dtypes[0]; + Py_INCREF(out_dtypes[2]); + out_dtypes[3] = out_dtypes[0]; + Py_INCREF(out_dtypes[3]); + } + + /* Check against the casting rules */ + if (PyUFunc_ValidateCasting(ufunc, casting, operands, out_dtypes) < 0) { + for (i = 0; i < 4; ++i) { + Py_DECREF(out_dtypes[i]); + out_dtypes[i] = NULL; + } + return -1; + } + + return 0; +} + +/* + * This function applies special type resolution rules for the clip + * ufunc. For legacy reasons, allow float -> int. + * + * Returns 0 on success, -1 on error. + */ +NPY_NO_EXPORT int +PyUFunc_ClipTypeResolver(PyUFuncObject *ufunc, + NPY_CASTING casting, + PyArrayObject **operands, + PyObject *type_tup, + PyArray_Descr **out_dtypes) +{ + casting = NPY_UNSAFE_CASTING; + return PyUFunc_SimpleTernaryOperationTypeResolver(ufunc, casting, operands, type_tup, out_dtypes); +} + /* * This function applies special type resolution rules for the absolute * ufunc. This ufunc converts complex -> float, so isn't covered diff --git a/numpy/core/src/umath/ufunc_type_resolution.h b/numpy/core/src/umath/ufunc_type_resolution.h index eaf5e91cec67..1f81b8f4b777 100644 --- a/numpy/core/src/umath/ufunc_type_resolution.h +++ b/numpy/core/src/umath/ufunc_type_resolution.h @@ -36,6 +36,13 @@ PyUFunc_SimpleBinaryOperationTypeResolver(PyUFuncObject *ufunc, PyObject *type_tup, PyArray_Descr **out_dtypes); +NPY_NO_EXPORT int +PyUFunc_SimpleTernaryOperationTypeResolver(PyUFuncObject *ufunc, + NPY_CASTING casting, + PyArrayObject **operands, + PyObject *type_tup, + PyArray_Descr **out_dtypes); + NPY_NO_EXPORT int PyUFunc_AbsoluteTypeResolver(PyUFuncObject *ufunc, NPY_CASTING casting, @@ -85,6 +92,13 @@ PyUFunc_DivisionTypeResolver(PyUFuncObject *ufunc, PyObject *type_tup, PyArray_Descr **out_dtypes); +NPY_NO_EXPORT int +PyUFunc_ClipTypeResolver(PyUFuncObject *ufunc, + NPY_CASTING casting, + PyArrayObject **operands, + PyObject *type_tup, + PyArray_Descr **out_dtypes); + /* * Does a linear search for the best inner loop of the ufunc. * diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py index 0f87ffdf2a5f..385770fde85e 100644 --- a/numpy/core/tests/test_numeric.py +++ b/numpy/core/tests/test_numeric.py @@ -1695,11 +1695,43 @@ def test_clip_func_takes_out(self): def test_clip_nan(self): d = np.arange(7.) - assert_equal(d.clip(min=np.nan), d) - assert_equal(d.clip(max=np.nan), d) - assert_equal(d.clip(min=np.nan, max=np.nan), d) - assert_equal(d.clip(min=-2, max=np.nan), d) - assert_equal(d.clip(min=np.nan, max=10), d) + assert_array_equal(d.clip(min=np.nan), d) + assert_array_equal(d.clip(max=np.nan), d) + assert_array_equal(d.clip(min=np.nan, max=np.nan), d) + assert_array_equal(d.clip(min=-2, max=np.nan), d) + assert_array_equal(d.clip(min=np.nan, max=10), d) + + def test_clip_nan2(self): + a = np.array([np.NaN]) + assert_array_equal(a.clip(min=2), a) + assert_array_equal(a.clip(max=2), a) + assert_array_equal(a.clip(min=2, max=2), a) + + def test_clip_with_out_order_c_to_f(self): + # Ensure that the clip() function takes an out with different order from input. + a = self._generate_data(self.nr, self.nc) + out = a.copy(order='F') + out2 = a.copy(order='F') + m = -0.5 + M = 0.6 + self.fastclip(a, m, M, out=out) + self.clip(a, m, M, out=out2) + + assert_array_strict_equal(out, out2) + self.assertTrue(np.isfortran(out)) + + def test_clip_with_out_order_f_to_c(self): + # Ensure that the clip() function takes an out with different order from input. + a = self._generate_data(self.nr, self.nc).copy(order='F') + out = a.copy(order='C') + out2 = a.copy(order='C') + m = -0.5 + M = 0.6 + self.fastclip(a, m, M, out=out) + self.clip(a, m, M, out=out2) + + assert_array_strict_equal(out, out2) + self.assertFalse(np.isfortran(out)) class TestAllclose(object):