Repository: sunpy/sunpy

Issue:
Observer-based RotatedSunFrame can't transform to a non-observer-based frame
```python
>>> from astropy import units as u
>>> from astropy.coordinates import SkyCoord

>>> from sunpy.coordinates import HeliographicStonyhurst, Helioprojective, RotatedSunFrame

>>> r = RotatedSunFrame(0*u.arcsec, 0*u.arcsec, base=Helioprojective(observer='earth', obstime='2021-01-01'))
>>> r
<RotatedSunHelioprojective Coordinate (base=<Helioprojective Frame (obstime=2021-01-01T00:00:00.000, rsun=695700.0 km, observer=<HeliographicStonyhurst Coordinate for 'earth'>)>, duration=0.0 d, rotation_model=howard): (Tx, Ty) in arcsec
    (0., 0.)>

>>> r.transform_to(HeliographicStonyhurst(obstime=r.base.obstime))
...
ConvertError: This transformation cannot be performed because the Heliocentric frame has observer=None.
```
This is because neither `RotatedSunHelioprojective` nor `HeliographicStonyhurst` have the `observer` frame attribute, but the HCC->HGS transformation step needs to have a defined observer.  (The `observer` frame attribute that you see in the repr for `RotatedSunHelioprojective` is a frame attribute of the `base` frame attribute.)  There are several approaches to fix this, but I need to investigate which approach makes the most sense.


Discussion:

Resolving patch:
diff --git a/sunpy/coordinates/metaframes.py b/sunpy/coordinates/metaframes.py
--- a/sunpy/coordinates/metaframes.py
+++ b/sunpy/coordinates/metaframes.py
@@ -12,7 +12,7 @@
 from sunpy.time import parse_time
 from sunpy.time.time import _variables_for_parse_time_docstring
 from sunpy.util.decorators import add_common_docstring
-from .frames import BaseHeliographic, HeliocentricInertial, SunPyBaseCoordinateFrame
+from .frames import BaseHeliographic, HeliographicStonyhurst, SunPyBaseCoordinateFrame
 from .offset_frame import NorthOffsetFrame
 from .transformations import _transformation_debug
 
@@ -69,20 +69,19 @@ def baseheliographic_init(self, *args, **kwargs):
     def rotatedsun_to_rotatedsun(from_rotatedsun_coord, to_rotatedsun_frame):
         """Transform between two rotated-Sun frames."""
         # This transform goes through the parent frames on each side.
-        # from_frame -> from_frame.base -> to_frame.base -> to_frame
-        intermediate_from = from_rotatedsun_coord.transform_to(from_rotatedsun_coord.base)
-        intermediate_to = intermediate_from.transform_to(to_rotatedsun_frame.base)
-        return intermediate_to.transform_to(to_rotatedsun_frame)
-
-    @frame_transform_graph.transform(FunctionTransform, framecls, _RotatedSunFramecls)
-    @_transformation_debug(f"{framecls.__name__}->{_RotatedSunFramecls.__name__}")
-    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)  # obstime change handled here
-        oldrepr = hci_coord.spherical
-
-        # Rotate the coordinate in HCI
+        # from_frame -> HGS -> to_frame
+        int_frame = HeliographicStonyhurst(obstime=from_rotatedsun_coord.base.obstime)
+        int_coord = from_rotatedsun_coord.transform_to(int_frame)
+        return int_coord.transform_to(to_rotatedsun_frame)
+
+    @frame_transform_graph.transform(FunctionTransform, HeliographicStonyhurst, _RotatedSunFramecls)
+    @_transformation_debug(f"HGS->{_RotatedSunFramecls.__name__}")
+    def reference_to_rotatedsun(hgs_coord, rotatedsun_frame):
+        int_frame = HeliographicStonyhurst(obstime=rotatedsun_frame.base.obstime)
+        int_coord = hgs_coord.transform_to(int_frame)  # obstime change handled here
+        oldrepr = int_coord.spherical
+
+        # Rotate the coordinate in HGS
         from sunpy.physics.differential_rotation import diff_rot
         log.debug(f"Applying {rotatedsun_frame.duration} of solar rotation")
         newlon = oldrepr.lon - diff_rot(rotatedsun_frame.duration,
@@ -91,20 +90,20 @@ def reference_to_rotatedsun(reference_coord, rotatedsun_frame):
                                         frame_time='sidereal')
         newrepr = SphericalRepresentation(newlon, oldrepr.lat, oldrepr.distance)
 
-        # Transform back from HCI
-        new_coord = hci_coord.realize_frame(newrepr).transform_to(rotatedsun_frame.base)
+        # Transform from HGS
+        new_coord = int_coord.realize_frame(newrepr).transform_to(rotatedsun_frame.base)
         return rotatedsun_frame.realize_frame(new_coord.data)
 
