Repository: numpy/numpy

Issue:
gradient() no longer accepts scalar-like wrappers for dx
This is with 1.12.0b1. `gradient()` no longer works with scalar values from a package called [pint](http://github.com/hgrecco/pint). With 1.11, this works:
```python
import pint
ureg = pint.UnitRegistry()
u = np.ones((3, 3)) * ureg('m/s')
np.gradient(u, 1 * ureg('m'))
```
With the beta you get:
```pytb
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-1908bac90f0e> in <module>()
      1 u = np.ones((3, 3)) * ureg('m/s')
----> 2 np.gradient(u, 1 * ureg('m'))

/Users/rmay/miniconda3/envs/dev/lib/python3.5/site-packages/numpy/lib/function_base.py in gradient(f, *varargs, **kwargs)
   1596             "invalid number of arguments")
   1597     if any([not np.isscalar(dxi) for dxi in dx]):
-> 1598         raise ValueError("distances must be scalars")
   1599 
   1600     edge_order = kwargs.pop('edge_order', 1)

ValueError: distances must be scalars
```

It looks like the new behavior was introduced by #7618. It seems like `isscalar` failed before for `pint`-based scalar values; the problem is the check being added to gradient. The `pint` scalars behave just like scalars for all purposes, so IMO there should be no problem passing them in.

It should be noted, though, that `gradient` itself has other problems working with the array-like objects from `pint`, and required wrapping gradient to use it anyway. This breaking isn't the end of the world--I just thought you guys should be aware of the impacts.


Discussion:
Any thoughts of ways to improve this?

Aside, I wonder if the generator needs to be in a list?

List is definitely unnecessary.

The ideal solution is probably to make `isscalar` work correctly for this case.

Hmm, making gradient work correctly here could get tricky, especially with unevenly spaced samples as proposed in #8446. For scalars, it is suggested that custom scalar types inherit from `np.generic`. What does `1 * ureg('m')` look like? 
It's just a thin wrapper around a Python integer:

```python
In [1]: import pint
In [2]: import numpy as np
In [3]: ureg = pint.UnitRegistry()

In [4]: 1 * ureg('m')
Out[4]: <Quantity(1, 'meter')>

In [5]: type((1 * ureg('m')).magnitude)
Out[5]: int
```

Seems like `isscalar` should be fixed to accept instances of int and long (Python2). I assume that `isinstance(ureg, int) works, is that correct?
It'd be more like
```python
s = 1 * ureg('m')
isinstance(s, int)
```
But regardless, that last check returns `False`. I think only duck typing would solve that problem--and I understand if that's too much to bite off.
Not sure, but `isscalar` actually checks for scalar objects, not even 0-d arrays would fit that for example. Don't we have some other similar way to achieve this? 
The former example (numpy v.1.10):
x = np.array([0, 1, 2, 3, 4])
dx = np.gradient(x)
y = x**2
np.gradient(y, dx, edge_order=2)
also fails, because dx is always an array of arrays and never an array of scalars if varargs is supplied.
@seberg 

> Not sure, but isscalar actually checks for scalar objects, not even 0-d arrays

Based on this, is it ever appropriate to use `isscalar` in its current form? `isscalar` was the cause of the bug behind #8441. Shouldn't the test just be `np.ndim(x) == 0`, always?
bump...I'm using another package [pynbody](https://github.com/pynbody/pynbody) which implements `SimArrays`, wrappers around numpy arrays that carry units (meters, seconds, etc...).  Allowing zero dimensional arrays in np.gradient would be a big help.  Older versions of numpy worked just fine with this.
@ibackus: This looks like an easy fix to me, making the change I suggest in the comment above. Do you want to have a crack at implementing it?
@eric-wieser: For now I'll have to pass...I've not contributed to numpy before and don't really have the time to getting things set up so that I can do that properly.  all that is required is changing `isscalar` to `np.ndim(x)==0` as you noted.
@eric-wieser, I would fix this but I do not grasp your solution fully. I see that `np.isinstance` is not working properly. Am I right that you are suggesting to get rid of that check and instead check the dimension of x?
Assuming you mean `np.isscalar` and not `isinstance`, then yes - checking dimension == 0 should suffice
Looking into it I found this might already been resolved by #8446?
@maluethi: Good find, but I think the fix is still needed on [this line](https://github.com/numpy/numpy/pull/8446/files#diff-9d44603693b9544b900e135a203b3ae2R1703).

You should be able to test this with something like `np.gradient(np.ones((3, 3)), np.array(1))`

Resolving patch:
diff --git a/numpy/lib/function_base.py b/numpy/lib/function_base.py
--- a/numpy/lib/function_base.py
+++ b/numpy/lib/function_base.py
@@ -1685,23 +1685,28 @@ def gradient(f, *varargs, **kwargs):
     len_axes = len(axes)
     n = len(varargs)
     if n == 0:
+        # no spacing argument - use 1 in all axes
         dx = [1.0] * len_axes
-    elif n == len_axes or (n == 1 and np.isscalar(varargs[0])):
+    elif n == 1 and np.ndim(varargs[0]) == 0:
+        # single scalar for all axes
+        dx = varargs * len_axes
+    elif n == len_axes:
+        # scalar or 1d array for each axis
         dx = list(varargs)
         for i, distances in enumerate(dx):
-            if np.isscalar(distances):
+            if np.ndim(distances) == 0:
                 continue
+            elif np.ndim(distances) != 1:
+                raise ValueError("distances must be either scalars or 1d")
             if len(distances) != f.shape[axes[i]]:
-                raise ValueError("distances must be either scalars or match "
+                raise ValueError("when 1d, distances must match "
                                  "the length of the corresponding dimension")
-            diffx = np.diff(dx[i])
+            diffx = np.diff(distances)
             # if distances are constant reduce to the scalar case
             # since it brings a consistent speedup
             if (diffx == diffx[0]).all():
                 diffx = diffx[0]
             dx[i] = diffx
-        if len(dx) == 1:
-            dx *= len_axes
     else:
         raise TypeError("invalid number of arguments")
 
@@ -1751,7 +1756,7 @@ def gradient(f, *varargs, **kwargs):
         # result allocation
         out = np.empty_like(y, dtype=otype)
 
-        uniform_spacing = np.isscalar(dx[i])
+        uniform_spacing = np.ndim(dx[i]) == 0
 
         # Numerical differentiation: 2nd order interior
         slice1[axis] = slice(1, -1)


Test patch:
diff --git a/numpy/lib/tests/test_function_base.py b/numpy/lib/tests/test_function_base.py
--- a/numpy/lib/tests/test_function_base.py
+++ b/numpy/lib/tests/test_function_base.py
@@ -742,8 +742,11 @@ def test_args(self):
 
         # distances must be scalars or have size equal to gradient[axis]
         gradient(np.arange(5), 3.)
+        gradient(np.arange(5), np.array(3.))
         gradient(np.arange(5), dx)
-        gradient(f_2d, 1.5)  # dy is set equal to dx because scalar
+        # dy is set equal to dx because scalar
+        gradient(f_2d, 1.5)
+        gradient(f_2d, np.array(1.5))
 
         gradient(f_2d, dx_uneven, dx_uneven)
         # mix between even and uneven spaces and
@@ -753,6 +756,10 @@ def test_args(self):
         # 2D but axis specified
         gradient(f_2d, dx, axis=1)
 
+        # 2d coordinate arguments are not yet allowed
+        assert_raises_regex(ValueError, '.*scalars or 1d',
+            gradient, f_2d, np.stack([dx]*2, axis=-1), 1)
+
     def test_badargs(self):
         f_2d = np.arange(25).reshape(5, 5)
         x = np.cumsum(np.ones(5))