Repository: sunpy/sunpy

Issue:
Add coordinate-system checks to maputils functions
Lots of `sunpy/map/maputils.py` assumes that a helioprojective map is being passed. Therefore, if (e.g.) a synoptic map is passed into lots of those functions, I suspect there will be lots of unhelpful errors. We should

- Create a helper function `is_helioprojective_map` that returns `True` if a map is helioprojective, `False` otherwise
- Use this check in `maputil` functions
- Raise a user-friendly error if the check fails


Discussion:
I changed the title of this issue to reflect its contents.  A separate issue can be created for enhancing `maputils` functions as possible to support synoptic maps.

I'll note that there are other projected coordinate systems beyond helioprojective (e.g., any equatorial-like system using RA/dec) that all `maputils` functions in principle can support.  One sticking point is that `Helioprojective` carries with it the radius of the Sun to use, and other coordinate frames do not.
To check if a map is helioprojective or not, is the following adequate?
```python
>>> from sunpy.coordinates import Helioprojective
>>> isinstance(map.coordinate_frame, Helioprojective)
```
Yup.  One `maputils` function already has such a check:
https://github.com/sunpy/sunpy/blob/332dc4ebd91e1ae62f10253c3e7ff81c06aa04fe/sunpy/map/maputils.py#L225-L226
Would it be better to have the function check if the frame passed is helioprojective or not, as opposed to the map passed? As in, have the function `is_helioprojective_frame`. I'm asking because some functions require a check if the coordinates (not maps) they use are helioprojective or not. This function would then be able to handle both - frames of coordinates and maps.

Resolving patch:
diff --git a/sunpy/map/maputils.py b/sunpy/map/maputils.py
--- a/sunpy/map/maputils.py
+++ b/sunpy/map/maputils.py
@@ -80,7 +80,21 @@ def map_edges(smap):
     return top, bottom, left_hand_side, right_hand_side
 
 
-@u.quantity_input
+def _verify_coordinate_helioprojective(coordinates):
+    """
+    Raises an error if the coordinate is not in the
+    `~sunpy.coordinates.frames.Helioprojective` frame.
+
+    Parameters
+    ----------
+    coordinates : `~astropy.coordinates.SkyCoord`, `~astropy.coordinates.BaseCoordinateFrame`
+    """
+    frame = coordinates.frame if hasattr(coordinates, 'frame') else coordinates
+    if not isinstance(frame, Helioprojective):
+        raise ValueError(f"The input coordinate(s) is of type {type(frame).__name__}, "
+                         "but must be in the Helioprojective frame.")
+
+
 def solar_angular_radius(coordinates):
     """
     Calculates the solar angular radius as seen by the observer.
@@ -101,6 +115,7 @@ def solar_angular_radius(coordinates):
     angle : `~astropy.units.Quantity`
         The solar angular radius.
     """
+    _verify_coordinate_helioprojective(coordinates)
     return sun._angular_radius(coordinates.rsun, coordinates.observer.radius)
 
 
@@ -165,6 +180,7 @@ def contains_full_disk(smap):
     within the field of the view of the instrument (although no emission
     from the disk itself is present in the data.)
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     edge_of_world = _edge_coordinates(smap)
     # Calculate the distance of the edge of the world in solar radii
     coordinate_angles = np.sqrt(edge_of_world.Tx ** 2 + edge_of_world.Ty ** 2)
@@ -192,6 +208,7 @@ def contains_solar_center(smap):
     bool
         True if the map contains the solar center.
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     bottom_left = smap.pixel_to_world(-0.5 * u.pix, -0.5 * u.pix)
     top_right = smap.pixel_to_world(*(u.Quantity(smap.dimensions) - 0.5 * u.pix))
     # Test if the x and y component of the coordinate changes sign along
@@ -221,9 +238,7 @@ def coordinate_is_on_solar_disk(coordinates):
     `~bool`
         Returns `True` if the coordinate is on disk, `False` otherwise.
     """
-
-    if not isinstance(coordinates.frame, Helioprojective):
-        raise ValueError('The input coordinate(s) must be in the Helioprojective Cartesian frame.')
+    _verify_coordinate_helioprojective(coordinates)
     # Calculate the angle of every pixel from the center of the Sun and compare it the angular
     # radius of the Sun.
     return np.sqrt(coordinates.Tx ** 2 + coordinates.Ty ** 2) < solar_angular_radius(coordinates)
@@ -253,6 +268,7 @@ def is_all_off_disk(smap):
     within the field of view of the instrument, but the solar disk itself is not imaged.
     For such images this function will return `False`.
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     edge_of_world = _edge_coordinates(smap)
     # Calculate the distance of the edge of the world in solar radii
     coordinate_angles = np.sqrt(edge_of_world.Tx ** 2 + edge_of_world.Ty ** 2)
@@ -282,6 +298,7 @@ def is_all_on_disk(smap):
         Returns `True` if all map coordinates have an angular radius less than
         the angular radius of the Sun.
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     edge_of_world = _edge_coordinates(smap)
     return np.all(coordinate_is_on_solar_disk(edge_of_world))
 
@@ -314,6 +331,7 @@ def contains_limb(smap):
     within the field of view of the instrument, but the solar disk itself is not imaged.
     For such images this function will return `True`.
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     if contains_full_disk(smap):
         return True
     on_disk = coordinate_is_on_solar_disk(_edge_coordinates(smap))
@@ -338,6 +356,7 @@ def on_disk_bounding_coordinates(smap):
         top right coordinate of the smallest rectangular region that contains
         all the on-disk pixels in the input map.
     """