-    @frame_transform_graph.transform(FunctionTransform, _RotatedSunFramecls, framecls)
-    @_transformation_debug(f"{_RotatedSunFramecls.__name__}->{framecls.__name__}")
-    def rotatedsun_to_reference(rotatedsun_coord, reference_frame):
-        # Transform to HCI
+    @frame_transform_graph.transform(FunctionTransform, _RotatedSunFramecls, HeliographicStonyhurst)
+    @_transformation_debug(f"{_RotatedSunFramecls.__name__}->HGS")
+    def rotatedsun_to_reference(rotatedsun_coord, hgs_frame):
+        # Transform to HGS
         from_coord = rotatedsun_coord.base.realize_frame(rotatedsun_coord.data)
-        hci_frame = HeliocentricInertial(obstime=rotatedsun_coord.base.obstime)
-        hci_coord = from_coord.transform_to(hci_frame)
-        oldrepr = hci_coord.spherical
+        int_frame = HeliographicStonyhurst(obstime=rotatedsun_coord.base.obstime)
+        int_coord = from_coord.transform_to(int_frame)
+        oldrepr = int_coord.spherical
 
-        # Rotate the coordinate in HCI
+        # Rotate the coordinate in HGS
         from sunpy.physics.differential_rotation import diff_rot
         log.debug(f"Applying {rotatedsun_coord.duration} of solar rotation")
         newlon = oldrepr.lon + diff_rot(rotatedsun_coord.duration,
@@ -113,9 +112,9 @@ def rotatedsun_to_reference(rotatedsun_coord, reference_frame):
                                         frame_time='sidereal')
         newrepr = SphericalRepresentation(newlon, oldrepr.lat, oldrepr.distance)
 
-        # Transform back from HCI
-        hci_coord = hci_frame.realize_frame(newrepr)
-        return hci_coord.transform_to(reference_frame)  # obstime change handled here
+        # Transform from HGS
+        int_coord = int_frame.realize_frame(newrepr)
+        return int_coord.transform_to(hgs_frame)  # obstime change handled here
 
     _rotatedsun_cache[framecls] = _RotatedSunFramecls
     return _RotatedSunFramecls


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
@@ -9,6 +9,7 @@
 import sunpy.coordinates.frames as f
 from sunpy.coordinates.metaframes import RotatedSunFrame, _rotatedsun_cache
 from sunpy.physics.differential_rotation import diff_rot
+from sunpy.sun import constants
 from .helpers import assert_longitude_allclose
 from .strategies import latitudes, longitudes, times
 
@@ -92,8 +93,8 @@ def test_class_creation(indirect_fixture):
 
     # Check that one-leg transformations have been created
     assert len(frame_transform_graph.get_transform(rot_class, rot_class).transforms) == 1
-    assert len(frame_transform_graph.get_transform(base_class, rot_class).transforms) == 1
-    assert len(frame_transform_graph.get_transform(rot_class, base_class).transforms) == 1
+    assert len(frame_transform_graph.get_transform(f.HeliographicStonyhurst, rot_class).transforms) == 1
+    assert len(frame_transform_graph.get_transform(rot_class, f.HeliographicStonyhurst).transforms) == 1
 
     # Check that the base frame is in the cache
     assert base_class in _rotatedsun_cache
@@ -201,6 +202,11 @@ def test_rotatedsun_transforms(frame, lon, lat, obstime, rotated_time1, rotated_
     else:
         base = frame(lon=lon, lat=lat, obstime=obstime)
 
+    # Map any 2D coordinate to the surface of the Sun
+    if base.spherical.distance.unit is u.one and u.allclose(base.spherical.distance, 1*u.one):
+        newrepr = base.spherical * constants.radius
+        base = base.realize_frame(newrepr)
+
     # Test the RotatedSunFrame->base transformation
     rsf1 = RotatedSunFrame(base=base, rotated_time=rotated_time1)
     result1 = rsf1.transform_to(base)
@@ -259,3 +265,14 @@ def test_obstime_change_loopback(indirect_fixture):
     loopback = rot_frame.transform_to(int_frame).transform_to(rot_frame)
 
     assert_longitude_allclose(loopback.lon, rot_frame.lon, atol=1e-10*u.deg)
+
+
+@pytest.mark.parametrize("indirect_fixture",
+                         ["rot_hgs", "rot_hgc", "rot_hci", "rot_hcc", "rot_hpc", "rot_hme"], indirect=True)
+def test_tranformation_to_nonobserver_frame(indirect_fixture):
+    base_class, rot_frame = indirect_fixture
+
+    hgs_frame = f.HeliographicStonyhurst(obstime='2020-01-01')
+    hgs_coord = rot_frame.transform_to(hgs_frame)
+
+    assert hgs_coord.obstime == hgs_frame.obstime