Repository: numpy/numpy

Issue:
Why does `numpy.broadcast` not accept a single input array?
I'm wondering what's the reasoning behind forbidding a single array as input to `numpy.broadcast`. When writing code which is supposed to work in N dimensions with N >= 1, it would be nice to be able to use `broadcast` as-is. A standard use case is the calculation of the shape of an output array when using meshgrids as input for function evaluation:

``` python
>>> coord_vecs = [[1, 2], [3, 4, 5]]
>>> mesh = np.meshgrid(*coord_vecs, indexing='ij', sparse=True)  # for large stuff
>>> out_shape = np.broadcast(*mesh).shape
>>> out_shape
(2, 3)
>>> coord_vecs = [[1, 2]]
>>> mesh = np.meshgrid(*coord_vecs, indexing='ij', sparse=True)  # still works
>>> out_shape = np.broadcast(*mesh).shape
Traceback (most recent call last):
...
ValueError: Need at least two and fewer than (32) array objects.
```

As noted above, `meshgrid` works and returns a list with one element, as expected. Why does `broadcast` not return a broadcast object with shape `(2,)` in the example above? The other public methods of such an object would probably also make sense with a single input array.

EDIT: Last sentence was wrong in the first version.



Discussion:
Apparently, there is [one single place](https://github.com/numpy/numpy/blob/004639d07fd161d1394f5dda1b6ed42c777f3c80/numpy/core/src/multiarray/iterators.c#L1606-L1614) where this situation is caught. Without knowing much about internals, the function around that check seems to be completely generic, and simply replacing 2 with 1 could be the only change necessary to allow single-array(-like) input. No idea about side effects of such a change, though.

Well, maybe worth some playing around, then?

I also find this surprising.

Seems to work right away with the proposed (trivial) change:

``` python
>>> import numpy as np
>>> np.__path__  # My local GH copy
['/home/hkohr/Software/numpy/numpy']
>>> a = np.arange(3)
>>> bc = np.broadcast(a)
>>> bc.nd
1
>>> bc.numiter
1
>>> bc.shape
(3,)
>>> bc.size
3
```

I get three errors when running `np.test()`, but they're probably unrelated. I just post them for the record:

```
======================================================================
ERROR: Failure: ImportError (cannot import name ccompiler)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/loader.py", line 418, in loadTestsFromName
    addr.filename, addr.module)
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/importer.py", line 47, in importFromPath
    return self.importFromDir(dir_path, fqname)
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/importer.py", line 94, in importFromDir
    mod = load_module(part_fqname, fh, filename, desc)
  File "numpy/distutils/__init__.py", line 8, in <module>
    from . import ccompiler
  File "numpy/distutils/ccompiler.py", line 8, in <module>
    from distutils import ccompiler
  File "numpy/distutils/__init__.py", line 8, in <module>
    from . import ccompiler
  File "numpy/distutils/ccompiler.py", line 8, in <module>
    from distutils import ccompiler
ImportError: cannot import name ccompiler

======================================================================
ERROR: test suite for <module 'test_array_from_pyobj' from '/home/hkohr/Software/numpy/numpy/f2py/tests/test_array_from_pyobj.py'>
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/suite.py", line 209, in run
    self.setUp()
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/suite.py", line 292, in setUp
    self.setupContext(ancestor)
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/suite.py", line 315, in setupContext
    try_run(context, names)
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/util.py", line 471, in try_run
    return func()
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/test_array_from_pyobj.py", line 42, in setup
    'test_array_from_pyobj_ext')
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 78, in wrapper
    memo[key] = func(*a, **kw)
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 312, in build_module_distutils
    __import__(module_name)
ImportError: /tmp/tmpqyEUs_/test_array_from_pyobj_ext.so: failed to map segment from shared object

======================================================================
ERROR: test_callback.TestF77Callback.test_string_callback
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/case.py", line 381, in setUp
    try_run(self.inst, ('setup', 'setUp'))
  File "/home/hkohr/.virtualenvs/numpy-py2.7/lib/python2.7/site-packages/nose/util.py", line 471, in try_run
    return func()
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 361, in setUp
    module_name=self.module_name)
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 78, in wrapper
    memo[key] = func(*a, **kw)
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 169, in build_code
    module_name=module_name)
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 78, in wrapper
    memo[key] = func(*a, **kw)
  File "/home/hkohr/Software/numpy/numpy/f2py/tests/util.py", line 149, in build_module
    __import__(module_name)
ImportError: /tmp/tmpqyEUs_/_test_ext_module_5403.so: failed to map segment from shared object

----------------------------------------------------------------------
```

+1 on exploring this change: looking at the code it certainly seems like an unnecessary restriction.

Your test errors do not seem related, do you get the same errors if testing master unchanged? Would be good to put a PR together and see what Travis thinks.


Resolving patch:
diff --git a/numpy/lib/stride_tricks.py b/numpy/lib/stride_tricks.py
--- a/numpy/lib/stride_tricks.py
+++ b/numpy/lib/stride_tricks.py
@@ -121,9 +121,6 @@ def _broadcast_shape(*args):
     """
     if not args:
         raise ValueError('must provide at least one argument')
-    if len(args) == 1:
-        # a single argument does not work with np.broadcast
-        return np.asarray(args[0]).shape
     # use the old-iterator because np.nditer does not handle size 0 arrays
     # consistently
     b = np.broadcast(*args[:32])


Test patch:
diff --git a/numpy/core/tests/test_indexing.py b/numpy/core/tests/test_indexing.py
--- a/numpy/core/tests/test_indexing.py
+++ b/numpy/core/tests/test_indexing.py
@@ -895,10 +895,7 @@ def _get_multi_index(self, arr, indices):
                                     + arr.shape[ax + len(indx[1:]):]))
 
                 # Check if broadcasting works
-                if len(indx[1:]) != 1:
-                    res = np.broadcast(*indx[1:])  # raises ValueError...
-                else:
-                    res = indx[1]
+                res = np.broadcast(*indx[1:])
                 # unfortunately the indices might be out of bounds. So check
                 # that first, and use mode='wrap' then. However only if
                 # there are any indices...
diff --git a/numpy/core/tests/test_numeric.py b/numpy/core/tests/test_numeric.py
--- a/numpy/core/tests/test_numeric.py
+++ b/numpy/core/tests/test_numeric.py
@@ -2207,11 +2207,20 @@ def test_broadcast_in_args(self):
             for a, ia in zip(arrs, mit.iters):
                 assert_(a is ia.base)
 
+    def test_broadcast_single_arg(self):
+        # gh-6899
+        arrs = [np.empty((5, 6, 7))]
+        mit = np.broadcast(*arrs)
+        assert_equal(mit.shape, (5, 6, 7))
+        assert_equal(mit.nd, 3)
+        assert_equal(mit.numiter, 1)
+        assert_(arrs[0] is mit.iters[0].base)
+
     def test_number_of_arguments(self):
         arr = np.empty((5,))
         for j in range(35):
             arrs = [arr] * j
-            if j < 2 or j > 32:
+            if j < 1 or j > 32:
                 assert_raises(ValueError, np.broadcast, *arrs)
             else:
                 mit = np.broadcast(*arrs)