From 7a09080a87fa9d2a2b9b0f5ca91abe2701d4f971 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Mon, 30 Apr 2018 11:52:16 -0400 Subject: [PATCH 1/5] ENH: add "axis" argument to generalized ufuncs. It is only allowed for gufuncs with a single, shared core dimension. --- doc/scipy-sphinx-theme | 2 +- numpy/core/src/umath/ufunc_object.c | 123 +++++++++++++++++++++++++--- numpy/core/tests/test_ufunc.py | 44 +++++++++- 3 files changed, 154 insertions(+), 15 deletions(-) diff --git a/doc/scipy-sphinx-theme b/doc/scipy-sphinx-theme index d990ab913419..c466764e2231 160000 --- a/doc/scipy-sphinx-theme +++ b/doc/scipy-sphinx-theme @@ -1 +1 @@ -Subproject commit d990ab9134199f6496b9ac8567f10791f04a720a +Subproject commit c466764e2231ba132c09826b5b138fffa1cfcec3 diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index aacf3f780c99..c77087751532 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -565,11 +565,12 @@ get_ufunc_arguments(PyUFuncObject *ufunc, NPY_ORDER *out_order, NPY_CASTING *out_casting, PyObject **out_extobj, - PyObject **out_typetup, - int *out_subok, - PyArrayObject **out_wheremask, - PyObject **out_axes, - int *out_keepdims) + PyObject **out_typetup, /* type: Tuple[np.dtype] */ + int *out_subok, /* bool */ + PyArrayObject **out_wheremask, /* PyArray of bool */ + PyObject **out_axes, /* type: List[Tuple[T]] */ + PyObject **out_axis, /* type: T */ + int *out_keepdims) /* bool */ { int i, nargs; int nin = ufunc->nin; @@ -826,8 +827,21 @@ get_ufunc_arguments(PyUFuncObject *ufunc, case 'a': /* possible axes argument for generalized ufunc */ if (out_axes != NULL && strcmp(str, "axes") == 0) { + if (out_axis != NULL && *out_axis != NULL) { + PyErr_SetString(PyExc_RuntimeError, + "cannot specify both 'axis' and 'axes'"); + goto fail; + } *out_axes = value; - + bad_arg = 0; + } + else if (out_axis != NULL && strcmp(str, "axis") == 0) { + if (out_axes != NULL && *out_axes != NULL) { + PyErr_SetString(PyExc_RuntimeError, + "cannot specify both 'axis' and 'axes'"); + goto fail; + } + *out_axis = value; bad_arg = 0; } break; @@ -1884,6 +1898,27 @@ _has_output_coredims(PyUFuncObject *ufunc) { return 0; } +/* + * Check whether the gufunc can be used with axis, i.e., that there is only + * a single, shared core dimension (which means that operands either have + * that dimension, or have no core dimensions). Returns 0 if all is fine, + * and sets an error and returns -1 if not. + */ +static int +_check_axis_support(PyUFuncObject *ufunc) { + if (ufunc->core_num_dim_ix != 1) { + PyErr_Format(PyExc_TypeError, + "%s: axis can only be used with a single shared core " + "dimension, not with the %d distinct ones implied by " + "signature %s.", + ufunc_get_name_cstr(ufunc), + ufunc->core_num_dim_ix, + ufunc->core_signature); + return -1; + } + return 0; +} + /* * Check whether the gufunc can be used with keepdims, i.e., that all its * input arguments have the same number of core dimension, and all output @@ -1899,7 +1934,7 @@ _check_keepdims_support(PyUFuncObject *ufunc) { if (ufunc->core_num_dims[i] != (i < nin ? input_core_dims : 0)) { PyErr_Format(PyExc_TypeError, "%s does not support keepdims: its signature %s requires " - "that %s %d has %d core dimensions, but keepdims can only " + "%s %d to have %d core dimensions, but keepdims can only " "be used when all inputs have the same number of core " "dimensions and all outputs have no core dimensions.", ufunc_get_name_cstr(ufunc), @@ -1913,6 +1948,42 @@ _check_keepdims_support(PyUFuncObject *ufunc) { return 0; } +/* + * Translate axis to axes list of the form [(axis,), ...], with an + * empty tuple for operands without core dimensions. + * Returns an axes tuple or NULL on failure. + */ +static PyObject* +_build_axes_tuple_from_axis(PyObject *axis, int core_num_dims[], int nop) { + int i; + PyObject *axes = NULL, *axis_tuple = NULL, *tuple; + PyObject *empty_tuple = PyTuple_New(0); /* cannot realistically fail */ + + axes = PyList_New(nop); + if (axes == NULL) { + return NULL; + } + axis_tuple = PyTuple_Pack(1, axis); + if (axis_tuple == NULL) { + goto fail; + } + + for (i = 0; i < nop; i++) { + tuple = core_num_dims[i] == 1 ? axis_tuple : empty_tuple; + Py_INCREF(tuple); + PyList_SET_ITEM(axes, i, tuple); + } + Py_DECREF(axis_tuple); + Py_DECREF(empty_tuple); + return axes; + +fail: + Py_XDECREF(axis_tuple); + Py_XDECREF(axes); + Py_DECREF(empty_tuple); + return NULL; +} + /* * Interpret a possible axes keyword argument, using it to fill the remap_axis * array which maps default to actual axes for each operand, indexed as @@ -2239,8 +2310,8 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, NPY_ORDER order = NPY_KEEPORDER; /* Use the default assignment casting rule */ NPY_CASTING casting = NPY_DEFAULT_ASSIGN_CASTING; - /* When provided, extobj, typetup, and axes contain borrowed references */ - PyObject *extobj = NULL, *type_tup = NULL, *axes = NULL; + /* other possible keyword arguments */ + PyObject *extobj = NULL, *type_tup = NULL, *axes = NULL, *axis = NULL; int keepdims = -1; if (ufunc == NULL) { @@ -2265,10 +2336,17 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, NPY_UF_DBG_PRINT("Getting arguments\n"); - /* Get all the arguments */ + /* + * Get all the arguments. + * + * Here, when provided, extobj, typetup, axes, and axis contain borrowed + * references; axes, however, may be set internally (from axis), and thus + * is XDECREF'd at the end. Hence, if passed in, we get our own reference. + */ retval = get_ufunc_arguments(ufunc, args, kwds, op, &order, &casting, &extobj, - &type_tup, &subok, NULL, &axes, &keepdims); + &type_tup, &subok, NULL, &axes, &axis, &keepdims); + Py_XINCREF(axes); if (retval < 0) { goto fail; } @@ -2283,6 +2361,12 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, goto fail; } } + if (axis != NULL) { + retval = _check_axis_support(ufunc); + if (retval < 0) { + goto fail; + } + } /* * If keepdims is set and true, signal all dimensions will be the same. */ @@ -2350,6 +2434,19 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, goto fail; } + /* + * Translate axis to axes list of the form [(axis,), ...], with an + * empty tuple for operands without core dimensions. + */ + if (axis) { + assert(axes == NULL); /* parser prevents passing in both axis & axes */ + axes = _build_axes_tuple_from_axis(axis, core_num_dims, nop); + if (axes == NULL) { + retval = -1; + goto fail; + } + } + /* Possibly remap axes. */ if (axes) { remap_axis = PyArray_malloc(sizeof(remap_axis[0]) * nop); @@ -2709,6 +2806,7 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, Py_XDECREF(type_tup); Py_XDECREF(full_args.in); Py_XDECREF(full_args.out); + Py_XDECREF(axes); NPY_UF_DBG_PRINT("Returning Success\n"); @@ -2728,6 +2826,7 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, Py_XDECREF(type_tup); Py_XDECREF(full_args.in); Py_XDECREF(full_args.out); + Py_XDECREF(axes); PyArray_free(remap_axis_memory); PyArray_free(remap_axis); return retval; @@ -2804,7 +2903,7 @@ PyUFunc_GenericFunction(PyUFuncObject *ufunc, /* Get all the arguments */ retval = get_ufunc_arguments(ufunc, args, kwds, op, &order, &casting, &extobj, - &type_tup, &subok, &wheremask, NULL, NULL); + &type_tup, &subok, &wheremask, NULL, NULL, NULL); if (retval < 0) { goto fail; } diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py index b7fda3f2e093..b0bf57a45f1c 100644 --- a/numpy/core/tests/test_ufunc.py +++ b/numpy/core/tests/test_ufunc.py @@ -718,6 +718,36 @@ def test_axes_argument(self): assert_raises(ValueError, mm, z[1], z, axes=[0, 1]) assert_raises(ValueError, mm, z, z, out=z[0], axes=[0, 1]) + def test_axis_argument(self): + # inner1d signature: '(i),(i)->()' + inner1d = umt.inner1d + a = np.arange(27.).reshape((3, 3, 3)) + b = np.arange(10., 19.).reshape((3, 1, 3)) + c = inner1d(a, b) + assert_array_equal(c, (a * b).sum(-1)) + c = inner1d(a, b, axis=-1) + assert_array_equal(c, (a * b).sum(-1)) + out = np.zeros_like(c) + d = inner1d(a, b, axis=-1, out=out) + assert_(d is out) + assert_array_equal(d, c) + c = inner1d(a, b, axis=0) + assert_array_equal(c, (a * b).sum(0)) + # Sanity check on innerwt. + a = np.arange(6).reshape((2, 3)) + b = np.arange(10, 16).reshape((2, 3)) + w = np.arange(20, 26).reshape((2, 3)) + assert_array_equal(umt.innerwt(a, b, w, axis=0), + np.sum(a * b * w, axis=0)) + # Check errors. + # Cannot pass in both axis and axes. + assert_raises(RuntimeError, inner1d, a, b, axis=0, axes=[0, 0]) + # Not an integer. + assert_raises(TypeError, inner1d, a, b, axis=[0]) + # more than 1 core dimensions. + mm = umt.matrix_multiply + assert_raises(TypeError, mm, a, b, axis=1) + def test_keepdims_argument(self): # inner1d signature: '(i),(i)->()' inner1d = umt.inner1d @@ -733,7 +763,15 @@ def test_keepdims_argument(self): d = inner1d(a, b, keepdims=True, out=out) assert_(d is out) assert_array_equal(d, c) - # Now combined with axes. + # Now combined with axis and axes. + c = inner1d(a, b, axis=-1, keepdims=False) + assert_array_equal(c, (a * b).sum(-1, keepdims=False)) + c = inner1d(a, b, axis=-1, keepdims=True) + assert_array_equal(c, (a * b).sum(-1, keepdims=True)) + c = inner1d(a, b, axis=0, keepdims=False) + assert_array_equal(c, (a * b).sum(0, keepdims=False)) + c = inner1d(a, b, axis=0, keepdims=True) + assert_array_equal(c, (a * b).sum(0, keepdims=True)) c = inner1d(a, b, axes=[(-1,), (-1,), ()], keepdims=False) assert_array_equal(c, (a * b).sum(-1)) c = inner1d(a, b, axes=[(-1,), (-1,), (-1,)], keepdims=True) @@ -777,10 +815,12 @@ def test_keepdims_argument(self): w = np.arange(20, 26).reshape((2, 3)) assert_array_equal(umt.innerwt(a, b, w, keepdims=True), np.sum(a * b * w, axis=-1, keepdims=True)) + assert_array_equal(umt.innerwt(a, b, w, axis=0, keepdims=True), + np.sum(a * b * w, axis=0, keepdims=True)) # Check errors. # Not a boolean assert_raises(TypeError, inner1d, a, b, keepdims='true') - # 1 core dimension only. + # More than 1 core dimension, and core output dimensions. mm = umt.matrix_multiply assert_raises(TypeError, mm, a, b, keepdims=True) assert_raises(TypeError, mm, a, b, keepdims=False) From e8b64c1a2e883c58185846449d02afc2f21bb6e1 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Mon, 30 Apr 2018 12:32:26 -0400 Subject: [PATCH 2/5] DOC: Describe new axis argument. Both in the general documentation and in the release notes. --- doc/release/1.15.0-notes.rst | 11 +++++++++-- doc/source/reference/ufuncs.rst | 13 ++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/doc/release/1.15.0-notes.rst b/doc/release/1.15.0-notes.rst index e8465d21cdd6..e3069a99c5cf 100644 --- a/doc/release/1.15.0-notes.rst +++ b/doc/release/1.15.0-notes.rst @@ -359,8 +359,8 @@ Increased performance in ``random.permutation`` for multidimensional arrays ``permutation`` uses the fast path in ``random.shuffle`` for all input array dimensions. Previously the fast path was only used for 1-d arrays. -Generalized ufuncs now accept ``axes`` and ``keepdims`` arguments ------------------------------------------------------------------ +Generalized ufuncs now accept ``axes``, ``axis`` and ``keepdims`` arguments +--------------------------------------------------------------------------- One can control over which axes a generalized ufunc operates by passing in an ``axes`` argument, a list of tuples with indices of particular axes. For instance, for a signature of ``(i,j),(j,k)->(i,k)`` appropriate for matrix @@ -376,12 +376,19 @@ tuples can be omitted. Hence, for a signature of ``(i),(i)->()`` appropriate for an inner product, one could pass in ``axes=[0, 0]`` to indicate that the vectors are stored in the first dimensions of the two inputs arguments. +As a short-cut for generalized ufuncs that are similar to reductions, i.e., +that act on a single, shared core dimension such as the inner product example +above, one can pass an ``axis`` argument. This is equivalent to passing in +``axes`` with identical entries for all arguments with that core dimension +(e.g., for the example above, ``axes=[(axis,), (axis,)]``). + Furthermore, like for reductions, for generalized ufuncs that have inputs that all have the same number of core dimensions and outputs with no core dimension, one can pass in ``keepdims`` to leave a dimension with size 1 in the outputs, thus allowing proper broadcasting against the original inputs. The location of the extra dimension can be controlled with ``axes``. For instance, for the inner-product example, ``keepdims=True, axes=[-2, -2, -2]`` would act on the +inner-product example, ``keepdims=True, axis=-2`` would act on the one-but-last dimension of the input arguments, and leave a size 1 dimension in that place in the output. diff --git a/doc/source/reference/ufuncs.rst b/doc/source/reference/ufuncs.rst index 995542d777e5..3cc956887569 100644 --- a/doc/source/reference/ufuncs.rst +++ b/doc/source/reference/ufuncs.rst @@ -360,6 +360,17 @@ advanced usage and will not typically be used. and for generalized ufuncs for which all outputs are scalars, the output tuples can be omitted. +*axis* + + .. versionadded:: 1.15 + + A single axis over which a generalized ufunc should operate. This is a + short-cut for ufuncs that operate over a single, shared core dimension, + equivalent to passing in ``axes`` with entries of ``(axis,)`` for each + single-core-dimension argument and ``()`` for all others. For instance, + for a signature ``(i),(i)->()``, it is equivalent to passing in + ``axes=[(axis,), (axis,), ()]``. + *keepdims* .. versionadded:: 1.15 @@ -370,7 +381,7 @@ advanced usage and will not typically be used. ufuncs that operate on inputs that all have the same number of core dimensions and with outputs that have no core dimensions , i.e., with signatures like ``(i),(i)->()`` or ``(m,m)->()``. If used, the location of - the dimensions in the output can be controlled with ``axes``. + the dimensions in the output can be controlled with ``axes`` and ``axis``. *casting* From 79681ba7e1e73f7ef7da23b6be0d2ae334a729c1 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Wed, 2 May 2018 11:31:24 -0400 Subject: [PATCH 3/5] MAINT: let ufunc override reject passing in both axis and axes. --- numpy/core/src/umath/override.c | 12 ++++++++++-- numpy/core/tests/test_umath.py | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/numpy/core/src/umath/override.c b/numpy/core/src/umath/override.c index c298fe315bd2..c0bc47b7b3cb 100644 --- a/numpy/core/src/umath/override.c +++ b/numpy/core/src/umath/override.c @@ -51,6 +51,7 @@ normalize___call___args(PyUFuncObject *ufunc, PyObject *args, npy_intp nin = ufunc->nin; npy_intp nout = ufunc->nout; npy_intp nargs = PyTuple_GET_SIZE(args); + npy_intp nkwds = PyDict_Size(*normal_kwds); PyObject *obj; if (nargs < nin) { @@ -74,7 +75,7 @@ normalize___call___args(PyUFuncObject *ufunc, PyObject *args, /* If we have more args than nin, they must be the output variables.*/ if (nargs > nin) { - if(PyDict_GetItemString(*normal_kwds, "out")) { + if(nkwds > 0 && PyDict_GetItemString(*normal_kwds, "out")) { PyErr_Format(PyExc_TypeError, "argument given by name ('out') and position " "(%"NPY_INTP_FMT")", nin); @@ -112,8 +113,15 @@ normalize___call___args(PyUFuncObject *ufunc, PyObject *args, Py_DECREF(obj); } } + /* gufuncs accept either 'axes' or 'axis', but not both */ + if (nkwds >= 2 && (PyDict_GetItemString(*normal_kwds, "axis") && + PyDict_GetItemString(*normal_kwds, "axes"))) { + PyErr_SetString(PyExc_TypeError, + "cannot specify both 'axis' and 'axes'"); + return -1; + } /* finally, ufuncs accept 'sig' or 'signature' normalize to 'signature' */ - return normalize_signature_keyword(*normal_kwds); + return nkwds == 0 ? 0 : normalize_signature_keyword(*normal_kwds); } static int diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py index 93ec73094b9b..3c0d1759a7ea 100644 --- a/numpy/core/tests/test_umath.py +++ b/numpy/core/tests/test_umath.py @@ -1810,6 +1810,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): assert_raises(TypeError, np.multiply, a) assert_raises(TypeError, np.multiply, a, a, a, a) assert_raises(TypeError, np.multiply, a, a, sig='a', signature='a') + assert_raises(TypeError, ncu_tests.inner1d, a, a, axis=0, axes=[0, 0]) # reduce, positional args res = np.multiply.reduce(a, 'axis0', 'dtype0', 'out0', 'keep0') From 9dad029be9ce19f92988efcbb9de33e00cbe054c Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Sun, 27 May 2018 10:08:10 -0400 Subject: [PATCH 4/5] MAINT: Interpret gufunc axis directly rather than construct axes. Much faster, and much less code duplication than I feared. --- numpy/core/src/umath/ufunc_object.c | 123 +++++++++++++++------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index c77087751532..67c00d623810 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -1948,42 +1948,6 @@ _check_keepdims_support(PyUFuncObject *ufunc) { return 0; } -/* - * Translate axis to axes list of the form [(axis,), ...], with an - * empty tuple for operands without core dimensions. - * Returns an axes tuple or NULL on failure. - */ -static PyObject* -_build_axes_tuple_from_axis(PyObject *axis, int core_num_dims[], int nop) { - int i; - PyObject *axes = NULL, *axis_tuple = NULL, *tuple; - PyObject *empty_tuple = PyTuple_New(0); /* cannot realistically fail */ - - axes = PyList_New(nop); - if (axes == NULL) { - return NULL; - } - axis_tuple = PyTuple_Pack(1, axis); - if (axis_tuple == NULL) { - goto fail; - } - - for (i = 0; i < nop; i++) { - tuple = core_num_dims[i] == 1 ? axis_tuple : empty_tuple; - Py_INCREF(tuple); - PyList_SET_ITEM(axes, i, tuple); - } - Py_DECREF(axis_tuple); - Py_DECREF(empty_tuple); - return axes; - -fail: - Py_XDECREF(axis_tuple); - Py_XDECREF(axes); - Py_DECREF(empty_tuple); - return NULL; -} - /* * Interpret a possible axes keyword argument, using it to fill the remap_axis * array which maps default to actual axes for each operand, indexed as @@ -1996,8 +1960,7 @@ static int _parse_axes_arg(PyUFuncObject *ufunc, int core_num_dims[], PyObject *axes, PyArrayObject **op, int broadcast_ndim, int **remap_axis) { int nin = ufunc->nin; - int nout = ufunc->nout; - int nop = nin + nout; + int nop = ufunc->nargs; int iop, list_size; if (!PyList_Check(axes)) { @@ -2115,6 +2078,59 @@ _parse_axes_arg(PyUFuncObject *ufunc, int core_num_dims[], PyObject *axes, return 0; } +/* + * Simplified version of the above, using axis to fill the remap_axis + * array, which maps default to actual axes for each operand, indexed as + * as remap_axis[iop][iaxis]. The default axis order has first all broadcast + * axes and then the core axes the gufunc operates on. + * + * Returns 0 on success, and -1 on failure + */ +static int +_parse_axis_arg(PyUFuncObject *ufunc, int core_num_dims[], PyObject *axis, + PyArrayObject **op, int broadcast_ndim, int **remap_axis) { + int nop = ufunc->nargs; + int iop, axis_int; + + axis_int = PyArray_PyIntAsInt(axis); + if (error_converting(axis_int)) { + return -1; + } + + for (iop = 0; iop < nop; ++iop) { + int axis, op_ndim, op_axis; + + /* _check_axis_support ensures core_num_dims is 0 or 1 */ + if (core_num_dims[iop] == 0) { + remap_axis[iop] = NULL; + continue; + } + if (op[iop]) { + op_ndim = PyArray_NDIM(op[iop]); + } + else { + op_ndim = broadcast_ndim + 1; + } + op_axis = axis_int; /* ensure we don't modify axis_int */ + if (check_and_adjust_axis(&op_axis, op_ndim) < 0) { + return -1; + } + /* Are we actually remapping away from last axis? */ + if (op_axis == op_ndim - 1) { + remap_axis[iop] = NULL; + continue; + } + remap_axis[iop][op_ndim - 1] = op_axis; + for (axis = 0; axis < op_axis; axis++) { + remap_axis[iop][axis] = axis; + } + for (axis = op_axis; axis < op_ndim - 1; axis++) { + remap_axis[iop][axis] = axis + 1; + } + } /* end of for(iop) loop over operands */ + return 0; +} + #define REMAP_AXIS(iop, axis) ((remap_axis != NULL && \ remap_axis[iop] != NULL)? \ remap_axis[iop][axis] : axis) @@ -2340,13 +2356,11 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, * Get all the arguments. * * Here, when provided, extobj, typetup, axes, and axis contain borrowed - * references; axes, however, may be set internally (from axis), and thus - * is XDECREF'd at the end. Hence, if passed in, we get our own reference. + * references. */ retval = get_ufunc_arguments(ufunc, args, kwds, op, &order, &casting, &extobj, &type_tup, &subok, NULL, &axes, &axis, &keepdims); - Py_XINCREF(axes); if (retval < 0) { goto fail; } @@ -2434,21 +2448,8 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, goto fail; } - /* - * Translate axis to axes list of the form [(axis,), ...], with an - * empty tuple for operands without core dimensions. - */ - if (axis) { - assert(axes == NULL); /* parser prevents passing in both axis & axes */ - axes = _build_axes_tuple_from_axis(axis, core_num_dims, nop); - if (axes == NULL) { - retval = -1; - goto fail; - } - } - /* Possibly remap axes. */ - if (axes) { + if (axes != NULL || axis != NULL) { remap_axis = PyArray_malloc(sizeof(remap_axis[0]) * nop); remap_axis_memory = PyArray_malloc(sizeof(remap_axis_memory[0]) * nop * NPY_MAXDIMS); @@ -2459,8 +2460,14 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, for (i=0; i < nop; i++) { remap_axis[i] = remap_axis_memory + i * NPY_MAXDIMS; } - retval = _parse_axes_arg(ufunc, core_num_dims, axes, op, broadcast_ndim, - remap_axis); + if (axis) { + retval = _parse_axis_arg(ufunc, core_num_dims, axis, op, + broadcast_ndim, remap_axis); + } + else { + retval = _parse_axes_arg(ufunc, core_num_dims, axes, op, + broadcast_ndim, remap_axis); + } if(retval < 0) { goto fail; } @@ -2806,7 +2813,6 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, Py_XDECREF(type_tup); Py_XDECREF(full_args.in); Py_XDECREF(full_args.out); - Py_XDECREF(axes); NPY_UF_DBG_PRINT("Returning Success\n"); @@ -2826,7 +2832,6 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc, Py_XDECREF(type_tup); Py_XDECREF(full_args.in); Py_XDECREF(full_args.out); - Py_XDECREF(axes); PyArray_free(remap_axis_memory); PyArray_free(remap_axis); return retval; From 9a3f8c88f034537eb706dbb7d9748d202e4390b9 Mon Sep 17 00:00:00 2001 From: Marten van Kerkwijk Date: Sun, 27 May 2018 21:13:40 -0400 Subject: [PATCH 5/5] DOC: Add release note about __array_ufunc__. --- doc/release/1.15.0-notes.rst | 5 ++++- doc/scipy-sphinx-theme | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/release/1.15.0-notes.rst b/doc/release/1.15.0-notes.rst index e3069a99c5cf..e148dad3ba08 100644 --- a/doc/release/1.15.0-notes.rst +++ b/doc/release/1.15.0-notes.rst @@ -193,7 +193,7 @@ combining these 5 compiled builds products into a single "fat" binary. ``return_indices`` keyword added for ``np.intersect1d`` ------------------------------------------------------- New keyword ``return_indices`` returns the indices of the two input arrays -that correspond to the common elements. +that correspond to the common elements. ``np.quantile`` and ``np.nanquantile`` -------------------------------------- @@ -408,6 +408,9 @@ is the same as:: ``np.put_along_axis`` acts as the dual operation for writing to these indices within an array. +.. note:: Implementations of ``__array_ufunc__`` should ensure that they can + handle either ``axis`` or ``axes``. In future, we may convert + ``axis`` to ``axes`` before passing it on. Changes ======= diff --git a/doc/scipy-sphinx-theme b/doc/scipy-sphinx-theme index c466764e2231..d990ab913419 160000 --- a/doc/scipy-sphinx-theme +++ b/doc/scipy-sphinx-theme @@ -1 +1 @@ -Subproject commit c466764e2231ba132c09826b5b138fffa1cfcec3 +Subproject commit d990ab9134199f6496b9ac8567f10791f04a720a