Repository: sunpy/sunpy

Issue:
`.submap()` doesn't preserve `.plot_settings['norm']`
Creating a submap with `GenericMap.submap()` does not preserve any custom set norm (ie. `.plot_settings['norm']`) in the new map.


Discussion:
Huh...but we are passing in `self.plot_settings` to `_new_instance`: https://github.com/sunpy/sunpy/blob/3da48422cf176492ffa029e09fb1d1cdbb292374/sunpy/map/mapbase.py#L1881
I can reproduce this if the map is an `AIAMap`. This snippet gives,

```python
import sunpy.data.sample
import sunpy.map
from astropy.visualization import ImageNormalize, LogStretch
m = sunpy.map.Map(sunpy.data.sample.AIA_171_IMAGE)
m.plot_settings['norm'] = ImageNormalize(vmin=1.1, vmax=42, stretch=LogStretch())
print(m.plot_settings['norm'].vmin)
print(m.plot_settings['norm'].vmax)
print(type(m.plot_settings['norm'].stretch))
m_sub = m.submap(m.bottom_left_coord,top_right=m.center)
print(m_sub.plot_settings['norm'].vmin)
print(m_sub.plot_settings['norm'].vmax)
print(type(m_sub.plot_settings['norm'].stretch))
```

```python
1.1
42
<class 'astropy.visualization.stretch.LogStretch'>
None
None
<class 'astropy.visualization.stretch.AsinhStretch'>
```

However, in the case of a `GenericMap`, the plot settings persist past the submap call,

```python
m = sunpy.map.GenericMap(m.data, m.meta)
m.plot_settings['norm'] = ImageNormalize(vmin=1.1, vmax=42, stretch=LogStretch())
print(m.plot_settings['norm'].vmin)
print(m.plot_settings['norm'].vmax)
print(type(m.plot_settings['norm'].stretch))
m_sub = m.submap(m.bottom_left_coord,top_right=m.center)
print(m_sub.plot_settings['norm'].vmin)
print(m_sub.plot_settings['norm'].vmax)
print(type(m_sub.plot_settings['norm'].stretch))
```

```python
1.1
42
<class 'astropy.visualization.stretch.LogStretch'>
1.1
42
<class 'astropy.visualization.stretch.LogStretch'>
```

I have not tested this with other map source classes, but I suspect the results will be the same for other map classes as well. I think this is because we reset the plot settings in the constructor of the Map sources to settings that we've deemed appropriate for that source. 

I think a possible fix here to set the plot settings on the map in `_new_instance` *after* we instantiate the map, i.e. not through the constructor. The larger issue here seems to be that we ignore plot settings in the constructors of our map sources.
Thanks for the example - I think it was an `AIAMap` that I found this issue.

Resolving patch:
diff --git a/sunpy/map/mapbase.py b/sunpy/map/mapbase.py
--- a/sunpy/map/mapbase.py
+++ b/sunpy/map/mapbase.py
@@ -494,7 +494,14 @@ def _new_instance(cls, data, meta, plot_settings=None, **kwargs):
         Instantiate a new instance of this class using given data.
         This is a shortcut for ``type(self)(data, meta, plot_settings)``.
         """
-        return cls(data, meta, plot_settings=plot_settings, **kwargs)
+        new_map = cls(data, meta, **kwargs)
+        # plot_settings are set explicitly here as some map sources
+        # explicitly set some of the plot_settings in the constructor
+        # and we want to preserve the plot_settings of the previous
+        # instance.
+        if plot_settings is not None:
+            new_map.plot_settings.update(plot_settings)
+        return new_map
 
     def _get_lon_lat(self, frame):
         """
@@ -516,10 +523,10 @@ def _new_instance_from_op(self, new_data):
         """
         new_meta = copy.deepcopy(self.meta)
         new_meta['bunit'] = new_data.unit.to_string('fits')
-        return self._new_instance(new_data.value, new_meta)
+        return self._new_instance(new_data.value, new_meta, plot_settings=self.plot_settings)
 
     def __neg__(self):
-        return self._new_instance(-self.data, self.meta)
+        return self._new_instance(-self.data, self.meta, plot_settings=self.plot_settings)
 
     @check_arithmetic_compatibility
     def __pow__(self, value):
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
@@ -627,7 +627,7 @@ def differential_rotate(smap, observer=None, time=None, **diff_rot_kwargs):
         padded_meta['crpix2'] += deltay
 
         # Create the padded map that will be used to create the rotated map.
-        smap = smap._new_instance(padded_data, padded_meta)
+        smap = smap._new_instance(padded_data, padded_meta, smap.plot_settings)
 
     # Check for masked maps
     if smap.mask is not None:
@@ -670,6 +670,6 @@ def differential_rotate(smap, observer=None, time=None, **diff_rot_kwargs):
             ((center_rotated.Tx - smap.center.Tx)/smap.scale.axis1).value
         out_meta['crpix2'] = 1 + smap.data.shape[0]/2.0 + \
             ((center_rotated.Ty - smap.center.Ty)/smap.scale.axis2).value
-        return smap._new_instance(out_data, out_meta).submap(rotated_bl, top_right=rotated_tr)
+        return smap._new_instance(out_data, out_meta, smap.plot_settings).submap(rotated_bl, top_right=rotated_tr)
     else:
-        return smap._new_instance(out_data, out_meta)
+        return smap._new_instance(out_data, out_meta, smap.plot_settings)


Test patch:
diff --git a/sunpy/map/sources/tests/test_aia_source.py b/sunpy/map/sources/tests/test_aia_source.py
--- a/sunpy/map/sources/tests/test_aia_source.py
+++ b/sunpy/map/sources/tests/test_aia_source.py
@@ -8,6 +8,7 @@
 import pytest
 
 import astropy.units as u
+from astropy.visualization.mpl_normalize import ImageNormalize
 
 import sunpy.data.test
 from sunpy.map import Map
@@ -63,6 +64,27 @@ def test_norm_clip(createAIAMap):
     assert not createAIAMap.plot_settings['norm'].clip
 
 
+def test_new_instance_preserves_plot_settings(createAIAMap):
+    # Tests that the _new_instance method preserves the plot_settings
+    # of the old instance. This is done on the AIA source as the AIAMap
+    # constructor explicitly sets the cmap and norm and we want to test
+    # that _new_instance persists the old custom plot_settings
+    createAIAMap.plot_settings['norm'] = ImageNormalize(vmin=0.1, vmax=42)
+    createAIAMap.plot_settings['cmap'] = 'inferno'
+    new_aia_map = createAIAMap._new_instance(createAIAMap.data,
+                                             createAIAMap.meta,
+                                             plot_settings=createAIAMap.plot_settings)
+    assert new_aia_map.plot_settings['norm'].vmin == createAIAMap.plot_settings['norm'].vmin
+    assert new_aia_map.plot_settings['norm'].vmax == createAIAMap.plot_settings['norm'].vmax
+    assert new_aia_map.plot_settings['cmap'] == createAIAMap.plot_settings['cmap']
+    # If no plot settings are explicitly passed, the plot_settings should fall back to those
+    # in the constructor
+    new_aia_map = createAIAMap._new_instance(createAIAMap.data, createAIAMap.meta)
+    assert new_aia_map.plot_settings['norm'].vmin is None
+    assert new_aia_map.plot_settings['norm'].vmax is None
+    assert new_aia_map.plot_settings['cmap'] == new_aia_map._get_cmap_name()
+
+
 def test_wcs(createAIAMap):
     # Smoke test that WCS is valid and can transform from pixels to world coordinates
     createAIAMap.pixel_to_world(0*u.pix, 0*u.pix)