+    _verify_coordinate_helioprojective(smap.coordinate_frame)
     # Check that the input map is not all off disk.
     if is_all_off_disk(smap):
         raise ValueError("The entire map is off disk.")


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
@@ -9,12 +9,15 @@
 import sunpy.data.test
 import sunpy.map
 from sunpy.coordinates import HeliographicStonyhurst
+from sunpy.coordinates.frames import HeliographicCarrington
 from sunpy.coordinates.utils import GreatArc
 from sunpy.map.maputils import (
+    _verify_coordinate_helioprojective,
     all_coordinates_from_map,
     all_pixel_indices_from_map,
     contains_full_disk,
     contains_limb,
+    contains_solar_center,
     coordinate_is_on_solar_disk,
     is_all_off_disk,
     is_all_on_disk,
@@ -52,6 +55,24 @@ def sub_smap(aia171_test_map):
     return aia171_test_map.submap((0, 0)*u.pix, top_right=(50, 60)*u.pix)
 
 
+@pytest.fixture
+def non_helioprojective_map():
+    data = np.arange(0, 100).reshape(10, 10)
+    coord = SkyCoord(0*u.arcsec, 0*u.arcsec, obstime='2013-10-28 08:24',
+                     observer='earth', frame=HeliographicCarrington)
+    header = sunpy.map.header_helper.make_fitswcs_header(data, coord,
+                                                         reference_pixel=[0, 0]*u.pixel,
+                                                         scale=[2, 2]*u.arcsec/u.pixel,
+                                                         telescope='Fake Telescope', instrument='UV detector',
+                                                         wavelength=1000*u.angstrom)
+    return sunpy.map.Map(data, header)
+
+
+@pytest.fixture
+def non_helioprojective_skycoord():
+    return SkyCoord(0 * u.rad, 0 * u.rad, frame="icrs")
+
+
 @pytest.fixture
 def aia_test_arc(aia171_test_map):
     start = SkyCoord(735 * u.arcsec, -471 * u.arcsec, frame=aia171_test_map.coordinate_frame)
@@ -175,3 +196,46 @@ def test_data_at_coordinates(aia171_test_map, aia_test_arc):
     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)
+
+
+def test_contains_solar_center(aia171_test_map, all_off_disk_map, all_on_disk_map, straddles_limb_map, sub_smap):
+    assert contains_solar_center(aia171_test_map)
+    assert not contains_solar_center(all_off_disk_map)
+    assert not contains_solar_center(all_on_disk_map)
+    assert not contains_solar_center(straddles_limb_map)
+    assert not contains_solar_center(sub_smap)
+
+
+def test_verify_coordinate_helioprojective(aia171_test_map, all_off_disk_map, all_on_disk_map, straddles_limb_map,
+                                           sub_smap, non_helioprojective_map, non_helioprojective_skycoord):
+    # These should be helioprojective.
+    _verify_coordinate_helioprojective(aia171_test_map.coordinate_frame)
+    _verify_coordinate_helioprojective(all_off_disk_map.coordinate_frame)
+    _verify_coordinate_helioprojective(all_on_disk_map.coordinate_frame)
+    _verify_coordinate_helioprojective(straddles_limb_map.coordinate_frame)
+    _verify_coordinate_helioprojective(sub_smap.coordinate_frame)
+    # These are not.
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        _verify_coordinate_helioprojective(non_helioprojective_map.coordinate_frame)
+    with pytest.raises(ValueError, match=r"ICRS, .* Helioprojective"):
+        _verify_coordinate_helioprojective(non_helioprojective_skycoord)
+
+
+def test_functions_raise_non_frame_coords(non_helioprojective_skycoord):
+    with pytest.raises(ValueError, match=r"ICRS, .* Helioprojective"):
+        solar_angular_radius(non_helioprojective_skycoord)
+    with pytest.raises(ValueError, match=r"ICRS, .* Helioprojective"):
+        coordinate_is_on_solar_disk(non_helioprojective_skycoord)
+
+
+def test_functions_raise_non_frame_map(non_helioprojective_map):
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        contains_full_disk(non_helioprojective_map)
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        contains_solar_center(non_helioprojective_map)
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        is_all_off_disk(non_helioprojective_map)
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        contains_limb(non_helioprojective_map)
+    with pytest.raises(ValueError, match=r"HeliographicCarrington, .* Helioprojective"):
+        on_disk_bounding_coordinates(non_helioprojective_map)