Repository: astropy/astropy

Issue:
astropy.convolution.convolve won't accept pandas.Series as input
The issue:  Pandas Series can be passed to numpy functions just as if they were a numpy array.  However, astropy.convolution.convolve won't accept them.

The workaround: pass the Pandas series to convolve() with .as_matrix(), so that it is sent as a numpy array.  See example below.

The proposed change to Astropy:    convolve() should accept a Pandas Series as input.

Example code:
```python
import astropy.convolution
import numpy as np
import pandas
wave = np.array((np.linspace(5000,5100,10)))
wave_series = pandas.Series(wave)
boxcar = 3
this_works = astropy.convolution.convolve(wave, np.ones((boxcar,))/boxcar)
this_fails  = astropy.convolution.convolve(wave_series) np.ones((boxcar,))/boxcar)
workaround  = astropy.convolution.convolve(wave_series.as_matrix(), np.ones((boxcar,))/boxcar)
```

Discussion:
astropy-bot[bot]: Hi there @eteq :wave: - thanks for the pull request! I'm just a friendly :robot: that checks for issues related to the changelog and making sure that this pull request is milestoned and labeled correctly. This is mainly intended for the maintainers, so if you are not a maintainer you can ignore this, and a maintainer will let you know if any action is required on your part :smiley:.

I noticed the following issue with this pull request:

* Changelog entry not present (or pull request number missing) and neither the **Affects-dev** nor the **no-changelog-entry-needed** label are set

Would it be possible to fix this? Thanks!

