Repository: sunpy/sunpy

Issue:
Rename `GenericMap.shift` to `GenericMap.shift_pointing`
### Provide a general description of the issue or problem.

This is a useful method that I only recently realized existed. However, I think the name is misleading. If I have a map `m` and I call `m.shift(dx,dy)`, I would naively expect the actual image data to be shifted. However, this function is not doing that. It is shifting the pointing by applying shift to the reference coordinate, thereby shifting the pointing.

I would recommend that we

* rename this function `shift_pointing`
* make `shift` an alias of `shift_pointing`
* deprecate `shift` in 4.0 so we can remove it in 4.1

Additionally, we also keep track of the modifications to the pointing by incrementing `GenericMap.shifted_value` every time `shift` is called. However, I'm not sure how useful this actually is as soon you write this map to disk, all record of this shift is lost. I'd also be inclined to deprecate `shifted_value` but I'm happy to hear arguments against it.


Discussion:
How about being even more explicit and renaming to `shift_reference_coord`? Then it's super clear what the method does (and what it doesn't). To me `pointing` is a vague term/concept, but if others find it meaningful/well defined then I'm fine going with that 👍 
That's a good point. I usually think of "pointing" as the correspondence between `CRPIX` and `CRVAL`. `shift_pointing` might imply that you're altering both. 

Resolving patch:
diff --git a/sunpy/io/special/asdf/converters/generic_map.py b/sunpy/io/special/asdf/converters/generic_map.py
--- a/sunpy/io/special/asdf/converters/generic_map.py
+++ b/sunpy/io/special/asdf/converters/generic_map.py
@@ -1,6 +1,5 @@
 import numpy as np
 
-import astropy.units as u
 from asdf.extension import Converter
 
 __all__ = ["GenericMapConverter"]
@@ -12,25 +11,36 @@ class GenericMapConverter(Converter):
     @property
     def types(self):
         from sunpy.map.mapbase import GenericMap
-        return list(GenericMap._registry.keys())
+        return [GenericMap] + list(GenericMap._registry.keys())
+
+    def select_tag(self, obj, tags, ctx):
+        # Sort the tags in reverse alphabetical order and pick the first (i.e.
+        # the one with the highest version). This assumes that all the tags for
+        # this converter are named the same other than the version number.
+        tags = list(sorted(tags, reverse=True))
+        return tags[0]
 
     def from_yaml_tree(self, node, tag, ctx):
+        import astropy.units as u
+
         import sunpy.map
 
         # Use the factory here to get the correct subclass back
         out_map = sunpy.map.Map(np.asanyarray(node["data"]), node["meta"])
-        out_map = out_map.shift(*node["shift"])
-
         out_map.mask = node.get("mask")
         out_map.uncertainty = node.get("uncertainty")
         out_map._unit = node.get("unit")
+
+        # Old files can have a shift that needs applying
+        if 'generic_map-1.0.0' in tag and not np.allclose(node['shift'], 0 * u.deg):
+            out_map = out_map.shift_reference_coord(*node['shift'])
+
         return out_map
 
     def to_yaml_tree(self, smap, tag, ctx):
         node = {}
         node["data"] = np.asarray(smap.data)
         node["meta"] = dict(smap.meta)
-        node["shift"] = u.Quantity(smap.shifted_value)
         if smap.mask is not None:
             node["mask"] = smap.mask
         if smap.uncertainty is not None:
diff --git a/sunpy/map/mapbase.py b/sunpy/map/mapbase.py
--- a/sunpy/map/mapbase.py
+++ b/sunpy/map/mapbase.py
@@ -50,8 +50,9 @@
     add_common_docstring,
     cached_property_based_on,
     check_arithmetic_compatibility,
+    deprecated,
 )
-from sunpy.util.exceptions import warn_metadata, warn_user
+from sunpy.util.exceptions import warn_deprecated, warn_metadata, warn_user
 from sunpy.util.functools import seconddispatch
 from sunpy.util.util import _figure_to_base64
 from sunpy.visualization import axis_labels_from_ctype, peek_show, wcsaxes_compat
@@ -1010,15 +1011,32 @@ def center(self):
     def shifted_value(self):
         """The total shift applied to the reference coordinate by past applications of
         `~sunpy.map.GenericMap.shift`."""
+        warn_deprecated(
+            '`sunpy.map.GenericMap.shifted_value` is deprecated and will be removed in sunpy 4.1. '
+            'Use ``sunpy.map.GenericMap.meta.modified_items`` to see how the '
+            'reference coordinate has been modified.'
+        )
         return self._shift
 
+    @deprecated('4.0', alternative='`sunpy.map.GenericMap.shift_reference_coord`')
     @u.quantity_input
     def shift(self, axis1: u.deg, axis2: u.deg):
