Repository: dask/dask

Issue:
Expected behavior of dask.array.full() for non-scalar fill values?
What is the expected behavior for `dask.array.full()` for a non-scalar `fill_value`?

The `numpy.full()` signature in the documentation mentions "scalar" for `fill_value`.
https://docs.scipy.org/doc/numpy/reference/generated/numpy.full.html

However, it'll broadcast non-scalar values if it can:
```python
a = np.full((3, 2), [1, 2])
```

Since ``dask.array.full()`` wraps this function, it works similarly:
```python
import dask.array

a1 = dask.array.full((2, 3), [[1, 2, 3], [4, 5, 6]])
a1.compute()
```

The issue is that when the chunk sizes change, it won't work anymore, as it starts broadcasting *per chunk*:
```python
a2 = dask.array.full((2, 3), [[1, 2, 3], [4, 5, 6]], chunks=(1, 3))
a2.compute()  # ValueError: could not broadcast input array from shape (2,3) into shape (1,3)
a3 = dask.array.full((2, 3), [1, 2, 3], chunks=(1, 3))
a3.compute()
```

My feeling is that choosing different chunk sizes shouldn't lead to different behavior here. I think I'd prefer a ValueError for a non-scalar fill_value.


Discussion:
Thanks for raising this issue. I think your proposal of raising an Error when the fill_value is not a scalar is quite reasonable but I wouldn't want to break people unnecessarily. Using `full` with a list works with chunks as long as the `fill_value` can be contained in one chunk. 

