Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

BUG: handle length-0 axes correctly in ufunc.reduce without identity

In numpy 1.6, reduction operations with no identity
(e.g. numpy.maximum) gave an error iff they were asked to reduce a
0-element dimension. This regressed during the 1.7 development cycle,
so that they started giving an error if *any* dimension had 0
elements, even ones that were not reduced over. Fixes bug #2078.
  • Loading branch information...
commit a0448b83e6b2efd981ef6ad6689b8b2fafebb834 1 parent 9ef4719
njsmith authored May 20, 2012
27  numpy/core/src/umath/reduction.c
@@ -239,11 +239,12 @@ check_nonreorderable_axes(int ndim, npy_bool *axis_flags, const char *funcname)
239 239
  * it sees along the reduction axes to result, then return a view of
240 240
  * the operand which excludes that element.
241 241
  *
242  
- * If a reduction has an identity, such as 0 or 1, the result should
243  
- * be initialized by calling PyArray_AssignZero(result, NULL, NULL)
244  
- * or PyArray_AssignOne(result, NULL, NULL), because this
245  
- * function raises an exception when there are no elements to reduce.
246  
- *
  242
+ * If a reduction has an identity, such as 0 or 1, the result should be
  243
+ * initialized by calling PyArray_AssignZero(result, NULL, NULL) or
  244
+ * PyArray_AssignOne(result, NULL, NULL), because this function raises an
  245
+ * exception when there are no elements to reduce (which appropriate iff the
  246
+ * reduction operation has no identity).
  247
+ * 
247 248
  * This means it copies the subarray indexed at zero along each reduction axis
248 249
  * into 'result', then returns a view into 'operand' excluding those copied
249 250
  * elements.
@@ -293,14 +294,6 @@ PyArray_InitializeReduceResult(
293 294
         return NULL;
294 295
     }
295 296
 
296  
-    if (PyArray_SIZE(operand) == 0) {
297  
-        PyErr_Format(PyExc_ValueError,
298  
-                "zero-size array to reduction operation %s "
299  
-                "which has no identity",
300  
-                funcname);
301  
-        return NULL;
302  
-    }
303  
-
304 297
     /* Take a view into 'operand' which we can modify. */
305 298
     op_view = (PyArrayObject *)PyArray_View(operand, NULL, &PyArray_Type);
306 299
     if (op_view == NULL) {
@@ -320,6 +313,14 @@ PyArray_InitializeReduceResult(
320 313
     memcpy(shape_orig, shape, ndim * sizeof(npy_intp));
321 314
     for (idim = 0; idim < ndim; ++idim) {
322 315
         if (axis_flags[idim]) {
  316
+            if (shape[idim] == 0) {
  317
+                PyErr_Format(PyExc_ValueError,
  318
+                             "zero-size array to reduction operation %s "
  319
+                             "which has no identity",
  320
+                             funcname);
  321
+                Py_DECREF(op_view);
  322
+                return NULL;
  323
+            }
323 324
             shape[idim] = 1;
324 325
             ++nreduce_axes;
325 326
         }
61  numpy/core/tests/test_ufunc.py
@@ -681,5 +681,66 @@ def test_identityless_reduction_nonreorderable(self):
681 681
 
682 682
         assert_raises(ValueError, np.divide.reduce, a, axis=(0,1))
683 683
 
  684
+    def test_reduce_zero_axis(self):
  685
+        # If we have a n x m array and do a reduction with axis=1, then we are
  686
+        # doing n reductions, and each reduction takes an m-element array. For
  687
+        # a reduction operation without an identity, then:
  688
+        #   n > 0, m > 0: fine
  689
+        #   n = 0, m > 0: fine, doing 0 reductions of m-element arrays
  690
+        #   n > 0, m = 0: can't reduce a 0-element array, ValueError
  691
+        #   n = 0, m = 0: can't reduce a 0-element array, ValueError (for
  692
+        #     consistency with the above case)
  693
+        # This test doesn't actually look at return values, it just checks to
  694
+        # make sure that error we get an error in exactly those cases where we
  695
+        # expect one, and assumes the calculations themselves are done
  696
+        # correctly.
  697
+        def ok(f, *args, **kwargs):
  698
+            f(*args, **kwargs)
  699
+        def err(f, *args, **kwargs):
  700
+            assert_raises(ValueError, f, *args, **kwargs)
  701
+        def t(expect, func, n, m):
  702
+            expect(func, np.zeros((n, m)), axis=1)
  703
+            expect(func, np.zeros((m, n)), axis=0)
  704
+            expect(func, np.zeros((n // 2, n // 2, m)), axis=2)
  705
+            expect(func, np.zeros((n // 2, m, n // 2)), axis=1)
  706
+            expect(func, np.zeros((n, m // 2, m // 2)), axis=(1, 2))
  707
+            expect(func, np.zeros((m // 2, n, m // 2)), axis=(0, 2))
  708
+            expect(func, np.zeros((m // 3, m // 3, m // 3,
  709
+                                  n // 2, n //2)),
  710
+                                 axis=(0, 1, 2))
  711
+            # Check what happens if the inner (resp. outer) dimensions are a
  712
+            # mix of zero and non-zero:
  713
+            expect(func, np.zeros((10, m, n)), axis=(0, 1))
  714
+            expect(func, np.zeros((10, n, m)), axis=(0, 2))
  715
+            expect(func, np.zeros((m, 10, n)), axis=0)
  716
+            expect(func, np.zeros((10, m, n)), axis=1)
  717
+            expect(func, np.zeros((10, n, m)), axis=2)
  718
+        # np.maximum is just an arbitrary ufunc with no reduction identity
  719
+        assert_equal(np.maximum.identity, None)
  720
+        t(ok, np.maximum.reduce, 30, 30)
  721
+        t(ok, np.maximum.reduce, 0, 30)
  722
+        t(err, np.maximum.reduce, 30, 0)
  723
+        t(err, np.maximum.reduce, 0, 0)
  724
+        err(np.maximum.reduce, [])
  725
+        np.maximum.reduce(np.zeros((0, 0)), axis=())
  726
+
  727
+        # all of the combinations are fine for a reduction that has an
  728
+        # identity
  729
+        t(ok, np.add.reduce, 30, 30)
  730
+        t(ok, np.add.reduce, 0, 30)
  731
+        t(ok, np.add.reduce, 30, 0)
  732
+        t(ok, np.add.reduce, 0, 0)
  733
+        np.add.reduce([])
  734
+        np.add.reduce(np.zeros((0, 0)), axis=())
  735
+
  736
+        # OTOH, accumulate always makes sense for any combination of n and m,
  737
+        # because it maps an m-element array to an m-element array. These
  738
+        # tests are simpler because accumulate doesn't accept multiple axes.
  739
+        for uf in (np.maximum, np.add):
  740
+            uf.accumulate(np.zeros((30, 0)), axis=0)
  741
+            uf.accumulate(np.zeros((0, 30)), axis=0)
  742
+            uf.accumulate(np.zeros((30, 30)), axis=0)
  743
+            uf.accumulate(np.zeros((0, 0)), axis=0)
  744
+
684 745
 if __name__ == "__main__":
685 746
     run_module_suite()

0 notes on commit a0448b8

Please sign in to comment.
Something went wrong with that request. Please try again.