Skip to content

Add Metis Map#8519

Open
aburtovoi wants to merge 10 commits into
sunpy:mainfrom
aburtovoi:metis_branch2
Open

Add Metis Map#8519
aburtovoi wants to merge 10 commits into
sunpy:mainfrom
aburtovoi:metis_branch2

Conversation

@aburtovoi
Copy link
Copy Markdown

We have created Issue #8518, requesting integration on METISMap Class to the Sunpy core package.

In this respect, we have modified the following files of the package:

  • sunpy/sunpy/map/sources/solo.py
  • sunpy/visualization/colormaps/color_tables.py
  • sunpy/visualization/colormaps/cm.py
  • sunpy/sunpy/map/sources/tests/test_metis_source.py

@Cadair
Copy link
Copy Markdown
Member

Cadair commented Feb 19, 2026

Hi @aburtovoi and everyone.

Thanks so much for opening this PR, I'm really excited to get more support for different Solar Orbiter data into sunpy.

The first thing we need to talk about is the METIS specific methods you have added to this class. The approach we take in core is that all the different map sources have the same API as GenericMap, i.e. no extra methods or properties. The motivation for this is that if you write some code for one EUV imager you should be able to drop a map from another one in etc. i.e. code written for one map should work for others.

We should think about where this METIS functionality would best live, could you talk me through the various things you have added so we can figure it out?

@samaloney
Copy link
Copy Markdown
Member

This is really cool, I seen this in action at a recent meeting thanks @aburtovoi. I'd suggest separating this into two broad chunks: getting the Metis data into the map container and Metis-specific processing.

As these are Solar Orbiter fits files, they are pretty compliant with the FITS standard. All we need is the colour table and normalisation thresholds, which should stay in the map source but use the the meta data .meta['btype'] rather than the custom attributes?

The masking functions could be moved to be plain functions rather than methods taking and returning MetisMaps and either live in the solo map module or sunkit-instruments?

def mask_occulter(metis_map: MetisMap):
    # occulter masking stuff
    return metis_map  #or Map(masked_data, meta)

def mask_bad_qualtiy(metis_map, MetisMap, quality_array)
    # bad quality masking stuff
    return metis_map  #or Map(masked_data, meta)

Copy link
Copy Markdown
Member

@samaloney samaloney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the PR all the pieces are there may just need to move some of them around.

Comment thread sunpy/map/sources/solo.py Outdated
Comment on lines +280 to +284
rsun_deg = self.rsun_obs.value / 3600.0 # Convert arcsec to deg
rmin_rsun = self.meta["inn_fov"] / rsun_deg # Inner FOV in rsun
rmax_rsun = self.meta["out_fov"] / rsun_deg # Outer FOV in rsun
board_deg = 2.9 # Detector board edge in deg
board_rsun = board_deg / rsun_deg # Board radius in rsun
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maybe easier to read, less error prone by using the unit rather than dropping:

Suggested change
rsun_deg = self.rsun_obs.value / 3600.0 # Convert arcsec to deg
rmin_rsun = self.meta["inn_fov"] / rsun_deg # Inner FOV in rsun
rmax_rsun = self.meta["out_fov"] / rsun_deg # Outer FOV in rsun
board_deg = 2.9 # Detector board edge in deg
board_rsun = board_deg / rsun_deg # Board radius in rsun
rsun_deg = self.rsun_obs.to(u.deg) # Convert arcsec to deg
rmin_rsun = (self.meta["inn_fov"]*u.deg) = / rsun_deg # Inner FOV in rsun
rmax_rsun = (self.meta["out_fov"]*u.deg) / rsun_deg # Outer FOV in rsun
board_deg = 2.9 * u.deg # Detector board edge in deg
board_rsun = board_deg / rsun_deg # Board radius in rsun

but here it looks like board_rsun would be unit less so I've gotten something wrong?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I corrected this part as suggested.
Correct, rmin_rsun, rmax_rsun, board_rsun are in a unit of Rsun (i.e., unitless).

Comment thread sunpy/map/sources/solo.py Outdated
Comment thread sunpy/map/sources/solo.py Outdated
Comment on lines +320 to +331
inn_fov = self.meta["inn_fov"] * 3600 / self.meta["cdelt1"] # in pix
out_fov = self.meta["out_fov"] * 3600 / self.meta["cdelt2"] # in pix

# Create coordinate grids
x = np.arange(0, self.meta["naxis1"], 1)
y = np.arange(0, self.meta["naxis2"], 1)
xx, yy = np.meshgrid(x, y, sparse=True)

# Calculate distance from internal occulter center
in_xcen = self.meta["io_xcen"]
in_ycen = self.meta["io_ycen"]
dist_inncen = np.sqrt((xx - in_xcen) ** 2 + (yy - in_ycen) ** 2)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually prefer to try and use the map attributes, units and wcs methods so along the lines of

inn_fov = (self.meta["inn_fov"] * u.deg / self.scale[0]).decompose()
....
in_xcen = self.meta["io_xcen"] * u.pix
...
dist_inncen = np.sqrt((xx - in_xcen) ** 2 + (yy - in_ycen) ** 2)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected, thanks!

# ============================================================================


@pytest.fixture
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could parametrise this test with different header value to test the other code paths

