Repository: dask/dask

Issue:
Example outer product implementation doesn't work
The docstring for `dask.array.blockwise` contains this example:

```python
z = blockwise(operator.mul, 'ij', x, 'i', y, 'j', dtype='f8')  # doctest: +SKIP
```

However, it doesn't actually work (computing it fails), because `operator.mul` between two 1D chunks produces another 1D chunk, not a 2D outer product of the chunks. I think replacing it with `np.outer` will work, but I'll double-check and make a PR.


Discussion:

Resolving patch:
diff --git a/dask/array/blockwise.py b/dask/array/blockwise.py
--- a/dask/array/blockwise.py
+++ b/dask/array/blockwise.py
@@ -54,17 +54,34 @@ def blockwise(
     --------
     2D embarrassingly parallel operation from two arrays, x, and y.
 
-    >>> z = blockwise(operator.add, 'ij', x, 'ij', y, 'ij', dtype='f8')  # z = x + y  # doctest: +SKIP
-
-    Outer product multiplying x by y, two 1-d vectors
-
-    >>> z = blockwise(operator.mul, 'ij', x, 'i', y, 'j', dtype='f8')  # doctest: +SKIP
+    >>> import operator, numpy as np, dask.array as da
+    >>> x = da.from_array([[1, 2],
+    ...                    [3, 4]], chunks=(1, 2))
+    >>> y = da.from_array([[10, 20],
+    ...                    [0, 0]])
+    >>> z = blockwise(operator.add, 'ij', x, 'ij', y, 'ij', dtype='f8')
+    >>> z.compute()
+    array([[11, 22],
+           [ 3,  4]])
+
+    Outer product multiplying a by b, two 1-d vectors
+
+    >>> a = da.from_array([0, 1, 2], chunks=1)
+    >>> b = da.from_array([10, 50, 100], chunks=1)
+    >>> z = blockwise(np.outer, 'ij', a, 'i', b, 'j', dtype='f8')
+    >>> z.compute()
+    array([[ 0,    0,   0],
+           [ 10,  50, 100],
+           [ 20, 100, 200]])
 
     z = x.T
 
-    >>> z = blockwise(np.transpose, 'ji', x, 'ij', dtype=x.dtype)  # doctest: +SKIP
+    >>> z = blockwise(np.transpose, 'ji', x, 'ij', dtype=x.dtype)
+    >>> z.compute()
+    array([[1, 3],
+           [2, 4]])
 
-    The transpose case above is illustrative because it does same transposition
+    The transpose case above is illustrative because it does transposition
     both on each in-memory block by calling ``np.transpose`` and on the order
     of the blocks themselves, by switching the order of the index ``ij -> ji``.
 
@@ -73,7 +90,10 @@ def blockwise(
 
     z = X + Y.T
 
-    >>> z = blockwise(lambda x, y: x + y.T, 'ij', x, 'ij', y, 'ji', dtype='f8')  # doctest: +SKIP
+    >>> z = blockwise(lambda x, y: x + y.T, 'ij', x, 'ij', y, 'ji', dtype='f8')
+    >>> z.compute()
+    array([[11,  2],
+           [23,  4]])
 
     Any index, like ``i`` missing from the output index is interpreted as a
     contraction (note that this differs from Einstein convention; repeated
@@ -82,31 +102,35 @@ def blockwise(
     index.  To receive arrays concatenated along contracted dimensions instead
     pass ``concatenate=True``.
 
-    Inner product multiplying x by y, two 1-d vectors
+    Inner product multiplying a by b, two 1-d vectors
 
-    >>> def sequence_dot(x_blocks, y_blocks):
+    >>> def sequence_dot(a_blocks, b_blocks):
     ...     result = 0
-    ...     for x, y in zip(x_blocks, y_blocks):
-    ...         result += x.dot(y)
+    ...     for a, b in zip(a_blocks, b_blocks):
+    ...         result += a.dot(b)
     ...     return result
 
-    >>> z = blockwise(sequence_dot, '', x, 'i', y, 'i', dtype='f8')  # doctest: +SKIP
+    >>> z = blockwise(sequence_dot, '', a, 'i', b, 'i', dtype='f8')
+    >>> z.compute()
+    250
 
     Add new single-chunk dimensions with the ``new_axes=`` keyword, including
     the length of the new dimension.  New dimensions will always be in a single
     chunk.
 
-    >>> def f(x):
-    ...     return x[:, None] * np.ones((1, 5))
+    >>> def f(a):
+    ...     return a[:, None] * np.ones((1, 5))
 
-    >>> z = blockwise(f, 'az', x, 'a', new_axes={'z': 5}, dtype=x.dtype)  # doctest: +SKIP
+    >>> z = blockwise(f, 'az', a, 'a', new_axes={'z': 5}, dtype=a.dtype)
 
     New dimensions can also be multi-chunk by specifying a tuple of chunk
     sizes.  This has limited utility as is (because the chunks are all the
     same), but the resulting graph can be modified to achieve more useful
     results (see ``da.map_blocks``).
 
-    >>> z = blockwise(f, 'az', x, 'a', new_axes={'z': (5, 5)}, dtype=x.dtype)  # doctest: +SKIP
+    >>> z = blockwise(f, 'az', a, 'a', new_axes={'z': (5, 5)}, dtype=x.dtype)
+    >>> z.chunks
+    ((1, 1, 1), (5, 5))
 
     If the applied function changes the size of each chunk you can specify this
     with a ``adjust_chunks={...}`` dictionary holding a function for each index
@@ -116,11 +140,16 @@ def blockwise(
     ...     return np.concatenate([x, x])
 
     >>> y = blockwise(double, 'ij', x, 'ij',
-    ...               adjust_chunks={'i': lambda n: 2 * n}, dtype=x.dtype)  # doctest: +SKIP
+    ...               adjust_chunks={'i': lambda n: 2 * n}, dtype=x.dtype)
+    >>> y.chunks
+    ((2, 2), (2,))
 
     Include literals by indexing with None
 
-    >>> y = blockwise(add, 'ij', x, 'ij', 1234, None, dtype=x.dtype)  # doctest: +SKIP
+    >>> z = blockwise(operator.add, 'ij', x, 'ij', 1234, None, dtype=x.dtype)
+    >>> z.compute()
+    array([[1235, 1236],
+           [1237, 1238]])
     """
     out = name
     new_axes = new_axes or {}


Test patch: