summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
authorAllan Haldane <ealloc@gmail.com>2018-02-28 17:55:54 +0100
committerGitHub <noreply@github.com>2018-02-28 17:55:54 +0100
commitc7459dd7a889193685ce4bfb7028dcf2f73c2703 (patch)
tree48fc41a71ae85b0624ec04c83c710b07587d2dfd /numpy
parentbc07d0ec633e13f30e51178ebe20e1e2070e3bc3 (diff)
parent05a420af6504efa21f99f968b2c66de62c1668d7 (diff)
downloadnumpy-c7459dd7a889193685ce4bfb7028dcf2f73c2703.tar.gz
Merge pull request #8819 from mhvk/gufunc-axis-argument
ENH: Implement axes keyword argument for gufuncs.
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/src/umath/ufunc_object.c251
-rw-r--r--numpy/core/tests/test_ufunc.py99
2 files changed, 321 insertions, 29 deletions
diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c
index c67f60752..bf5a4ead3 100644
--- a/numpy/core/src/umath/ufunc_object.c
+++ b/numpy/core/src/umath/ufunc_object.c
@@ -555,7 +555,8 @@ get_ufunc_arguments(PyUFuncObject *ufunc,
PyObject **out_extobj,
PyObject **out_typetup,
int *out_subok,
- PyArrayObject **out_wheremask)
+ PyArrayObject **out_wheremask,
+ PyObject **out_axes)
{
int i, nargs;
int nin = ufunc->nin;
@@ -570,6 +571,9 @@ get_ufunc_arguments(PyUFuncObject *ufunc,
*out_extobj = NULL;
*out_typetup = NULL;
+ if (out_axes != NULL) {
+ *out_axes = NULL;
+ }
if (out_wheremask != NULL) {
*out_wheremask = NULL;
}
@@ -806,6 +810,13 @@ get_ufunc_arguments(PyUFuncObject *ufunc,
}
switch (str[0]) {
+ case 'a':
+ /* possible axis argument for generalized ufunc */
+ if (out_axes != NULL && strcmp(str, "axes") == 0) {
+ *out_axes = value;
+ bad_arg = 0;
+ }
+ break;
case 'c':
/* Provides a policy for allowed casting */
if (strcmp(str, "casting") == 0) {
@@ -995,6 +1006,10 @@ fail:
*out_extobj = NULL;
Py_XDECREF(*out_typetup);
*out_typetup = NULL;
+ if (out_axes != NULL) {
+ Py_XDECREF(*out_axes);
+ *out_axes = NULL;
+ }
if (out_wheremask != NULL) {
Py_XDECREF(*out_wheremask);
*out_wheremask = NULL;
@@ -1760,6 +1775,155 @@ make_arr_prep_args(npy_intp nin, PyObject *args, PyObject *kwds)
}
/*
+ * Check whether any of the outputs of a gufunc has core dimensions.
+ */
+static int
+_has_output_coredims(PyUFuncObject *ufunc) {
+ int i;
+ for (i = ufunc->nin; i < ufunc->nin + ufunc->nout; ++i) {
+ if (ufunc->core_num_dims[i] > 0) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/*
+ * 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
+ * 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_axes_arg(PyUFuncObject *ufunc, PyObject *axes, PyArrayObject **op,
+ int broadcast_ndim, int **remap_axis) {
+ int nin = ufunc->nin;
+ int nout = ufunc->nout;
+ int nop = nin + nout;
+ int iop, list_size;
+
+ if (!PyList_Check(axes)) {
+ PyErr_SetString(PyExc_TypeError, "axes should be a list.");
+ return -1;
+ }
+ list_size = PyList_Size(axes);
+ if (list_size != nop) {
+ if (list_size != nin || _has_output_coredims(ufunc)) {
+ PyErr_Format(PyExc_ValueError,
+ "axes should be a list with an entry for all "
+ "%d inputs and outputs; entries for outputs can only "
+ "be omitted if none of them has core axes.",
+ nop);
+ return -1;
+ }
+ for (iop = nin; iop < nop; iop++) {
+ remap_axis[iop] = NULL;
+ }
+ }
+ for (iop = 0; iop < list_size; ++iop) {
+ int op_ndim, op_ncore, op_nbroadcast;
+ int have_seen_axis[NPY_MAXDIMS] = {0};
+ PyObject *op_axes_tuple, *axis_item;
+ int axis, op_axis;
+
+ op_ncore = ufunc->core_num_dims[iop];
+ if (op[iop] != NULL) {
+ op_ndim = PyArray_NDIM(op[iop]);
+ op_nbroadcast = op_ndim - op_ncore;
+ }
+ else {
+ op_nbroadcast = broadcast_ndim;
+ op_ndim = broadcast_ndim + op_ncore;
+ }
+ /*
+ * Get axes tuple for operand. If not a tuple already, make it one if
+ * there is only one axis (its content is checked later).
+ */
+ op_axes_tuple = PyList_GET_ITEM(axes, iop);
+ if (PyTuple_Check(op_axes_tuple)) {
+ if (PyTuple_Size(op_axes_tuple) != op_ncore) {
+ if (op_ncore == 1) {
+ PyErr_Format(PyExc_ValueError,
+ "axes item %d should be a tuple with a "
+ "single element, or an integer", iop);
+ }
+ else {
+ PyErr_Format(PyExc_ValueError,
+ "axes item %d should be a tuple with %d "
+ "elements", iop, op_ncore);
+ }
+ return -1;
+ }
+ Py_INCREF(op_axes_tuple);
+ }
+ else if (op_ncore == 1) {
+ op_axes_tuple = PyTuple_Pack(1, op_axes_tuple);
+ if (op_axes_tuple == NULL) {
+ return -1;
+ }
+ }
+ else {
+ PyErr_Format(PyExc_TypeError, "axes item %d should be a tuple",
+ iop);
+ return -1;
+ }
+ /*
+ * Now create the remap, starting with the core dimensions, and then
+ * adding the remaining broadcast axes that are to be iterated over.
+ */
+ for (axis = op_nbroadcast; axis < op_ndim; axis++) {
+ axis_item = PyTuple_GET_ITEM(op_axes_tuple, axis - op_nbroadcast);
+ op_axis = PyArray_PyIntAsInt(axis_item);
+ if (error_converting(op_axis) ||
+ (check_and_adjust_axis(&op_axis, op_ndim) < 0)) {
+ Py_DECREF(op_axes_tuple);
+ return -1;
+ }
+ if (have_seen_axis[op_axis]) {
+ PyErr_Format(PyExc_ValueError,
+ "axes item %d has value %d repeated",
+ iop, op_axis);
+ Py_DECREF(op_axes_tuple);
+ return -1;
+ }
+ have_seen_axis[op_axis] = 1;
+ remap_axis[iop][axis] = op_axis;
+ }
+ Py_DECREF(op_axes_tuple);
+ /*
+ * Fill the op_nbroadcast=op_ndim-op_ncore axes not yet set,
+ * using have_seen_axis to skip over entries set above.
+ */
+ for (axis = 0, op_axis = 0; axis < op_nbroadcast; axis++) {
+ while (have_seen_axis[op_axis]) {
+ op_axis++;
+ }
+ remap_axis[iop][axis] = op_axis++;
+ }
+ /*
+ * Check whether we are actually remapping anything. Here,
+ * op_axis can only equal axis if all broadcast axes were the same
+ * (i.e., the while loop above was never entered).
+ */
+ if (axis == op_axis) {
+ while (axis < op_ndim && remap_axis[iop][axis] == axis) {
+ axis++;
+ }
+ }
+ if (axis == op_ndim) {
+ remap_axis[iop] = NULL;
+ }
+ } /* 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)
+
+/*
* Validate the core dimensions of all the operands, and collect all of
* the labelled core dimensions into 'core_dim_sizes'.
*
@@ -1781,7 +1945,7 @@ make_arr_prep_args(npy_intp nin, PyObject *args, PyObject *kwds)
*/
static int
_get_coredim_sizes(PyUFuncObject *ufunc, PyArrayObject **op,
- npy_intp* core_dim_sizes) {
+ npy_intp* core_dim_sizes, int **remap_axis) {
int i;
int nin = ufunc->nin;
int nout = ufunc->nout;
@@ -1796,27 +1960,14 @@ _get_coredim_sizes(PyUFuncObject *ufunc, PyArrayObject **op,
int dim_offset = ufunc->core_offsets[i];
int num_dims = ufunc->core_num_dims[i];
int core_start_dim = PyArray_NDIM(op[i]) - num_dims;
-
- /* Check if operands have enough dimensions */
- if (core_start_dim < 0) {
- PyErr_Format(PyExc_ValueError,
- "%s: %s operand %d does not have enough "
- "dimensions (has %d, gufunc core with "
- "signature %s requires %d)",
- ufunc_get_name_cstr(ufunc), i < nin ? "Input" : "Output",
- i < nin ? i : i - nin, PyArray_NDIM(op[i]),
- ufunc->core_signature, num_dims);
- return -1;
- }
-
/*
* Make sure every core dimension exactly matches all other core
* dimensions with the same label.
*/
for (idim = 0; idim < num_dims; ++idim) {
int core_dim_index = ufunc->core_dim_ixs[dim_offset+idim];
- npy_intp op_dim_size =
- PyArray_DIM(op[i], core_start_dim+idim);
+ npy_intp op_dim_size = PyArray_DIM(
+ op[i], REMAP_AXIS(i, core_start_dim+idim));
if (core_dim_sizes[core_dim_index] == -1) {
core_dim_sizes[core_dim_index] = op_dim_size;
@@ -1950,7 +2101,9 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
/* The sizes of the core dimensions (# entries is ufunc->core_num_dim_ix) */
npy_intp *core_dim_sizes = inner_dimensions + 1;
int core_dim_ixs_size;
-
+ /* swapping around of axes */
+ int *remap_axis_memory = NULL;
+ int **remap_axis = NULL;
/* The __array_prepare__ function to call for each output */
PyObject *arr_prep[NPY_MAXARGS];
/*
@@ -1962,8 +2115,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 and typetup contain borrowed references */
- PyObject *extobj = NULL, *type_tup = NULL;
+ /* When provided, extobj, typetup, and axes contain borrowed references */
+ PyObject *extobj = NULL, *type_tup = NULL, *axes = NULL;
if (ufunc == NULL) {
PyErr_SetString(PyExc_ValueError, "function not supported");
@@ -1990,12 +2143,30 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
/* Get all the arguments */
retval = get_ufunc_arguments(ufunc, args, kwds,
op, &order, &casting, &extobj,
- &type_tup, &subok, NULL);
+ &type_tup, &subok, NULL, &axes);
if (retval < 0) {
goto fail;
}
/*
+ * Check that operands have the minimum dimensions required.
+ * (Just checks core; broadcast dimensions are tested by the iterator.)
+ */
+ for (i = 0; i < nop; i++) {
+ if (op[i] != NULL && PyArray_NDIM(op[i]) < ufunc->core_num_dims[i]) {
+ PyErr_Format(PyExc_ValueError,
+ "%s: %s operand %d does not have enough "
+ "dimensions (has %d, gufunc core with "
+ "signature %s requires %d)",
+ ufunc_get_name_cstr(ufunc),
+ i < nin ? "Input" : "Output",
+ i < nin ? i : i - nin, PyArray_NDIM(op[i]),
+ ufunc->core_signature, ufunc->core_num_dims[i]);
+ goto fail;
+ }
+ }
+
+ /*
* Figure out the number of iteration dimensions, which
* is the broadcast result of all the input non-core
* dimensions.
@@ -2026,8 +2197,27 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
goto fail;
}
+ /* Possibly remap axes. */
+ if (axes) {
+ remap_axis = PyArray_malloc(sizeof(remap_axis[0]) * nop);
+ remap_axis_memory = PyArray_malloc(sizeof(remap_axis_memory[0]) *
+ nop * NPY_MAXDIMS);
+ if (remap_axis == NULL || remap_axis_memory == NULL) {
+ PyErr_NoMemory();
+ goto fail;
+ }
+ for (i=0; i < nop; i++) {
+ remap_axis[i] = remap_axis_memory + i * NPY_MAXDIMS;
+ }
+ retval = _parse_axes_arg(ufunc, axes, op, broadcast_ndim,
+ remap_axis);
+ if(retval < 0) {
+ goto fail;
+ }
+ }
+
/* Collect the lengths of the labelled core dimensions */
- retval = _get_coredim_sizes(ufunc, op, core_dim_sizes);
+ retval = _get_coredim_sizes(ufunc, op, core_dim_sizes, remap_axis);
if(retval < 0) {
goto fail;
}
@@ -2054,7 +2244,8 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
/* Broadcast all the unspecified dimensions normally */
for (idim = 0; idim < broadcast_ndim; ++idim) {
if (idim >= broadcast_ndim - n) {
- op_axes_arrays[i][idim] = idim - (broadcast_ndim - n);
+ op_axes_arrays[i][idim] =
+ REMAP_AXIS(i, idim - (broadcast_ndim - n));
}
else {
op_axes_arrays[i][idim] = -1;
@@ -2074,7 +2265,7 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
for (idim = 0; idim < num_dims; ++idim) {
iter_shape[j] = core_dim_sizes[
ufunc->core_dim_ixs[dim_offset + idim]];
- op_axes_arrays[i][j] = n + idim;
+ op_axes_arrays[i][j] = REMAP_AXIS(i, n + idim);
++j;
}
}
@@ -2220,11 +2411,12 @@ PyUFunc_GeneralizedFunction(PyUFuncObject *ufunc,
for (j = 0; j < num_dims; ++j) {
if (core_start_dim + j >= 0) {
/*
- * Force the stride to zero when the shape is 1, sot
+ * Force the stride to zero when the shape is 1, so
* that the broadcasting works right.
*/
- if (shape[core_start_dim + j] != 1) {
- inner_strides[idim++] = strides[core_start_dim + j];
+ int remapped_axis = REMAP_AXIS(i, core_start_dim + j);
+ if (shape[remapped_axis] != 1) {
+ inner_strides[idim++] = strides[remapped_axis];
} else {
inner_strides[idim++] = 0;
}
@@ -2375,7 +2567,8 @@ fail:
}
Py_XDECREF(type_tup);
Py_XDECREF(arr_prep_args);
-
+ PyArray_free(remap_axis_memory);
+ PyArray_free(remap_axis);
return retval;
}
@@ -2450,7 +2643,7 @@ PyUFunc_GenericFunction(PyUFuncObject *ufunc,
/* Get all the arguments */
retval = get_ufunc_arguments(ufunc, args, kwds,
op, &order, &casting, &extobj,
- &type_tup, &subok, &wheremask);
+ &type_tup, &subok, &wheremask, NULL);
if (retval < 0) {
goto fail;
}
diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py
index 57e0ec272..7e1bfbdbe 100644
--- a/numpy/core/tests/test_ufunc.py
+++ b/numpy/core/tests/test_ufunc.py
@@ -597,6 +597,105 @@ class TestUfunc(object):
umt.inner1d(a, b, out=c[..., 0])
assert_array_equal(c[..., 0], np.sum(a*b, axis=-1), err_msg=msg)
+ def test_axes_argument(self):
+ # inner1d signature: '(i),(i)->()'
+ in1d = umt.inner1d
+ a = np.arange(27.).reshape((3, 3, 3))
+ b = np.arange(10., 19.).reshape((3, 1, 3))
+ # basic tests on inputs (outputs tested below with matrix_multiply).
+ c = in1d(a, b)
+ assert_array_equal(c, (a * b).sum(-1))
+ # default
+ c = in1d(a, b, axes=[(-1,), (-1,), ()])
+ assert_array_equal(c, (a * b).sum(-1))
+ # integers ok for single axis.
+ c = in1d(a, b, axes=[-1, -1, ()])
+ assert_array_equal(c, (a * b).sum(-1))
+ # mix fine
+ c = in1d(a, b, axes=[(-1,), -1, ()])
+ assert_array_equal(c, (a * b).sum(-1))
+ # can omit last axis.
+ c = in1d(a, b, axes=[-1, -1])
+ assert_array_equal(c, (a * b).sum(-1))
+ # can pass in other types of integer (with __index__ protocol)
+ c = in1d(a, b, axes=[np.int8(-1), np.array(-1, dtype=np.int32)])
+ assert_array_equal(c, (a * b).sum(-1))
+ # swap some axes
+ c = in1d(a, b, axes=[0, 0])
+ assert_array_equal(c, (a * b).sum(0))
+ c = in1d(a, b, axes=[0, 2])
+ assert_array_equal(c, (a.transpose(1, 2, 0) * b).sum(-1))
+ # Check errors for inproperly constructed axes arguments.
+ # should have list.
+ assert_raises(TypeError, in1d, a, b, axes=-1)
+ # needs enough elements
+ assert_raises(ValueError, in1d, a, b, axes=[-1])
+ # should pass in indices.
+ assert_raises(TypeError, in1d, a, b, axes=[-1.0, -1.0])
+ assert_raises(TypeError, in1d, a, b, axes=[(-1.0,), -1])
+ assert_raises(TypeError, in1d, a, b, axes=[None, 1])
+ # cannot pass an index unless there is only one dimension
+ # (output is wrong in this case)
+ assert_raises(TypeError, in1d, a, b, axes=[-1, -1, -1])
+ # or pass in generally the wrong number of axes
+ assert_raises(ValueError, in1d, a, b, axes=[-1, -1, (-1,)])
+ assert_raises(ValueError, in1d, a, b, axes=[-1, (-2, -1), ()])
+ # axes need to have same length.
+ assert_raises(ValueError, in1d, a, b, axes=[0, 1])
+
+ # matrix_multiply signature: '(m,n),(n,p)->(m,p)'
+ mm = umt.matrix_multiply
+ a = np.arange(12).reshape((2, 3, 2))
+ b = np.arange(8).reshape((2, 2, 2, 1)) + 1
+ # Sanity check.
+ c = mm(a, b)
+ assert_array_equal(c, np.matmul(a, b))
+ # Default axes.
+ c = mm(a, b, axes=[(-2, -1), (-2, -1), (-2, -1)])
+ assert_array_equal(c, np.matmul(a, b))
+ # Default with explicit axes.
+ c = mm(a, b, axes=[(1, 2), (2, 3), (2, 3)])
+ assert_array_equal(c, np.matmul(a, b))
+ # swap some axes.
+ c = mm(a, b, axes=[(0, -1), (1, 2), (-2, -1)])
+ assert_array_equal(c, np.matmul(a.transpose(1, 0, 2),
+ b.transpose(0, 3, 1, 2)))
+ # Default with output array.
+ c = np.empty((2, 2, 3, 1))
+ d = mm(a, b, out=c, axes=[(1, 2), (2, 3), (2, 3)])
+ assert_(c is d)
+ assert_array_equal(c, np.matmul(a, b))
+ # Transposed output array
+ c = np.empty((1, 2, 2, 3))
+ d = mm(a, b, out=c, axes=[(-2, -1), (-2, -1), (3, 0)])
+ assert_(c is d)
+ assert_array_equal(c, np.matmul(a, b).transpose(3, 0, 1, 2))
+ # Check errors for inproperly constructed axes arguments.
+ # wrong argument
+ assert_raises(TypeError, mm, a, b, axis=1)
+ # axes should be list
+ assert_raises(TypeError, mm, a, b, axes=1)
+ assert_raises(TypeError, mm, a, b, axes=((-2, -1), (-2, -1), (-2, -1)))
+ # list needs to have right length
+ assert_raises(ValueError, mm, a, b, axes=[])
+ assert_raises(ValueError, mm, a, b, axes=[(-2, -1)])
+ # list should contain tuples for multiple axes
+ assert_raises(TypeError, mm, a, b, axes=[-1, -1, -1])
+ assert_raises(TypeError, mm, a, b, axes=[(-2, -1), (-2, -1), -1])
+ assert_raises(TypeError,
+ mm, a, b, axes=[[-2, -1], [-2, -1], [-2, -1]])
+ assert_raises(TypeError,
+ mm, a, b, axes=[(-2, -1), (-2, -1), [-2, -1]])
+ assert_raises(TypeError, mm, a, b, axes=[(-2, -1), (-2, -1), None])
+ # tuples should not have duplicated values
+ assert_raises(ValueError, mm, a, b, axes=[(-2, -1), (-2, -1), (-2, -2)])
+ # arrays should have enough axes.
+ z = np.zeros((2, 2))
+ assert_raises(ValueError, mm, z, z[0])
+ assert_raises(ValueError, mm, z, z, out=z[:, 0])
+ assert_raises(ValueError, mm, z[1], z, axes=[0, 1])
+ assert_raises(ValueError, mm, z, z, out=z[0], axes=[0, 1])
+
def test_innerwt(self):
a = np.arange(6).reshape((2, 3))
b = np.arange(10, 16).reshape((2, 3))