+        # Note that the doc redirection for this method is at the end of the file.
+
+        new_map = self.shift_reference_coord(axis1, axis2)
+        new_map._shift = SpatialPair(self.shifted_value[0] + axis1,
+                                     self.shifted_value[1] + axis2)
+        return new_map
+
+    @u.quantity_input
+    def shift_reference_coord(self, axis1: u.deg, axis2: u.deg):
         """
         Returns a map shifted by a specified amount to, for example, correct
         for a bad map location. These values are applied directly to the
-        `~sunpy.map.GenericMap.reference_coordinate`. To check how much shift
-        has already been applied see `~sunpy.map.GenericMap.shifted_value`
+        `~sunpy.map.GenericMap.reference_coordinate`. To check how much the
+        reference coordinate has been modified, see
+        ``sunpy.map.GenericMap.meta.modified_items['CRVAL1']`` and
+        ``sunpy.map.GenericMap.meta.modified_items['CRVAL2']``.
 
         Parameters
         ----------
@@ -1041,9 +1059,6 @@ def shift(self, axis1: u.deg, axis2: u.deg):
         # Create new map with the modification
         new_map = self._new_instance(self.data, new_meta, self.plot_settings)
 
-        new_map._shift = SpatialPair(self.shifted_value[0] + axis1,
-                                     self.shifted_value[1] + axis2)
-
         return new_map
 
     def _rsun_meters(self, dsun=None):
@@ -2710,6 +2725,7 @@ def reproject_to(self, target_wcs, algorithm='interpolation', return_footprint=F
 
 
 GenericMap.__doc__ += textwrap.indent(_notes_doc, "    ")
+GenericMap.shift.__doc__ = GenericMap.shift_reference_coord.__doc__
 
 
 class InvalidHeaderInformation(ValueError):


Test patch:
diff --git a/sunpy/data/test/_generate_asdf_test.py b/sunpy/data/test/_generate_asdf_test.py
new file mode 100644
--- /dev/null
+++ b/sunpy/data/test/_generate_asdf_test.py
@@ -0,0 +1,23 @@
+"""
+This file will generate an asdf file in the test data, using the newest schema version.
+"""
+import asdf
+import astropy.units as u
+
+from sunpy.data.test import rootdir
+
+
+def generate_asdf_tree(obj, filename):
+    tree = {"object": obj}
+    with asdf.AsdfFile(tree) as af:
+        # TODO: Automatically determine filename based on tag used
+        af.write_to(rootdir / filename)
+
+
+if __name__ == "__main__":
+    import sunpy.map
+    test_map = rootdir / "aia_171_level1.fits"
+    obj = sunpy.map.Map(test_map)
+    obj = obj.resample((2, 2)*u.pix)
+    #obj = obj.shift(10*u.arcsec, 0*u.arcsec)
+    generate_asdf_tree(obj, "aiamap_genericmap_1.0.0.asdf")
diff --git a/sunpy/data/test/aiamap_genericmap_1.0.0.asdf b/sunpy/data/test/aiamap_genericmap_1.0.0.asdf
new file mode 100644
Binary files /dev/null and b/sunpy/data/test/aiamap_genericmap_1.0.0.asdf differ
diff --git a/sunpy/data/test/aiamap_shift_genericmap_1.0.0.asdf b/sunpy/data/test/aiamap_shift_genericmap_1.0.0.asdf
new file mode 100644
Binary files /dev/null and b/sunpy/data/test/aiamap_shift_genericmap_1.0.0.asdf differ
diff --git a/sunpy/io/special/asdf/tests/test_genericmap.py b/sunpy/io/special/asdf/tests/test_genericmap.py
--- a/sunpy/io/special/asdf/tests/test_genericmap.py
+++ b/sunpy/io/special/asdf/tests/test_genericmap.py
@@ -1,6 +1,7 @@
 import numpy as np
 import pytest
 
+import asdf
 import astropy.units as u
 
 import sunpy.map
@@ -12,13 +13,10 @@
 def assert_roundtrip_map(old):
     new = roundtrip_object(old)
     np.testing.assert_allclose(old.data, new.data)
-
     # Test the meta by force!
     for ok, ov in old.meta.items():
         assert ok in new.meta
         assert new.meta[ok] == ov
-
-    assert u.allclose(old.shifted_value, new.shifted_value)
     if old.mask is not None and new.mask is not None:
         np.testing.assert_allclose(old.mask, new.mask)
     assert old.unit == new.unit
@@ -37,11 +35,29 @@ def test_genericmap_basic(aia171_test_map, tmpdir):
 
 @asdf_entry_points
 def test_genericmap_mask(aia171_test_map, tmpdir):
-
     mask = np.zeros_like(aia171_test_map.data)
     mask[10, 10] = 1
-
     aia171_test_map.mask = mask
     aia171_test_map._unit = u.m
-
     assert_roundtrip_map(aia171_test_map)
+
+
+@asdf_entry_points
+def test_load_100_file_with_shift():
+    fname = get_test_filepath("aiamap_shift_genericmap_1.0.0.asdf")
+    with asdf.open(fname) as af:
+        aiamap = af['object']
+        assert isinstance(aiamap, sunpy.map.sources.AIAMap)
+        assert "crval1" in aiamap.meta.modified_items
+        crval1 = aiamap.meta.modified_items["crval1"]
+        assert crval1.current - crval1.original == 10
+
+
+@asdf_entry_points
+def test_load_100_file_with_no_shift():
+    fname = get_test_filepath("aiamap_genericmap_1.0.0.asdf")
+    with asdf.open(fname) as af:
+        aiamap = af['object']
+        assert isinstance(aiamap, sunpy.map.sources.AIAMap)
+        assert "crval1" not in aiamap.meta.modified_items
+        assert "crval2" not in aiamap.meta.modified_items
diff --git a/sunpy/map/tests/test_mapbase.py b/sunpy/map/tests/test_mapbase.py
--- a/sunpy/map/tests/test_mapbase.py
+++ b/sunpy/map/tests/test_mapbase.py
@@ -464,6 +464,19 @@ def test_save_compressed(aia171_test_map):
     assert isinstance(loaded_save, sunpy.map.sources.AIAMap)
 
 
+DEP_WARNING_SHIFTED_VAL = (
+    'ignore:`sunpy.map.GenericMap.shifted_value` is deprecated and will be removed in sunpy 4.1. '
+    'Use ``sunpy.map.GenericMap.meta.modified_items`` to see how the reference coordinate has been '
+    'modified.:sunpy.util.exceptions.SunpyDeprecationWarning'
+)
+DEP_WARNING_SHIFT = (
+    'ignore:The shift function is deprecated and may be removed in version 4.1.'
+    r'\s+Use `sunpy.map.GenericMap.shift_reference_coord` instead.'
+    ':sunpy.util.exceptions.SunpyDeprecationWarning'
+)
+
+
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFTED_VAL)
 def test_default_shift():
     """Test that the default shift is zero"""
     data = np.ones([6, 6], dtype=np.float64)
@@ -490,6 +503,8 @@ def test_default_shift():
     assert cd_map.shifted_value[1].value == 0
 
 
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFT)
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFTED_VAL)
 def test_shift_applied(generic_map):
     """Test that adding a shift actually updates the reference coordinate"""
     original_reference_coord = (generic_map.reference_coordinate.Tx,
@@ -500,23 +515,28 @@ def test_shift_applied(generic_map):
     assert shifted_map.reference_coordinate.Tx - x_shift == original_reference_coord[0]
     assert shifted_map.reference_coordinate.Ty - y_shift == original_reference_coord[1]
     crval1 = ((generic_map.meta.get('crval1') * generic_map.spatial_units[0] +
-               shifted_map.shifted_value[0]).to(shifted_map.spatial_units[0])).value
+               x_shift).to(shifted_map.spatial_units[0])).value
     assert shifted_map.meta.get('crval1') == crval1
     crval2 = ((generic_map.meta.get('crval2') * generic_map.spatial_units[1] +
-               shifted_map.shifted_value[1]).to(shifted_map.spatial_units[1])).value
+               y_shift).to(shifted_map.spatial_units[1])).value
     assert shifted_map.meta.get('crval2') == crval2
 
 
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFT)
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFTED_VAL)
 def test_set_shift(generic_map):
     """Test that previously applied shift is stored in the shifted_value property"""
     x_shift = 5 * u.arcsec
     y_shift = 13 * u.arcsec
     shifted_map = generic_map.shift(x_shift, y_shift)
-    resultant_shift = shifted_map.shifted_value
-    assert resultant_shift[0] == x_shift
-    assert resultant_shift[1] == y_shift
+    mod_crval1 = shifted_map.meta.modified_items['crval1']
+    mod_crval2 = shifted_map.meta.modified_items['crval2']
+    assert x_shift == (mod_crval1.current - mod_crval1.original) * shifted_map.spatial_units[0]
+    assert y_shift == (mod_crval2.current - mod_crval2.original) * shifted_map.spatial_units[1]
 
 
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFT)
+@pytest.mark.filterwarnings(DEP_WARNING_SHIFTED_VAL)
 def test_shift_history(generic_map):
     """Test the shifted_value is added to a non-zero previous shift"""
     x_shift1 = 5 * u.arcsec
@@ -527,9 +547,12 @@ def test_shift_history(generic_map):
     y_shift2 = 120 * u.arcsec
     final_shifted_map = shifted_map1.shift(x_shift2, y_shift2)
 
-    resultant_shift = final_shifted_map.shifted_value
-    assert resultant_shift[0] == x_shift1 + x_shift2
-    assert resultant_shift[1] == y_shift1 + y_shift2
+    mod_crval1 = final_shifted_map.meta.modified_items['crval1']
+    mod_crval2 = final_shifted_map.meta.modified_items['crval2']
+    delta_crval1 = (mod_crval1.current - mod_crval1.original) * final_shifted_map.spatial_units[0]
+    delta_crval2 = (mod_crval2.current - mod_crval2.original) * final_shifted_map.spatial_units[1]
+    assert x_shift1 + x_shift2 == delta_crval1
+    assert y_shift1 + y_shift2 == delta_crval2
 
 
 def test_corners(simple_map):