*If there are any issues with this message, please report them [here](https://github.com/astropy/astropy-bot/issues).*
eteq: OK, I made the change suggested to catch more specific exceptions, so I think this is gtg if the tests pass.
astrofrog: I wonder whether it would make sense to factor out the checking of the array and kernel for the ``convolve`` and ``convolve_fft`` functions into a single function so that they are exactly the same?
mhvk: :+1: to splitting out common functions.

And, a bit nitpicky, don't call this duck-typing, as it isn't: you're just converting everything to array. Truer ducktyping would be to assume that anything that has an `astype` method would behave sufficiently like an array that it would work in the rest of the code to, i.e.,
```
try:
    array_dtype = array.dtype
    array_internal = array.astype(float, copy=False)
except Exception as exc:
    try:
        array_internal = np.array(array, dtype=float, copy=False)
        array_dtype = array_internal.dtype
    except ...
...
```
eteq: Alright, I followed @mhvk's suggestion of cleaning the if/else up with ``asanyarray``, and indeed it is quite a bit simpler!

> And, a bit nitpicky, don't call this duck-typing

I think it's still duck-typing in that you are assuming the input is something that "behaves like" an array.  So it's duck-typing *by asanyarray*, rather than duck-typing by *this function*.  And I think that's appropriate here because we *want* it to behave this way for e.g. lists, lists-of-lists, or pandas series (the last one being the thing that triggered this in the first place). But this is probably just going deep down a semantic 🐰 🕳 . 😉 

On @astrofrog's suggestion of unifying the inputs... I'm not sure this is possible much more than already is the case.  If you look at the start of the two functions they do quite different things to the arrays and the kernel.  In particular, the arrays immediately get turned into complex numbers in the fft case (which is why this PR makes them *more* consistent - the fft one accepts any array because it has to make it complex). Similarly, the kernel parsing is quite different (although I haven't looked so closely at the details there).  @keflavich might be able to shed some light on this?

More prosaically, I'm not sure how much time I'll have to do more on this in the near future, so would rather see this merged in roughly it's current state (if we're happy with it), and have the unifying be a separate follow-on PR.
eteq: (the circleCI failure seems to be unrelated - there's just one and it seems to be in ``TestTableFunctions.test_mask_array``)
keflavich: There is one relevant difference in how `convolve` and `convolve_fft` handle kernels and images: `convolve` does something completely different if given a `kernel` as the first entry, the `image`: it creates a new `Kernel` instance.  `convolve_fft` has no analogous behavior.  @astrofrog, what was the motivation for this?  I never questioned its presence, but I also don't know its use case.   Should we do the same in `convolve_fft`?  This other behavior also only works in the 1D and 2D cases.
mhvk: @eteq - good to see the `asanyarray` worked.  As for duck-typing (sorry, cannot resist going further off-topic): I think true ducktyping would imply overloading such that you would get back a convolved image of the same class as you put in. Which I don't think is really possible in this case, so definitely fine with what you added.
eteq: (note I did a rebase to make sure the tests are now passing - previously they were failing to what I *think* are reasons unrelated to this PR, but want to make sure...)
pllim: Sphinx warning seems related, but not the SAMP failure.
```
../CHANGES.rst:16: WARNING: Inline literal start-string without end-string.
```

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index db622f8750..8a98fe0219 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,6 +13,9 @@ astropy.constants
 astropy.convolution
 ^^^^^^^^^^^^^^^^^^^
 
+- ``convolve`` now accepts any array-like input, not just ``numpy.ndarray`` or
+  lists. [#7303]
+
 astropy.coordinates
 ^^^^^^^^^^^^^^^^^^^
 
diff --git a/astropy/convolution/convolve.py b/astropy/convolution/convolve.py
index ae353d559c..8c4ce4e328 100644
--- a/astropy/convolution/convolve.py
+++ b/astropy/convolution/convolve.py
@@ -40,7 +40,7 @@ def convolve(array, kernel, boundary='fill', fill_value=0.,
 
     Parameters
     ----------
-    array : `numpy.ndarray` or `~astropy.nddata.NDData`
+    array : `~astropy.nddata.NDData` or `numpy.ndarray` or array-like
         The array to convolve. This should be a 1, 2, or 3-dimensional array
         or a list or a set of nested lists representing a 1, 2, or
         3-dimensional array.  If an `~astropy.nddata.NDData`, the ``mask`` of
@@ -158,17 +158,15 @@ def convolve(array, kernel, boundary='fill', fill_value=0.,
         kernel = kernel.array
 
     # Check that the arguments are lists or Numpy arrays
-
-    if isinstance(array, list):
-        array_internal = np.array(array, dtype=float)
-        array_dtype = array_internal.dtype
-    elif isinstance(array, np.ndarray):
+    try:
         # Note this won't copy if it doesn't have to -- which is okay
         # because none of what follows modifies array_internal.
-        array_dtype = array.dtype
-        array_internal = array.astype(float, copy=False)
-    else:
-        raise TypeError("array should be a list or a Numpy array")
+        array_internal = np.asanyarray(array, dtype=float)
+    except (TypeError, ValueError) as e:
+        raise TypeError('array should be a Numpy array or something '
+                        'convertable into a float array', e)
+    array_dtype = getattr(array, 'dtype', array_internal.dtype)
+
 
     if isinstance(kernel, list):
         kernel_internal = np.array(kernel, dtype=float)


Test patch:
diff --git a/astropy/convolution/tests/test_convolve.py b/astropy/convolution/tests/test_convolve.py
index ba7a6184ba..075721a689 100644
--- a/astropy/convolution/tests/test_convolve.py
+++ b/astropy/convolution/tests/test_convolve.py
@@ -30,6 +30,12 @@ try:
 except ImportError:
     HAS_SCIPY = False
 
+HAS_PANDAS = True
+try:
+    import pandas
+except ImportError:
+    HAS_PANDAS = False
+
 
 class TestConvolve1D:
     def test_list(self):
@@ -751,3 +757,20 @@ def test_astropy_convolution_against_scipy():
                               convolve(y, x, normalize_kernel=False))
     assert_array_almost_equal(fftconvolve(y, x, 'same'),
                               convolve_fft(y, x, normalize_kernel=False))
+
+@pytest.mark.skipif('not HAS_PANDAS')
+def test_regression_6099():
+    wave = np.array((np.linspace(5000, 5100, 10)))
+    boxcar = 3
+    nonseries_result = convolve(wave, np.ones((boxcar,))/boxcar)
+
+    wave_series = pandas.Series(wave)
+    series_result  = convolve(wave_series, np.ones((boxcar,))/boxcar)
+
+    assert_array_almost_equal(nonseries_result, series_result)
+
+def test_invalid_array_convolve():
+    kernel = np.ones(3)/3.
+
+    with pytest.raises(TypeError):
+        convolve('glork', kernel)