Repository: sunpy/sunpy

Issue:
Deprecate changing solar-{x,y} CTYPE
Originally suggested in https://github.com/sunpy/sunpy/pull/5501#discussion_r724886420. Currently we do the following correction:

```python
        if ctype1.lower() in ("solar-x", "solar_x"):
            ctype1 = 'HPLN-TAN'

        if ctype2.lower() in ("solar-y", "solar_y"):
            ctype2 = 'HPLT-TAN'
```
We should deprecate this, and eventually remove it. If any particular map sources depend on this fix, it should move into the map source.

Discussion:
tanishy7777: 
pre-commit.ci autofix


ayshih: Er, do we know why the `oldestdeps` CI run is failing?
tanishy7777: > Er, do we know why the `oldestdeps` CI run is failing?

to test that `mapbase.py` was giving deprecation warnings I had added the following code in `test_mapbase.py`

```python
  def test_coordinate_system_solar_x_solar_y(generic_map):
        generic_map.meta['ctype1'] = 'SOLAR-X'
        generic_map.meta['ctype2'] = 'SOLAR-Y'
       with pytest.warns(SunpyDeprecationWarning, match="CTYPE1 value 'solar-x'/'solar_x' is deprecated") 
```

but for some reason the `SunpyDeprecationWarning` warning is not generated on 
![image](https://github.com/user-attachments/assets/27406f15-dc0d-4fb2-a4e2-e95b2489cdf2)
oldestdeps 

nabobalis: Should be fixed now.
tanishy7777: yep now its working
nabobalis: > yep now its working. Why did you remove the request for review? @nabobalis

Because only the two humans requested are needed.

Patch:
diff --git a/changelog/7953.bugfix.rst b/changelog/7953.bugfix.rst
new file mode 100644
index 000000000..5674ce55a
--- /dev/null
+++ b/changelog/7953.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed a bug in :func:`~sunpy.physics.differential_rotation.differential_rotate` that assumed the input map header had ``RSUN_REF`` defined.
diff --git a/changelog/7955.deprecation.rst b/changelog/7955.deprecation.rst
new file mode 100644
index 000000000..f13d93cea
--- /dev/null
+++ b/changelog/7955.deprecation.rst
@@ -0,0 +1,1 @@
+General support within `~sunpy.map.Map` for the non-standard CTYPE values 'SOLAR-X' and 'SOLAR-Y' is now deprecated, and should instead be handled by a source-specific subclass.
diff --git a/changelog/8002.bugfix.rst b/changelog/8002.bugfix.rst
new file mode 100644
index 000000000..b5bf698fd
--- /dev/null
+++ b/changelog/8002.bugfix.rst
@@ -0,0 +1,1 @@
+Examples in docs for `sunpy.net.attr.Attr` are now rendering properly.
diff --git a/sunpy/map/mapbase.py b/sunpy/map/mapbase.py
index 6ee91a4b8..a0e26a4ab 100644
--- a/sunpy/map/mapbase.py
+++ b/sunpy/map/mapbase.py
@@ -55,7 +55,7 @@ from sunpy.util.decorators import (
     deprecate_positional_args_since,
     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, fix_duplicate_notes
 from sunpy.visualization import axis_labels_from_ctype, peek_show, wcsaxes_compat
@@ -1268,9 +1268,11 @@ class GenericMap(NDData):
         # changes it to a ctype that is understood.  See Thompson, 2006, A.&A.,
         # 449, 791.
         if ctype1.lower() in ("solar-x", "solar_x"):
+            warn_deprecated("CTYPE1 value 'solar-x'/'solar_x' is deprecated, use 'HPLN-TAN' instead.")
             ctype1 = 'HPLN-TAN'
 
         if ctype2.lower() in ("solar-y", "solar_y"):
+            warn_deprecated("CTYPE2 value 'solar-y'/'solar_y' is deprecated, use 'HPLN-TAN' instead.")
             ctype2 = 'HPLT-TAN'
 
         return SpatialPair(ctype1, ctype2)
diff --git a/sunpy/map/sources/hinode.py b/sunpy/map/sources/hinode.py
index 8e5bfc925..7cd59a992 100644
--- a/sunpy/map/sources/hinode.py
+++ b/sunpy/map/sources/hinode.py
@@ -2,7 +2,7 @@
 import astropy.units as u
 from astropy.visualization import ImageNormalize, LogStretch
 
-from sunpy.map import GenericMap
+from sunpy.map.mapbase import GenericMap, SpatialPair
 from sunpy.map.sources.source_type import source_stretch
 
 __author__ = ["Jack Ireland, Jose Ivan Campos-Rozo, David Perez-Suarez"]
@@ -60,6 +60,18 @@ class XRTMap(GenericMap):
         self.plot_settings['norm'] = ImageNormalize(
             stretch=source_stretch(self.meta, LogStretch()), clip=False)
 
+    @property
+    def coordinate_system(self):
+        """
+        Override the default implementation to handle SOTMap-specific logic for CTYPE values.
+        """
+        ctype1, ctype2 = self.meta['ctype1'], self.meta['ctype2']
+        if ctype1.lower() in ("solar-x", "solar_x"):
+            ctype1 = 'HPLN-TAN'
+        if ctype2.lower() in ("solar-y", "solar_y"):
+            ctype2 = 'HPLT-TAN'
+        return SpatialPair(ctype1, ctype2)
+
     @property
     def _timesys(self):
         if self.meta.get('timesys', '').upper() == 'UTC (TBR)':
@@ -168,6 +180,18 @@ class SOTMap(GenericMap):
 
         self.plot_settings['cmap'] = 'hinodesot' + color[self.instrument]
 
+    @property
+    def coordinate_system(self):
+        """
+        Override the default implementation to handle SOTMap-specific logic for CTYPE values.
+        """
+        ctype1, ctype2 = self.meta['ctype1'], self.meta['ctype2']
+        if ctype1.lower() in ("solar-x", "solar_x"):
+            ctype1 = 'HPLN-TAN'
+        if ctype2.lower() in ("solar-y", "solar_y"):
+            ctype2 = 'HPLT-TAN'
+        return SpatialPair(ctype1, ctype2)
+
     @property
     def detector(self):
         return "SOT"
diff --git a/sunpy/map/sources/soho.py b/sunpy/map/sources/soho.py
index 49d458f59..be15a26bb 100644
--- a/sunpy/map/sources/soho.py
+++ b/sunpy/map/sources/soho.py
@@ -42,6 +42,18 @@ class EITMap(GenericMap):
         self.plot_settings['norm'] = ImageNormalize(
             stretch=source_stretch(self.meta, PowerStretch(0.5)), clip=False)
 
+    @property
+    def coordinate_system(self):
+        """
+        Override the default implementation of coordinate_system to handle EITMAP-specific logic for CTYPE values.
+        """
+        ctype1, ctype2 = self.meta['ctype1'], self.meta['ctype2']
+        if ctype1.lower() in ("solar-x", "solar_x"):
+            ctype1 = 'HPLN-TAN'
+        if ctype2.lower() in ("solar-y", "solar_y"):
+            ctype2 = 'HPLT-TAN'
+        return SpatialPair(ctype1, ctype2)
+
     @property
     def date(self):
         # Old EIT data has date-obs in format of dd-JAN-yy so we use date_obs where available
@@ -113,6 +125,18 @@ class LASCOMap(GenericMap):
         self.plot_settings['norm'] = ImageNormalize(
             stretch=source_stretch(self.meta, PowerStretch(0.5)), clip=False)
 
+    @property
+    def coordinate_system(self):
+        """
+        Override the default implementation to handle LASCOMAP-specific logic for CTYPE values.
+        """
+        ctype1, ctype2 = self.meta['ctype1'], self.meta['ctype2']
+        if ctype1.lower() in ("solar-x", "solar_x"):
+            ctype1 = 'HPLN-TAN'
+        if ctype2.lower() in ("solar-y", "solar_y"):
+            ctype2 = 'HPLT-TAN'
+        return SpatialPair(ctype1, ctype2)
+
     @property
     def spatial_units(self):
         return SpatialPair(u.Unit(self.meta.get('cunit1').lower()),
diff --git a/sunpy/map/sources/trace.py b/sunpy/map/sources/trace.py
index 00a701ff7..695c5e6d0 100644
--- a/sunpy/map/sources/trace.py
+++ b/sunpy/map/sources/trace.py
@@ -55,6 +55,18 @@ class TRACEMap(GenericMap):
         self.plot_settings['norm'] = ImageNormalize(
             stretch=source_stretch(self.meta, LogStretch()), clip=False)
 
+    @property
+    def coordinate_system(self):
+        """
+        Override the default implementation to handle TRACEMAP-specific logic for CTYPE values.
+        """
+        ctype1, ctype2 = self.meta['ctype1'], self.meta['ctype2']
+        if ctype1.lower() in ("solar-x", "solar_x"):
+            ctype1 = 'HPLN-TAN'
+        if ctype2.lower() in ("solar-y", "solar_y"):
+            ctype2 = 'HPLT-TAN'
+        return SpatialPair(ctype1, ctype2)
+
     @property
     def spatial_units(self):
         """
diff --git a/sunpy/net/attr.py b/sunpy/net/attr.py
index ef0bc3fd7..ae17f7cc7 100644
--- a/sunpy/net/attr.py
+++ b/sunpy/net/attr.py
@@ -201,7 +201,8 @@ class Attr(metaclass=AttrMeta):
 
         Examples
         --------
-        # The first import is to make this example work, it should not be used otherwise
+        The first import is to make this example work, it should not be used otherwise
+
         >>> from sunpy.net.dataretriever import GenericClient
         >>> from sunpy.net import attr, attrs
         >>> attr.Attr.update_values({GenericClient : {
diff --git a/sunpy/physics/differential_rotation.py b/sunpy/physics/differential_rotation.py
index 9ab8db711..6120366eb 100644
--- a/sunpy/physics/differential_rotation.py
+++ b/sunpy/physics/differential_rotation.py
@@ -594,7 +594,7 @@ def differential_rotate(smap, observer=None, time=None, **diff_rot_kwargs):
         out_meta.pop(key)
 
     # Add a new HGS observer
-    out_meta.update(get_observer_meta(new_observer, out_meta['rsun_ref']*u.m))
+    out_meta.update(get_observer_meta(new_observer, smap.rsun_meters))
 
     if is_sub_full_disk:
         # Define a new reference pixel and the value at the reference pixel.


Test patch:
diff --git a/sunpy/map/sources/tests/test_eit_source.py b/sunpy/map/sources/tests/test_eit_source.py
index 1131b3aea..468dd43c8 100644
--- a/sunpy/map/sources/tests/test_eit_source.py
+++ b/sunpy/map/sources/tests/test_eit_source.py
@@ -7,6 +7,7 @@ import astropy.units as u
 
 import sunpy.map
 from sunpy.data.test import get_dummy_map_from_header, get_test_filepath
+from sunpy.map.mapbase import SpatialPair
 from sunpy.map.sources.soho import EITMap
 from .helpers import _test_private_date_setters
 
@@ -24,6 +25,10 @@ def test_fitstoEIT(eit_map):
     assert isinstance(eit_map, EITMap)
 
 
+def test_eitmap_coordinate_system(eit_map):
+    assert eit_map.coordinate_system ==  SpatialPair(axis1='HPLN-TAN', axis2='HPLT-TAN')
+
+
 def test_reference_date(eit_map):
     assert eit_map.reference_date.isot == "2004-03-01T00:00:10.515"
 
diff --git a/sunpy/map/sources/tests/test_lasco_source.py b/sunpy/map/sources/tests/test_lasco_source.py
index b0f0842a6..d61321425 100644
--- a/sunpy/map/sources/tests/test_lasco_source.py
+++ b/sunpy/map/sources/tests/test_lasco_source.py
@@ -8,6 +8,7 @@ import astropy.units as u
 
 from sunpy.data.test import get_dummy_map_from_header, get_test_filepath
 from sunpy.map import Map
+from sunpy.map.mapbase import SpatialPair
 from sunpy.map.sources.soho import LASCOMap
 from sunpy.tests.helpers import skip_glymur
 from sunpy.util.exceptions import SunpyMetadataWarning
@@ -36,6 +37,10 @@ def test_fitstoLASCO(lasco_map):
     assert isinstance(lasco_map, LASCOMap)
 
 
+def test_lasco_coordinate_system(lasco_map):
+    assert lasco_map.coordinate_system ==  SpatialPair(axis1='HPLN-TAN', axis2='HPLT-TAN')
+
+
 def test_is_datasource_for(lasco_map):
     """Test the is_datasource_for method of LASCOMap.
     Note that header data to be provided as an argument
diff --git a/sunpy/map/sources/tests/test_sot_source.py b/sunpy/map/sources/tests/test_sot_source.py
index 95b58765b..25354031f 100644
--- a/sunpy/map/sources/tests/test_sot_source.py
+++ b/sunpy/map/sources/tests/test_sot_source.py
@@ -4,6 +4,7 @@ import pytest
 import astropy.units as u
 
 from sunpy.data.test import get_dummy_map_from_header, get_test_filepath
+from sunpy.map.mapbase import SpatialPair
 from sunpy.map.sources.hinode import SOTMap
 from sunpy.util.exceptions import SunpyMetadataWarning
 from .helpers import _test_private_date_setters
@@ -19,6 +20,10 @@ def test_fitstoSOT(sot):
     assert isinstance(sot, SOTMap)
 
 
+def test_sot_coordinate_system(sot):
+    assert sot.coordinate_system ==  SpatialPair(axis1='HPLN-TAN', axis2='HPLT-TAN')
+
+
 def test_is_datasource_for(sot):
     """Test the is_datasource_for method of SOTMap.
     Note that header data to be provided as an argument
diff --git a/sunpy/map/sources/tests/test_trace_source.py b/sunpy/map/sources/tests/test_trace_source.py
index 64df6aa4e..889f1f82b 100644
--- a/sunpy/map/sources/tests/test_trace_source.py
+++ b/sunpy/map/sources/tests/test_trace_source.py
@@ -3,6 +3,7 @@ import pytest
 import astropy.units as u
 
 from sunpy.data.test import get_dummy_map_from_header, get_test_filepath
+from sunpy.map.mapbase import SpatialPair
 from sunpy.map.sources.trace import TRACEMap
 from sunpy.util.exceptions import SunpyMetadataWarning
 from .helpers import _test_private_date_setters
@@ -18,6 +19,10 @@ def test_fitstoTRACE(trace_map):
     assert isinstance(trace_map, TRACEMap)
 
 
+def test_trace_coordinate_system(trace_map):
+    assert trace_map.coordinate_system ==  SpatialPair(axis1='HPLN-TAN', axis2='HPLT-TAN')
+
+
 def test_is_datasource_for(trace_map):
     """Test the is_datasource_for method of TRACEMap.
     Note that header data to be provided as an argument
diff --git a/sunpy/map/sources/tests/test_xrt_source.py b/sunpy/map/sources/tests/test_xrt_source.py
index 3ec12a636..063e00103 100644
--- a/sunpy/map/sources/tests/test_xrt_source.py
+++ b/sunpy/map/sources/tests/test_xrt_source.py
@@ -8,6 +8,7 @@ import pytest
 import astropy.units as u
 
 from sunpy.data.test import get_dummy_map_from_header, get_test_filepath
+from sunpy.map.mapbase import SpatialPair
 from sunpy.map.sources.hinode import XRTMap
 from .helpers import _test_private_date_setters
 
@@ -24,6 +25,10 @@ def test_fits_to_xrt(xrt_map):
     assert isinstance(xrt_map, XRTMap)
 
 
+def test_xrt_coordinate_system(xrt_map):
+    assert xrt_map.coordinate_system ==  SpatialPair(axis1='HPLN-TAN', axis2='HPLT-TAN')
+
+
 def test_is_datasource_for(xrt_map):
     """Test the is_datasource_for method of XRTMap.
     Note that header data to be provided as an argument
diff --git a/sunpy/map/tests/test_mapbase.py b/sunpy/map/tests/test_mapbase.py
index 35a2968f9..a881cfd3f 100644
--- a/sunpy/map/tests/test_mapbase.py
+++ b/sunpy/map/tests/test_mapbase.py
@@ -383,6 +383,15 @@ def test_default_coordinate_system(generic_map):
         assert generic_map.coordinate_system == ('HPLN-TAN', 'HPLT-TAN')
 
 
+@pytest.mark.skipif(pytest.__version__ < "8.0.0", reason="pytest >= 8.0.0 raises two warnings for this test")
+def test_coordinate_system_solar_x_solar_y(generic_map):
+    generic_map.meta['ctype1'] = 'SOLAR-X'
+    generic_map.meta['ctype2'] = 'SOLAR-Y'
+    with pytest.warns(SunpyDeprecationWarning, match="CTYPE1 value 'solar-x'/'solar_x' is deprecated") :
+        with pytest.warns(SunpyDeprecationWarning, match="CTYPE2 value 'solar-y'/'solar_y' is deprecated") :
+            assert generic_map.coordinate_system == ('HPLN-TAN', 'HPLT-TAN')
+
+
 def test_carrington_longitude(generic_map):
     assert u.allclose(generic_map.carrington_longitude, sun.L0(generic_map.date))
 
diff --git a/sunpy/physics/tests/test_differential_rotation.py b/sunpy/physics/tests/test_differential_rotation.py
index 787c68df7..735d66f20 100644
--- a/sunpy/physics/tests/test_differential_rotation.py
+++ b/sunpy/physics/tests/test_differential_rotation.py
@@ -22,6 +22,7 @@ from sunpy.physics.differential_rotation import (
     differential_rotate,
     solar_rotate_coordinate,
 )
+from sunpy.sun.constants import radius as R_sun
 from sunpy.util.exceptions import SunpyDeprecationWarning
 
 # Please note the numbers in these tests are not checked for physical
@@ -391,3 +392,14 @@ def test_differential_rotation(aia171_test_map):
     with pytest.warns(UserWarning, match="Using 'time' assumes an Earth-based observer"):
         rot_map = differential_rotate(aia171_test_map, time=2*u.day)
     return rot_map.data
+
+
+def test_rsun_fallback(aia171_test_map):
+    # Remove the AIA-specific value of the solar radius
+    assert_quantity_allclose(aia171_test_map.rsun_meters, 696 * u.Mm)
+    del aia171_test_map.meta['rsun_ref'], aia171_test_map.meta['rsun_obs']
+
+    # Confirm that the differentially rotated map has the default value for the solar radius
+    new_observer = get_earth(aia171_test_map.date + 2 * u.day)
+    rot_map = differential_rotate(aia171_test_map, observer=new_observer)
+    assert_quantity_allclose(rot_map.rsun_meters, R_sun)