Repository: dask/dask

Issue:
unexpected behavior with da.vstack and broadcasting
I have run into some unexpected behavior with `dask.array.vstack`, which seems like a bug to me.

```python
import dask.array as da
import numpy as np

x = da.arange(12, chunks=100).reshape((4,3))
y = da.vstack([x[:,0], x[:,1], x[:,2]]).T

ans1 = x * [0,0,1]
ans2 = y * [0,0,1]

# this is true
print(np.array_equal(x.compute() * [0,0,1], ans1.compute()))

# this is false
print(np.array_equal(ans1.compute(), ans2.compute()))

# a broadcast appears to have failed; shape is (4,9)
print(ans2.compute().shape)
```
I'm using dask v0.14 here.

Discussion:

Patch:
diff --git a/dask/array/__init__.py b/dask/array/__init__.py
index 23cd3800a..57a6fd8f0 100644
--- a/dask/array/__init__.py
+++ b/dask/array/__init__.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import, division, print_function
 from ..utils import ignoring
 from .core import (Array, stack, concatenate, take, tensordot, transpose,
         from_array, choose, where, coarsen, insert, broadcast_to, ravel,
-        roll, fromfunction, unique, store, squeeze, topk, bincount,
+        roll, fromfunction, unique, store, squeeze, topk, bincount, tile,
         digitize, histogram, map_blocks, atop, to_hdf5, dot, cov, array,
         dstack, vstack, hstack, to_npy_stack, from_npy_stack, compress,
         from_delayed, round, swapaxes, repeat, asarray)
diff --git a/dask/array/core.py b/dask/array/core.py
index 26539919b..b0d9d229f 100644
--- a/dask/array/core.py
+++ b/dask/array/core.py
@@ -2618,6 +2618,8 @@ def elemwise(op, *args, **kwargs):
         msg = "%s does not take the following keyword arguments %s"
         raise TypeError(msg % (op.__name__, str(sorted(set(kwargs) - set(['name', 'dtype'])))))
 
+    args = [np.asarray(a) if isinstance(a, (list, tuple)) else a for a in args]
+
     shapes = [getattr(arg, 'shape', ()) for arg in args]
     shapes = [s if isinstance(s, Iterable) else () for s in shapes]
     out_ndim = len(broadcast_shapes(*shapes))   # Raises ValueError if dimensions mismatch
@@ -3797,7 +3799,7 @@ def swapaxes(a, axis1, axis2):
                 dtype=a.dtype)
 
 
-@wraps(np.dot)
+@wraps(np.repeat)
 def repeat(a, repeats, axis=None):
     if axis is None:
         if a.ndim == 1:
@@ -3838,6 +3840,21 @@ def repeat(a, repeats, axis=None):
     return concatenate(out, axis=axis)
 
 
+@wraps(np.tile)
+def tile(A, reps):
+    if not isinstance(reps, Integral):
+        raise NotImplementedError("Only integer valued `reps` supported.")
+
+    if reps < 0:
+        raise ValueError("Negative `reps` are not allowed.")
+    elif reps == 0:
+        return A[..., :0]
+    elif reps == 1:
+        return A
+
+    return concatenate(reps * [A], axis=-1)
+
+
 def slice_with_dask_array(x, index):
     y = elemwise(getitem, x, index, dtype=x.dtype)
 
diff --git a/docs/source/array-api.rst b/docs/source/array-api.rst
index 4750f8b0d..32503530b 100644
--- a/docs/source/array-api.rst
+++ b/docs/source/array-api.rst
@@ -127,6 +127,7 @@ Top level user functions:
    tan
    tanh
    tensordot
+   tile
    topk
    transpose
    tril


Test patch:
diff --git a/dask/array/tests/test_array_core.py b/dask/array/tests/test_array_core.py
index 7e4fc14ef..b9b185b49 100644
--- a/dask/array/tests/test_array_core.py
+++ b/dask/array/tests/test_array_core.py
@@ -2589,6 +2589,44 @@ def test_repeat():
         assert all(concat(d.repeat(r).chunks))
 
 
+@pytest.mark.parametrize('shape, chunks', [
+    ((10,), (1,)),
+    ((10, 11, 13), (4, 5, 3)),
+])
+@pytest.mark.parametrize('reps', [0, 1, 2, 3, 5])
+def test_tile(shape, chunks, reps):
+    x = np.random.random(shape)
+    d = da.from_array(x, chunks=chunks)
+
+    assert_eq(np.tile(x, reps), da.tile(d, reps))
+
+
+@pytest.mark.parametrize('shape, chunks', [
+    ((10,), (1,)),
+    ((10, 11, 13), (4, 5, 3)),
+])
+@pytest.mark.parametrize('reps', [-1, -5])
+def test_tile_neg_reps(shape, chunks, reps):
+    x = np.random.random(shape)
+    d = da.from_array(x, chunks=chunks)
+
+    with pytest.raises(ValueError):
+        da.tile(d, reps)
+
+
+@pytest.mark.parametrize('shape, chunks', [
+    ((10,), (1,)),
+    ((10, 11, 13), (4, 5, 3)),
+])
+@pytest.mark.parametrize('reps', [[1], [1, 2]])
+def test_tile_array_reps(shape, chunks, reps):
+    x = np.random.random(shape)
+    d = da.from_array(x, chunks=chunks)
+
+    with pytest.raises(NotImplementedError):
+        da.tile(d, reps)
+
+
 def test_concatenate_stack_dont_warn():
     with warnings.catch_warnings(record=True) as record:
         da.concatenate([da.ones(2, chunks=1)] * 62)
@@ -2852,3 +2890,20 @@ def test_atop_with_numpy_arrays():
 
     s = da.sum(x)
     assert any(x is v for v in s.dask.values())
+
+
+@pytest.mark.parametrize('chunks', (100, 6))
+@pytest.mark.parametrize('other', [[0, 0, 1], [2, 1, 3], (0, 0, 1)])
+def test_elemwise_with_lists(chunks, other):
+    x = np.arange(12).reshape((4, 3))
+    d = da.arange(12, chunks=chunks).reshape((4, 3))
+
+    x2 = np.vstack([x[:, 0], x[:, 1], x[:, 2]]).T
+    d2 = da.vstack([d[:, 0], d[:, 1], d[:, 2]]).T
+
+    assert_eq(x2, d2)
+
+    x3 = x2 * other
+    d3 = d2 * other
+
+    assert_eq(x3, d3)