![image](https://user-images.githubusercontent.com/4806877/79608070-54075980-80c2-11ea-8896-bd4e4408a71e.png)

I do think it'd be better if we can raise an error before calling `.compute`. I'm just not sure how to go about that. For the record that would happen here: https://github.com/dask/dask/blob/8217ab08d258319b61fe0ef27f437a406fd13c3c/dask/array/wrap.py#L58

A few thoughts:
If I take a look at the dask.array API (https://docs.dask.org/en/latest/array-api.html#dask.array), I don't think many of the functions implicitly do this kind of broadcasting? -- So this "chunkwise broadcasting" maybe only affects `full`?

This is probably a naive idea, but:
```python
import dask.array

def full(shape, fill_value, chunks=None):
    return dask.array.broadcast_to(fill_value, shape, chunks=chunks)
```

`broadcast_to` does raise the kind of error we'd be after:

(From your examples):

```python
a1 = full((4, 2, 3), [[1, 2, 3], [4, 5, 6]])
a1.compute()

a2 = full((4, 2, 3), [[1, 2, 3], [4, 5, 6]], chunks=(2, 2, 3))
a2.compute()

a3 = full((4, 2, 3), [[1, 2, 3], [4, 5, 6]], chunks=(2, 2, 1))
```

The last line raises an error immediately, without `.compute`:

```python
ValueError: cannot broadcast chunks ((2,), (3,)) to chunks ((2, 2), (2,), (1, 1, 1)): new chunks must either be along a new dimension or a dimension of size 1
```

(Ignoring the `order` and `dtype` arguments of `full` in this example)
I think we should just error if `fill_value`  isn't a scalar. (I didn't even know numpy supported broadcasting of `fill_value`). The broadcasting behavior only works for dask in extremely specific cases, and only works by accident, not by design.

@Huite, would you be interested in making a PR adding a nice error message for this case?
I'd be happy to oblige, but I'm not sure what the best way of approaching this is, since `full` is currently justs wraps `np.full`:

https://github.com/dask/dask/blob/8217ab08d258319b61fe0ef27f437a406fd13c3c/dask/array/wrap.py#L140

This requires a specific check on `fill_value`, so I don't think there's an elegant solution within the wrapping functions. This is the easiest way I could special case it:

```python
# full() requires special casing due to argument check on fill_value
_full = w(np.full)  # Generate only once


def full(shape, fill_value, dtype=None, order='C'):
    # np.isscalar has somewhat strange behavior: 
    # https://docs.scipy.org/doc/numpy/reference/generated/numpy.isscalar.html 
    if np.ndim(fill_value) != 0:
        raise ValueError(f"fill_value must be scalar. Received {fill_value} instead.")
    return _full(shape, fill_value, dtype, order)


full.__doc__ = _full.__doc__
full.__name__ = _full.__name__
```

Not sure if that's what you had in mind, more or less?


The most elegant fix seems tackling it upstream in numpy. But I'd argue it's not a real issue in numpy anyway (even though the numpy signature explicitly says "scalar"), and it would likely cause significant breakage.
@Huite that looks good. Could you submit a pull request with it?
Yes, will do!

Resolving patch:
diff --git a/dask/array/creation.py b/dask/array/creation.py
--- a/dask/array/creation.py
+++ b/dask/array/creation.py
@@ -26,7 +26,7 @@
 from .utils import AxisError, meta_from_array, zeros_like_safe
 
 
-def empty_like(a, dtype=None, chunks=None):
+def empty_like(a, dtype=None, order="C", chunks=None, name=None):
     """
     Return a new array with the same shape and type as a given array.
 
@@ -37,9 +37,15 @@ def empty_like(a, dtype=None, chunks=None):
         returned array.
     dtype : data-type, optional
         Overrides the data type of the result.
+    order : {'C', 'F'}, optional
+        Whether to store multidimensional data in C- or Fortran-contiguous
+        (row- or column-wise) order in memory.
     chunks : sequence of ints
         The number of samples on each block. Note that the last block will have
         fewer samples if ``len(array) % chunks != 0``.
+    name : str, optional
+        An optional keyname for the array. Defaults to hashing the input
+        keyword arguments.
 
     Returns
     -------
@@ -66,11 +72,13 @@ def empty_like(a, dtype=None, chunks=None):
     return empty(
         a.shape,
         dtype=(dtype or a.dtype),
+        order=order,
         chunks=(chunks if chunks is not None else a.chunks),
+        name=name,
     )
 
 
-def ones_like(a, dtype=None, chunks=None):
+def ones_like(a, dtype=None, order="C", chunks=None, name=None):
     """
     Return an array of ones with the same shape and type as a given array.
 
@@ -81,9 +89,15 @@ def ones_like(a, dtype=None, chunks=None):
         the returned array.
     dtype : data-type, optional
         Overrides the data type of the result.
+    order : {'C', 'F'}, optional
+        Whether to store multidimensional data in C- or Fortran-contiguous
+        (row- or column-wise) order in memory.
     chunks : sequence of ints
         The number of samples on each block. Note that the last block will have
         fewer samples if ``len(array) % chunks != 0``.
+    name : str, optional
+        An optional keyname for the array. Defaults to hashing the input
+        keyword arguments.
 
     Returns
     -------
@@ -103,11 +117,13 @@ def ones_like(a, dtype=None, chunks=None):
     return ones(
         a.shape,
         dtype=(dtype or a.dtype),
+        order=order,
         chunks=(chunks if chunks is not None else a.chunks),
+        name=name,
     )
 
 
-def zeros_like(a, dtype=None, chunks=None):
+def zeros_like(a, dtype=None, order="C", chunks=None, name=None):
     """
     Return an array of zeros with the same shape and type as a given array.
 
@@ -118,9 +134,15 @@ def zeros_like(a, dtype=None, chunks=None):
         the returned array.
     dtype : data-type, optional
         Overrides the data type of the result.
+    order : {'C', 'F'}, optional
+        Whether to store multidimensional data in C- or Fortran-contiguous
+        (row- or column-wise) order in memory.
     chunks : sequence of ints
         The number of samples on each block. Note that the last block will have
         fewer samples if ``len(array) % chunks != 0``.
+    name : str, optional
+        An optional keyname for the array. Defaults to hashing the input
+        keyword arguments.
 
     Returns
     -------
@@ -140,11 +162,13 @@ def zeros_like(a, dtype=None, chunks=None):
     return zeros(
         a.shape,
         dtype=(dtype or a.dtype),
+        order=order,
         chunks=(chunks if chunks is not None else a.chunks),
+        name=name,
     )
 
 
-def full_like(a, fill_value, dtype=None, chunks=None):
+def full_like(a, fill_value, order="C", dtype=None, chunks=None, name=None):
     """
     Return a full array with the same shape and type as a given array.
 
@@ -157,9 +181,15 @@ def full_like(a, fill_value, dtype=None, chunks=None):
         Fill value.
     dtype : data-type, optional
         Overrides the data type of the result.
+    order : {'C', 'F'}, optional
+        Whether to store multidimensional data in C- or Fortran-contiguous
+        (row- or column-wise) order in memory.
     chunks : sequence of ints
         The number of samples on each block. Note that the last block will have
         fewer samples if ``len(array) % chunks != 0``.
+    name : str, optional
+        An optional keyname for the array. Defaults to hashing the input
+        keyword arguments.
 
     Returns
     -------
@@ -181,8 +211,10 @@ def full_like(a, fill_value, dtype=None, chunks=None):
     return full(
         a.shape,
         fill_value,
+        order=order,
         dtype=(dtype or a.dtype),
         chunks=(chunks if chunks is not None else a.chunks),
+        name=name,
     )
 
 
diff --git a/dask/array/wrap.py b/dask/array/wrap.py
--- a/dask/array/wrap.py
+++ b/dask/array/wrap.py
@@ -18,6 +18,7 @@ def _parse_wrap_args(func, args, kwargs, shape):
     if not isinstance(shape, (tuple, list)):
         shape = (shape,)
 
+    name = kwargs.pop("name", None)
     chunks = kwargs.pop("chunks", "auto")
 
     dtype = kwargs.pop("dtype", None)
@@ -26,7 +27,6 @@ def _parse_wrap_args(func, args, kwargs, shape):
     dtype = np.dtype(dtype)
 
     chunks = normalize_chunks(chunks, shape, dtype=dtype)
-    name = kwargs.pop("name", None)
 
     name = name or funcname(func) + "-" + tokenize(
         func, shape, chunks, dtype, args, kwargs
@@ -121,8 +121,8 @@ def wrap(wrap_func, func, **kwargs):
     template = """
     Blocked variant of %(name)s
 
-    Follows the signature of %(name)s exactly except that it also requires a
-    keyword argument chunks=(...)
+    Follows the signature of %(name)s exactly except that it also features
+    optional keyword arguments ``chunks: int, tuple, or dict`` and ``name: str``.
 
     Original signature follows below.
     """
@@ -137,13 +137,39 @@ def wrap(wrap_func, func, **kwargs):
 ones = w(np.ones, dtype="f8")
 zeros = w(np.zeros, dtype="f8")
 empty = w(np.empty, dtype="f8")
-full = w(np.full)
 
 
 w_like = wrap(wrap_func_like_safe)
 
 
-ones_like = w_like(np.ones, func_like=np.ones_like)
-zeros_like = w_like(np.zeros, func_like=np.zeros_like)
 empty_like = w_like(np.empty, func_like=np.empty_like)
-full_like = w_like(np.full, func_like=np.full_like)
+
+
+# full and full_like require special casing due to argument check on fill_value
+# Generate wrapped functions only once
+_full = w(np.full)
+_full_like = w_like(np.full, func_like=np.full_like)
+
+
+def full(shape, fill_value, *args, **kwargs):
+    # np.isscalar has somewhat strange behavior:
+    # https://docs.scipy.org/doc/numpy/reference/generated/numpy.isscalar.html
+    if np.ndim(fill_value) != 0:
+        raise ValueError(
+            f"fill_value must be scalar. Received {type(fill_value).__name__} instead."
+        )
+    return _full(shape=shape, fill_value=fill_value, *args, **kwargs)
+
+
+def full_like(a, fill_value, *args, **kwargs):
+    if np.ndim(fill_value) != 0:
+        raise ValueError(
+            f"fill_value must be scalar. Received {type(fill_value).__name__} instead."
+        )
+    return _full_like(a=a, fill_value=fill_value, *args, **kwargs,)
+
+
+full.__doc__ = _full.__doc__
+full.__name__ = _full.__name__
+full_like.__doc__ = _full_like.__doc__
+full_like.__name__ = _full_like.__name__


Test patch:
diff --git a/dask/array/tests/test_creation.py b/dask/array/tests/test_creation.py
--- a/dask/array/tests/test_creation.py
+++ b/dask/array/tests/test_creation.py
@@ -29,8 +29,10 @@
 @pytest.mark.parametrize("cast_shape", [tuple, list, np.asarray])
 @pytest.mark.parametrize("cast_chunks", [tuple, list, np.asarray])
 @pytest.mark.parametrize("shape, chunks", [((10, 10), (4, 4))])
+@pytest.mark.parametrize("name", [None, "my-name"])
+@pytest.mark.parametrize("order", ["C", "F"])
 @pytest.mark.parametrize("dtype", ["i4"])
-def test_arr_like(funcname, shape, cast_shape, dtype, cast_chunks, chunks):
+def test_arr_like(funcname, shape, cast_shape, dtype, cast_chunks, chunks, name, order):
     np_func = getattr(np, funcname)
     da_func = getattr(da, funcname)
     shape = cast_shape(shape)
@@ -48,11 +50,11 @@ def test_arr_like(funcname, shape, cast_shape, dtype, cast_chunks, chunks):
     if "like" in funcname:
         a = np.random.randint(0, 10, shape).astype(dtype)
 
-        np_r = np_func(a)
-        da_r = da_func(a, chunks=chunks)
+        np_r = np_func(a, order=order)
+        da_r = da_func(a, order=order, chunks=chunks, name=name)
     else:
-        np_r = np_func(shape, dtype=dtype)
-        da_r = da_func(shape, dtype=dtype, chunks=chunks)
+        np_r = np_func(shape, order=order, dtype=dtype)
+        da_r = da_func(shape, order=order, dtype=dtype, chunks=chunks, name=name)
 
     assert np_r.shape == da_r.shape
     assert np_r.dtype == da_r.dtype
@@ -60,6 +62,16 @@ def test_arr_like(funcname, shape, cast_shape, dtype, cast_chunks, chunks):
     if "empty" not in funcname:
         assert (np_r == np.asarray(da_r)).all()
 
+    if name is None:
+        assert funcname.split("_")[0] in da_r.name
+    else:
+        assert da_r.name == name
+
+    if "order" == "F":
+        assert np.isfortran(da_r.compute())
+    else:
+        assert not np.isfortran(da_r.compute())
+
 
 @pytest.mark.parametrize("endpoint", [True, False])
 def test_linspace(endpoint):
diff --git a/dask/array/tests/test_wrap.py b/dask/array/tests/test_wrap.py
--- a/dask/array/tests/test_wrap.py
+++ b/dask/array/tests/test_wrap.py
@@ -42,6 +42,17 @@ def test_full():
     assert a.name.startswith("full-")
 
 
+def test_full_error_nonscalar_fill_value():
+    with pytest.raises(ValueError, match="fill_value must be scalar"):
+        da.full((3, 3), [100, 100], chunks=(2, 2), dtype="i8")
+
+
+def test_full_like_error_nonscalar_fill_value():
+    x = np.full((3, 3), 1, dtype="i8")
+    with pytest.raises(ValueError, match="fill_value must be scalar"):
+        da.full_like(x, [100, 100], chunks=(2, 2), dtype="i8")
+
+
 def test_can_make_really_big_array_of_ones():
     ones((1000000, 1000000), chunks=(100000, 100000))
     ones(shape=(1000000, 1000000), chunks=(100000, 100000))