Repository: sunpy/sunpy

Issue:
Bounds determination for `differential_rotate()` not quite correct
The example that I created for https://github.com/sunpy/sunpy/issues/3323#issuecomment-777575463 suggests that the bounds determination for `differential_rotate()` is not quite correct.  Some data is getting clipped on the left, and there's too much padding on the right.

![download (2)](https://user-images.githubusercontent.com/991759/107733652-83c6c400-6cc9-11eb-8b02-79a61eae3cf9.png)
Generating code:
```python
import matplotlib.pyplot as plt

import astropy.units as u
from astropy.coordinates import SkyCoord

import sunpy.map
from sunpy.data.sample import AIA_171_IMAGE
from sunpy.physics.differential_rotation import differential_rotate

m = sunpy.map.Map(AIA_171_IMAGE)

rectangle = SkyCoord([-700, -650]*u.arcsec, [0, 600]*u.arcsec, frame=m.coordinate_frame)
sm = m.submap(rectangle)

new_observer = m.observer_coordinate.replicate(obstime=m.date + 6*u.day)
dm = differential_rotate(sm, new_observer)

fig = plt.figure(figsize=(12, 4))

ax1 = fig.add_subplot(131, projection=m)
m.plot(vmin=0, vmax=5000, title='Full map')
m.draw_rectangle(rectangle, edgecolor='red', linewidth=2)

ax2 = fig.add_subplot(132, projection=sm)
sm.plot(vmin=0, vmax=5000, title='Submap')

ax3 = fig.add_subplot(133, projection=dm)
dm.plot(vmin=0, vmax=5000, title='Diffrot of submap by 6 days')
ax3.plot_coord(dm.center, '+', color='red', label='map center')
ax3.legend()

plt.show()
```

Reconcile new RotatedSunFrame with differential_rotate
Now we have #3537 in we have the ability to replace [`differential_rotate`](https://docs.sunpy.org/en/stable/api/sunpy.physics.differential_rotation.differential_rotate.html#sunpy.physics.differential_rotation.differential_rotate) with reproject + `RotatedSunFrame`.

We should pin down any differences between these two approaches and re-write `differential_rotate` to use reproject underneath.


Discussion:
What would be the idea observation in the case of ```differential_rotate``` after 6 days? If you don't mind, can I have a go at this issue? 
I am not sure this is a problem worth solving, at least not in the form the code is currently in. A much more useful investment of time would be to calculate what the correct output WCS is and then use reproject to do the warp. This is obviously a reasonably similar problem to the one the current algorithm solves, but presented in quite a different way (if my memory of the current code serves).
@jeffreypaul15 I've fallen down a deep rabbit hole with the current code.  I recommend keeping far away from this issue...
Here's the specific comment with a comparison and generating code: https://github.com/sunpy/sunpy/pull/3537#issuecomment-559194239

And the image from that comment:
![comparison](https://user-images.githubusercontent.com/991759/69748078-916c4a00-1115-11ea-8a04-7f15cc7086e3.png)

Don't focus on the differences near the old limb, but rather at the (very slight) differences in the neighborhood the new meridian.

Resolving patch:
diff --git a/sunpy/coordinates/metaframes.py b/sunpy/coordinates/metaframes.py
--- a/sunpy/coordinates/metaframes.py
+++ b/sunpy/coordinates/metaframes.py
@@ -79,7 +79,7 @@ def rotatedsun_to_rotatedsun(from_rotatedsun_coord, to_rotatedsun_frame):
     def reference_to_rotatedsun(reference_coord, rotatedsun_frame):
         # Transform to HCI
         hci_frame = HeliocentricInertial(obstime=rotatedsun_frame.base.obstime)
-        hci_coord = reference_coord.transform_to(hci_frame)
+        hci_coord = reference_coord.transform_to(hci_frame)  # obstime change handled here
         oldrepr = hci_coord.spherical
 
         # Rotate the coordinate in HCI
@@ -100,7 +100,8 @@ def reference_to_rotatedsun(reference_coord, rotatedsun_frame):
     def rotatedsun_to_reference(rotatedsun_coord, reference_frame):
         # Transform to HCI
         from_coord = rotatedsun_coord.base.realize_frame(rotatedsun_coord.data)
-        hci_coord = from_coord.transform_to(HeliocentricInertial(obstime=reference_frame.obstime))
+        hci_frame = HeliocentricInertial(obstime=rotatedsun_coord.base.obstime)
+        hci_coord = from_coord.transform_to(hci_frame)
         oldrepr = hci_coord.spherical
 
         # Rotate the coordinate in HCI
@@ -113,8 +114,8 @@ def rotatedsun_to_reference(rotatedsun_coord, reference_frame):
         newrepr = SphericalRepresentation(newlon, oldrepr.lat, oldrepr.distance)
 
         # Transform back from HCI
-        hci_coord = HeliocentricInertial(newrepr, obstime=reference_frame.obstime)
-        return hci_coord.transform_to(reference_frame)
+        hci_coord = hci_frame.realize_frame(newrepr)
+        return hci_coord.transform_to(reference_frame)  # obstime change handled here
 
     _rotatedsun_cache[framecls] = _RotatedSunFramecls
     return _RotatedSunFramecls
diff --git a/sunpy/physics/differential_rotation.py b/sunpy/physics/differential_rotation.py
--- a/sunpy/physics/differential_rotation.py
+++ b/sunpy/physics/differential_rotation.py
@@ -8,6 +8,7 @@
 from astropy.time import TimeDelta
 
 from sunpy.coordinates import Heliocentric, HeliographicStonyhurst, Helioprojective
+from sunpy.coordinates.transformations import transform_with_sun_center
 from sunpy.map import (
     contains_full_disk,
     coordinate_is_on_solar_disk,
@@ -252,6 +253,11 @@ def solar_rotate_coordinate(coordinate, observer=None, time=None, **diff_rot_kwa
         The locations of the input coordinates after the application of
         solar rotation as seen from the point-of-view of the new observer.
 
+    Notes
+    -----
+    The translational motion of the Sun over the time interval will be ignored.
+    See :func:`~sunpy.coordinates.transform_with_sun_center`.
+
     Example
     -------
     >>> import astropy.units as u
@@ -270,8 +276,8 @@ def solar_rotate_coordinate(coordinate, observer=None, time=None, **diff_rot_kwa
     >>> new_observer = get_body("earth", end_time)
     >>> solar_rotate_coordinate(c, observer=new_observer)
     <SkyCoord (Helioprojective: obstime=2010-09-11T13:34:56.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate (obstime=2010-09-11T13:34:56.000): (lon, lat, radius) in (deg, deg, AU)
-        (-5.08888749e-14, 7.24318962, 1.00669016)>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
-        (-363.04027419, 104.87807178, 1.00241935)>
+        (-5.68434189e-14, 7.24318962, 1.00669016)>): (Tx, Ty, distance) in (arcsec, arcsec, AU)
+        (-378.27830452, 105.70767875, 1.00245134)>
     """
     # Check the input and create the new observer
     new_observer = _get_new_observer(coordinate.obstime, observer, time)
@@ -297,14 +303,16 @@ def solar_rotate_coordinate(coordinate, observer=None, time=None, **diff_rot_kwa
         heliographic_rotated = SkyCoord(heliographic_coordinate.lon + drot,
                                         heliographic_coordinate.lat,
                                         heliographic_coordinate.radius,
-                                        obstime=new_observer.obstime,
-                                        observer=new_observer,
+                                        obstime=coordinate.obstime,
                                         frame=HeliographicStonyhurst)
 
-        # Calculate where the rotated co-ordinate appears as seen by new observer,
-        # and then transform it into the co-ordinate system of the input
-        # co-ordinate.
-        return heliographic_rotated.transform_to(coordinate.frame.name)
+        # Calculate where the rotated co-ordinate appears as seen by new observer
+        # for the co-ordinate system of the input co-ordinate.  The translational
+        # motion of the Sun will be ignored for the transformation.
+        frame_newobs = coordinate.frame.replicate_without_data(observer=new_observer,
+                                                               obstime=new_observer.obstime)
+        with transform_with_sun_center():
+            return heliographic_rotated.transform_to(frame_newobs)
 
 
 def _rotate_submap_edge(smap, pixels, observer, **diff_rot_kwargs):
@@ -429,6 +437,11 @@ def _warp_sun_coordinates(xy, smap, new_observer, **diff_rot_kwargs):
     xy2 : `numpy.ndarray`
         Pixel coordinates in the map corresponding to the input pixels in the
         warped image.
+
+    Notes
+    -----
+    The translational motion of the Sun over the time interval will be ignored.
+    See :func:`~sunpy.coordinates.transform_with_sun_center`.
     """
     # Suppress NaN warnings in coordinate transforms
     with warnings.catch_warnings():
@@ -452,9 +465,6 @@ def _warp_sun_coordinates(xy, smap, new_observer, **diff_rot_kwargs):
                                      frame=Helioprojective)
 
         heliographic_coordinate = output_hpc_coords.transform_to(HeliographicStonyhurst)
-        # Now transform the HGS coordinates to the obstime of the input map (to account for movement of Earth)
-        heliographic_coordinate = heliographic_coordinate.transform_to(
-            HeliographicStonyhurst(obstime=smap.date))
 
         # Compute the differential rotation.
         drot = diff_rot(interval, heliographic_coordinate.lat.to(u.degree), **diff_rot_kwargs)
@@ -467,12 +477,13 @@ def _warp_sun_coordinates(xy, smap, new_observer, **diff_rot_kwargs):
                                  obstime=heliographic_coordinate.obstime,
                                  frame=HeliographicStonyhurst)
 
-        # As seen from the map observer, which coordinates are on disk and which are behind the Sun.
-        where_off_disk_from_map_observer = rotated_coord.transform_to(
-            Heliocentric(observer=smap.observer_coordinate)).z.value < 0
+        with transform_with_sun_center():
+            # As seen from the map observer, which coordinates are behind the Sun.
+            where_off_disk_from_map_observer = rotated_coord.transform_to(
+                Heliocentric(observer=smap.observer_coordinate)).z.value < 0
 
-        # Re-project the pixels which are on disk back to location of the original observer
-        coordinates_at_map_observer = rotated_coord.transform_to(smap.coordinate_frame)
+            # Re-project the pixels which are on disk back to location of the original observer
+            coordinates_at_map_observer = rotated_coord.transform_to(smap.coordinate_frame)
 
         # Go back to pixel co-ordinates
         x2, y2 = smap.world_to_pixel(coordinates_at_map_observer)
@@ -536,6 +547,11 @@ def differential_rotate(smap, observer=None, time=None, **diff_rot_kwargs):
     `~sunpy.map.GenericMap`
         A map with the result of applying solar differential rotation to the
         input map.
+
+    Notes
+    -----
+    The translational motion of the Sun over the time interval will be ignored.
+    See :func:`~sunpy.coordinates.transform_with_sun_center`.
     """
     # If the entire map is off-disk, return an error so the user is aware.
     if is_all_off_disk(smap):


Test patch:
diff --git a/sunpy/coordinates/tests/test_metaframes.py b/sunpy/coordinates/tests/test_metaframes.py
--- a/sunpy/coordinates/tests/test_metaframes.py
+++ b/sunpy/coordinates/tests/test_metaframes.py
@@ -232,3 +232,30 @@ def test_rotatedsun_transforms(frame, lon, lat, obstime, rotated_time1, rotated_
     assert_quantity_allclose(result3.lat, result1.lat)
     # Use the `spherical` property since the name of the component varies with frame
     assert_quantity_allclose(result3.spherical.distance, result1.spherical.distance)
+
+
+@pytest.mark.parametrize("indirect_fixture",
+                         ["rot_hgs", "rot_hci"], indirect=True)
+def test_obstime_change(indirect_fixture):
+    base_class, rot_frame = indirect_fixture
+
+    # Transforming a RotatedSun coordinate to a different obstime should give the same answer
+    # as first transforming to its base and then transforming to the different obstime
+    new_frame = base_class(obstime='2001-01-31')
+    new_implicit = rot_frame.transform_to(new_frame)
+    new_explicit = rot_frame.transform_to(rot_frame.base).transform_to(new_frame)
+
+    assert_longitude_allclose(new_implicit.lon, new_explicit.lon, atol=1e-10*u.deg)
+
+
+@pytest.mark.parametrize("indirect_fixture",
+                         ["rot_hgs", "rot_hci"], indirect=True)
+def test_obstime_change_loopback(indirect_fixture):
+    base_class, rot_frame = indirect_fixture
+
+    # Doing a explicit loopback transformation through an intermediate frame with a different
+    # obstime should get back to the original coordinate
+    int_frame = base_class(obstime='2001-01-31')
+    loopback = rot_frame.transform_to(int_frame).transform_to(rot_frame)
+
+    assert_longitude_allclose(loopback.lon, rot_frame.lon, atol=1e-10*u.deg)
diff --git a/sunpy/physics/tests/test_differential_rotation.py b/sunpy/physics/tests/test_differential_rotation.py
--- a/sunpy/physics/tests/test_differential_rotation.py
+++ b/sunpy/physics/tests/test_differential_rotation.py
@@ -12,6 +12,8 @@
 import sunpy.map
 from sunpy.coordinates import frames
 from sunpy.coordinates.ephemeris import get_earth
+from sunpy.coordinates.metaframes import RotatedSunFrame
+from sunpy.coordinates.transformations import transform_with_sun_center
 from sunpy.map.maputils import map_edges
 from sunpy.physics.differential_rotation import (
     _get_bounding_coordinates,
@@ -168,14 +170,35 @@ def test_solar_rotate_coordinate():
         assert isinstance(d, SkyCoord)
 
         # Test the coordinate
-        np.testing.assert_almost_equal(d.Tx.to(u.arcsec).value, -371.8885208634674, decimal=1)
-        np.testing.assert_almost_equal(d.Ty.to(u.arcsec).value, 105.35006656251727, decimal=1)
-        np.testing.assert_allclose(d.distance.to(u.km).value, 1.499642e+08, rtol=1e-5)
+        np.testing.assert_almost_equal(d.Tx.to(u.arcsec).value, -386.4519332773052, decimal=1)
+        np.testing.assert_almost_equal(d.Ty.to(u.arcsec).value, 106.1647811048218, decimal=1)
+        np.testing.assert_allclose(d.distance.to(u.km).value, 1.499689e+08, rtol=1e-5)
 
         # Test that the SkyCoordinate is Helioprojective
         assert isinstance(d.frame, frames.Helioprojective)
 
 
+def test_consistency_with_rotatedsunframe():
+    old_observer = frames.HeliographicStonyhurst(10*u.deg, 20*u.deg, 1*u.AU, obstime='2001-01-01')
+    new_observer = frames.HeliographicStonyhurst(30*u.deg, 40*u.deg, 2*u.AU, obstime='2001-01-08')
+
+    hpc_coord = SkyCoord(100*u.arcsec, 200*u.arcsec, frame='helioprojective',
+                         observer=old_observer, obstime=old_observer.obstime)
+
+    # Perform the differential rotation using solar_rotate_coordinate()
+    result1 = solar_rotate_coordinate(hpc_coord, observer=new_observer)
+
+    # Perform the differential rotation using RotatedSunFrame, with translational motion of the Sun
+    # ignored using transform_with_sun_center()
+    rsf_coord = RotatedSunFrame(base=hpc_coord, rotated_time=new_observer.obstime)
+    with transform_with_sun_center():
+        result2 = rsf_coord.transform_to(result1.replicate_without_data())
+
+    assert_quantity_allclose(result1.Tx, result2.Tx)
+    assert_quantity_allclose(result1.Ty, result2.Ty)
+    assert_quantity_allclose(result1.distance, result2.distance)
+
+
 # Testing using observer inputs
 def test_differential_rotate_observer_all_off_disk(all_off_disk_map):
     # Test a map that is entirely off the disk of the Sun
diff --git a/sunpy/physics/tests/test_solar_rotation.py b/sunpy/physics/tests/test_solar_rotation.py
--- a/sunpy/physics/tests/test_solar_rotation.py
+++ b/sunpy/physics/tests/test_solar_rotation.py
@@ -43,15 +43,15 @@ def aia171_test_mapsequence(aia171_test_submap):
 # Known displacements for these mapsequence layers when the layer index is set to 0
 @pytest.fixture
 def known_displacements_layer_index0():
-    return {'x': np.asarray([1.136868e-13, -1.048550e+01, -2.099555e+01]),
-            'y': np.asarray([1.136868e-13, 2.685361e-01, 5.230608e-01])}
+    return {'x': np.asarray([0., -9.827465, -19.676442]),
+            'y': np.asarray([0., 0.251137, 0.490014])}
 
 
 # Known displacements for these mapsequence layers when the layer index is set to 1
 @pytest.fixture
 def known_displacements_layer_index1():
-    return {'x': np.asarray([1.045963e+01, 8.526513e-14, -1.048549e+01]),
-            'y': np.asarray([-2.825134e-01, 1.136868e-13, 2.685360e-01])}
+    return {'x': np.asarray([9.804878, 0., -9.827465]),
+            'y': np.asarray([-0.263369, 0., 0.251137])}
 
 
 def test_calculate_solar_rotate_shift(aia171_test_mapsequence, known_displacements_layer_index0, known_displacements_layer_index1):