summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/src/umath/reduction.c27
-rw-r--r--numpy/core/tests/test_ufunc.py61
2 files changed, 75 insertions, 13 deletions
diff --git a/numpy/core/src/umath/reduction.c b/numpy/core/src/umath/reduction.c
index 2f0420610..529d5523a 100644
--- a/numpy/core/src/umath/reduction.c
+++ b/numpy/core/src/umath/reduction.c
@@ -245,11 +245,12 @@ check_nonreorderable_axes(int ndim, npy_bool *axis_flags, const char *funcname)
* it sees along the reduction axes to result, then return a view of
* the operand which excludes that element.
*
- * If a reduction has an identity, such as 0 or 1, the result should
- * be initialized by calling PyArray_AssignZero(result, NULL, NULL)
- * or PyArray_AssignOne(result, NULL, NULL), because this
- * function raises an exception when there are no elements to reduce.
- *
+ * If a reduction has an identity, such as 0 or 1, the result should be
+ * initialized by calling PyArray_AssignZero(result, NULL, NULL) or
+ * PyArray_AssignOne(result, NULL, NULL), because this function raises an
+ * exception when there are no elements to reduce (which appropriate iff the
+ * reduction operation has no identity).
+ *
* This means it copies the subarray indexed at zero along each reduction axis
* into 'result', then returns a view into 'operand' excluding those copied
* elements.
@@ -299,14 +300,6 @@ PyArray_InitializeReduceResult(
return NULL;
}
- if (PyArray_SIZE(operand) == 0) {
- PyErr_Format(PyExc_ValueError,
- "zero-size array to reduction operation %s "
- "which has no identity",
- funcname);
- return NULL;
- }
-
/* Take a view into 'operand' which we can modify. */
op_view = (PyArrayObject *)PyArray_View(operand, NULL, &PyArray_Type);
if (op_view == NULL) {
@@ -326,6 +319,14 @@ PyArray_InitializeReduceResult(
memcpy(shape_orig, shape, ndim * sizeof(npy_intp));
for (idim = 0; idim < ndim; ++idim) {
if (axis_flags[idim]) {
+ if (shape[idim] == 0) {
+ PyErr_Format(PyExc_ValueError,
+ "zero-size array to reduction operation %s "
+ "which has no identity",
+ funcname);
+ Py_DECREF(op_view);
+ return NULL;
+ }
shape[idim] = 1;
++nreduce_axes;
}
diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py
index eb1da6676..53928129f 100644
--- a/numpy/core/tests/test_ufunc.py
+++ b/numpy/core/tests/test_ufunc.py
@@ -681,5 +681,66 @@ class TestUfunc(TestCase):
assert_raises(ValueError, np.divide.reduce, a, axis=(0,1))
+ def test_reduce_zero_axis(self):
+ # If we have a n x m array and do a reduction with axis=1, then we are
+ # doing n reductions, and each reduction takes an m-element array. For
+ # a reduction operation without an identity, then:
+ # n > 0, m > 0: fine
+ # n = 0, m > 0: fine, doing 0 reductions of m-element arrays
+ # n > 0, m = 0: can't reduce a 0-element array, ValueError
+ # n = 0, m = 0: can't reduce a 0-element array, ValueError (for
+ # consistency with the above case)
+ # This test doesn't actually look at return values, it just checks to
+ # make sure that error we get an error in exactly those cases where we
+ # expect one, and assumes the calculations themselves are done
+ # correctly.
+ def ok(f, *args, **kwargs):
+ f(*args, **kwargs)
+ def err(f, *args, **kwargs):
+ assert_raises(ValueError, f, *args, **kwargs)
+ def t(expect, func, n, m):
+ expect(func, np.zeros((n, m)), axis=1)
+ expect(func, np.zeros((m, n)), axis=0)
+ expect(func, np.zeros((n // 2, n // 2, m)), axis=2)
+ expect(func, np.zeros((n // 2, m, n // 2)), axis=1)
+ expect(func, np.zeros((n, m // 2, m // 2)), axis=(1, 2))
+ expect(func, np.zeros((m // 2, n, m // 2)), axis=(0, 2))
+ expect(func, np.zeros((m // 3, m // 3, m // 3,
+ n // 2, n //2)),
+ axis=(0, 1, 2))
+ # Check what happens if the inner (resp. outer) dimensions are a
+ # mix of zero and non-zero:
+ expect(func, np.zeros((10, m, n)), axis=(0, 1))
+ expect(func, np.zeros((10, n, m)), axis=(0, 2))
+ expect(func, np.zeros((m, 10, n)), axis=0)
+ expect(func, np.zeros((10, m, n)), axis=1)
+ expect(func, np.zeros((10, n, m)), axis=2)
+ # np.maximum is just an arbitrary ufunc with no reduction identity
+ assert_equal(np.maximum.identity, None)
+ t(ok, np.maximum.reduce, 30, 30)
+ t(ok, np.maximum.reduce, 0, 30)
+ t(err, np.maximum.reduce, 30, 0)
+ t(err, np.maximum.reduce, 0, 0)
+ err(np.maximum.reduce, [])
+ np.maximum.reduce(np.zeros((0, 0)), axis=())
+
+ # all of the combinations are fine for a reduction that has an
+ # identity
+ t(ok, np.add.reduce, 30, 30)
+ t(ok, np.add.reduce, 0, 30)
+ t(ok, np.add.reduce, 30, 0)
+ t(ok, np.add.reduce, 0, 0)
+ np.add.reduce([])
+ np.add.reduce(np.zeros((0, 0)), axis=())
+
+ # OTOH, accumulate always makes sense for any combination of n and m,
+ # because it maps an m-element array to an m-element array. These
+ # tests are simpler because accumulate doesn't accept multiple axes.
+ for uf in (np.maximum, np.add):
+ uf.accumulate(np.zeros((30, 0)), axis=0)
+ uf.accumulate(np.zeros((0, 30)), axis=0)
+ uf.accumulate(np.zeros((30, 30)), axis=0)
+ uf.accumulate(np.zeros((0, 0)), axis=0)
+
if __name__ == "__main__":
run_module_suite()