Repository: sunpy/sunpy

Issue:
Add map.sample_at_coords()
Currently to sample the data in a map at a series of coordinates, one has to do (e.g. see [here](http://docs.sunpy.org/en/stable/generated/gallery/plotting/great_arc_example.html)):
```python
pixels = np.asarray(np.rint(m.world_to_pixel(great_arc.coordinates())), dtype=int)
x = pixels[0, :]
y = pixels[1, :]
intensity_along_arc = m.data[y, x]
```

It would be nice if there was a method `sample_at_coords(coords)` that automatically did this under the hood.


Discussion:
(this would need checking for astropy 3.1 compatibility)

To make this marginally easier you should be able to do:
```python
pixels = m.wcs.world_to_pixel_array(great_arc.coordinates())
intensity_along_arc = m.data[pixels]
```

[from the top of my head]
While creating the function, following the approach as suggested by @dstansby, I found that this approach fails when we pass a single coordinate, as then `np.rint` throws an error while handling  a single `PixelPair`, but works fine for a list of `PixelPair`
Any suggestions on how to handle this?

Also, the following snippet works in the same way (as suggested by @Cadair )
```python
pixels = np.asarray(np.rint(m.wcs.world_to_pixel(great_arc.coordinates())), dtype='int')
intensity_along_arc2 = m.data[pixels]
```
The implementation of this function should make use of the [`world_to_array_index`](https://github.com/astropy/astropy/blob/master/astropy/wcs/wcsapi/high_level_api.py#L82) method on the WCS object. i.e.

```python
def sample_at_coords(smap, coordinates):
    return smap.data[smap.wcs.world_to_array_index(coordinates)]
```
The fact that it's a one line operation does make me wonder if we actually need a helper function for it? @dstansby ?
Why not? Would make code more readable, and avoid the user having to worry about the `wcs`. I would add it as a method of `GenericMap` instead of a standalone function though.

(as a slight aside, it seems like a lot of the stuff in `map_utils.py` should be methods of `GenericMap` since they just take a map as their first argument anyway)
It was a deliberate choice not to put all those things as methods of `GenericMap`, all be it a rather controversial one at the time.
Was that not why utils was born?
yeah it was.

Resolving patch:
diff --git a/sunpy/map/maputils.py b/sunpy/map/maputils.py
--- a/sunpy/map/maputils.py
+++ b/sunpy/map/maputils.py
@@ -11,9 +11,10 @@
 from sunpy.coordinates import Helioprojective
 
 __all__ = ['all_pixel_indices_from_map', 'all_coordinates_from_map',
-           'map_edges', 'solar_angular_radius', 'contains_full_disk',
-           'is_all_off_disk', 'is_all_on_disk', 'contains_limb',
-           'coordinate_is_on_solar_disk', 'on_disk_bounding_coordinates']
+           'map_edges', 'solar_angular_radius', 'sample_at_coords',
+           'contains_full_disk', 'is_all_off_disk', 'is_all_on_disk',
+           'contains_limb', 'coordinate_is_on_solar_disk',
+           'on_disk_bounding_coordinates']
 
 
 def all_pixel_indices_from_map(smap):
@@ -102,6 +103,27 @@ def solar_angular_radius(coordinates):
     return np.arctan(coordinates.rsun / coordinates.observer.radius)
 
 
+def sample_at_coords(smap, coordinates):
+    """
+    Samples the data in a map at given series of coordinates.
+    Uses nearest-neighbor interpolation of coordinates in map, as
+    it effectively uses array indexing.
+
+    Parameters
+    ----------
+    smap : `~sunpy.map.GenericMap`
+        A SunPy map.
+    coordinates : `~astropy.coordinates.SkyCoord`
+        Input coordinates.
+    Returns
+    -------
+    `numpy.array`
+        A `numpy.array` corresponding to the data obtained from the map,
+        at the input coordinates.
+    """
+    return smap.data[smap.wcs.world_to_array_index(coordinates)]
+
+
 def contains_full_disk(smap):
     """
     Checks if a map contains the full disk of the Sun.


Test patch:
diff --git a/sunpy/map/tests/test_maputils.py b/sunpy/map/tests/test_maputils.py
--- a/sunpy/map/tests/test_maputils.py
+++ b/sunpy/map/tests/test_maputils.py
@@ -7,15 +7,17 @@
 
 from sunpy.map.maputils import (all_pixel_indices_from_map,
                                 all_coordinates_from_map, map_edges, solar_angular_radius,
-                                contains_full_disk, is_all_off_disk,
+                                sample_at_coords, contains_full_disk, is_all_off_disk,
                                 is_all_on_disk, contains_limb,
                                 coordinate_is_on_solar_disk, on_disk_bounding_coordinates)
 from sunpy.coordinates import HeliographicStonyhurst
+from sunpy.coordinates.utils import GreatArc
 import sunpy.data.test
 import sunpy.map
 
 testpath = sunpy.data.test.rootdir
 
+
 @pytest.fixture
 def aia171_test_map():
     return sunpy.map.Map(os.path.join(testpath, 'aia_171_level1.fits'))
@@ -41,6 +43,13 @@ def sub_smap(aia171_test_map):
     return aia171_test_map.submap((0, 0)*u.pix, (50, 60)*u.pix)
 
 
+@pytest.fixture
+def aia_test_arc(aia171_test_map):
+    start = SkyCoord(735 * u.arcsec, -471 * u.arcsec, frame=aia171_test_map.coordinate_frame)
+    end = SkyCoord(-100 * u.arcsec, 800 * u.arcsec, frame=aia171_test_map.coordinate_frame)
+    return GreatArc(start, end)
+
+
 def test_all_pixel_indices_from_map(sub_smap):
     pixel_indices = all_pixel_indices_from_map(sub_smap)
     shape = sub_smap.data.shape
@@ -146,3 +155,14 @@ def test_on_disk_bounding_coordinates(aia171_test_map):
     np.testing.assert_almost_equal(bl.Ty.to(u.arcsec).value, -965.93063472, decimal=1)
     np.testing.assert_almost_equal(tr.Tx.to(u.arcsec).value, 964.27061417, decimal=1)
     np.testing.assert_almost_equal(tr.Ty.to(u.arcsec).value, 971.63586861, decimal=1)
+
+
+def test_data_at_coordinates(aia171_test_map, aia_test_arc):
+    data = sample_at_coords(aia171_test_map, aia_test_arc.coordinates())
+    pixels = np.asarray(np.rint(
+        aia171_test_map.world_to_pixel(aia_test_arc.coordinates())), dtype=int)
+    x = pixels[0, :]
+    y = pixels[1, :]
+    intensity_along_arc = aia171_test_map.data[y, x]
+    np.testing.assert_almost_equal(data[0], intensity_along_arc[0], decimal=1)
+    np.testing.assert_almost_equal(data[-1], intensity_along_arc[-1], decimal=1)