Comment thread sunpy/map/sources/solo.py Outdated
Comment thread sunpy/map/sources/solo.py Outdated
instrume = header.get("INSTRUME", "").strip().upper()
obsrvtry = header.get("OBSRVTRY", "").strip().upper()

return ("METIS" in instrume) and ("SOLAR ORBITER" in obsrvtry)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this source work or load only L2 files if so should add to the check here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for the moment METISMap works only with L2 Metis data. Added the corresponding check

Comment thread sunpy/map/sources/solo.py Outdated
@samaloney samaloney added the map Affects the map submodule label Feb 20, 2026
Copy link
Copy Markdown
Author

@aburtovoi aburtovoi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed solo.py code (class METISMap) as requested

Comment thread sunpy/map/sources/solo.py Outdated
Comment thread sunpy/map/sources/solo.py Outdated
Comment thread sunpy/map/sources/solo.py Outdated
instrume = header.get("INSTRUME", "").strip().upper()
obsrvtry = header.get("OBSRVTRY", "").strip().upper()

return ("METIS" in instrume) and ("SOLAR ORBITER" in obsrvtry)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for the moment METISMap works only with L2 Metis data. Added the corresponding check

Comment thread sunpy/map/sources/solo.py Outdated
Comment on lines +280 to +284
rsun_deg = self.rsun_obs.value / 3600.0 # Convert arcsec to deg
rmin_rsun = self.meta["inn_fov"] / rsun_deg # Inner FOV in rsun
rmax_rsun = self.meta["out_fov"] / rsun_deg # Outer FOV in rsun
board_deg = 2.9 # Detector board edge in deg
board_rsun = board_deg / rsun_deg # Board radius in rsun
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I corrected this part as suggested.
Correct, rmin_rsun, rmax_rsun, board_rsun are in a unit of Rsun (i.e., unitless).

Comment thread sunpy/map/sources/solo.py Outdated
Comment on lines +320 to +331
inn_fov = self.meta["inn_fov"] * 3600 / self.meta["cdelt1"] # in pix
out_fov = self.meta["out_fov"] * 3600 / self.meta["cdelt2"] # in pix

# Create coordinate grids
x = np.arange(0, self.meta["naxis1"], 1)
y = np.arange(0, self.meta["naxis2"], 1)
xx, yy = np.meshgrid(x, y, sparse=True)

# Calculate distance from internal occulter center
in_xcen = self.meta["io_xcen"]
in_ycen = self.meta["io_ycen"]
dist_inncen = np.sqrt((xx - in_xcen) ** 2 + (yy - in_ycen) ** 2)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected, thanks!

Comment thread sunpy/map/sources/solo.py Outdated
@aburtovoi aburtovoi closed this Feb 25, 2026
@aburtovoi aburtovoi reopened this Feb 25, 2026
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think cmcrameri is a default matplotlib colormap which is causing some errors in the doc builds?

# cmap_name, cmap, cmap.name
cmap_dict = {
'solometisvl-tb': (
cmcrameri.cm.batlow.copy(), 'SolO Metis VL Total Brightness'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use matplotlib's (somewhat similar) "cividis" instead? Or is it important to use specifically Crameri's "batlow"?

@wtbarnes
Copy link
Copy Markdown
Member

It's great to see more map sources, especially for Solar Orbiter data so thanks very much for this contribution! There was some discussion about this at the dev meeting today which I'll try my best to summarize. @Cadair or @samaloney can jump in if I've misrepresented something.

As @Cadair noted in his initial comment, GenericMap subclasses should all have the same API, i.e. subclasses should not introduce additional (public) methods. From what I can tell, almost all of the additional methods here are related to either building the mask or setting the appropriate plot settings given the masked array. Given that, I would suggest doing the following:

  • Make all methods added here that do not already exist on GenericMap private
  • In the constructor, use the aforementioned private methods to set the mask property on the map. This will then propagate through to all of the map visualization methods (e.g. https://docs.sunpy.org/en/stable/generated/gallery/computer_vision_techniques/mask_disk.html) while leaving the original data untouched.
  • Also in the constructor, but following the step above, adjust plot settings as needed, using the mask that you've just constructed.

Comment thread sunpy/map/sources/solo.py
Comment on lines +165 to +172
@property
def prodtype(self):
"""Product type identifier for this METIS data."""
return self._prodtype

@prodtype.setter
def prodtype(self, value):
raise AttributeError("Cannot manually set prodtype for METISMap")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: previous comments, prodtype is not a property exposed on other subclasses. I would suggest instead overriding measurement and just moving the logic of get_prodtype here (or just calling it as a private method _get_prodtype).

@Cadair Cadair added this to the 8.0.0 milestone Apr 22, 2026
@Cadair Cadair added the Needs Adoption PRs that went stale but should be picked up again and completed. label May 13, 2026
Hermanlrx added a commit to Hermanlrx/sunpy that referenced this pull request May 19, 2026
- adopting pr 8519
- changed prodtype to measurement from sunpy#8519 comments
- changed cmaps to cividis from sunpy#8519 comments
- changed pytest to use metis_map where tests are agnostic to specific
header values from sunpy#8519 comments
@Hermanlrx Hermanlrx mentioned this pull request May 20, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

map Affects the map submodule Needs Adoption PRs that went stale but should be picked up again and completed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants