Add Metis Map#8519
Conversation
Accomplishments: - Implemented 12 validation tests for METISMap. - Verified WCS, metadata fallback, and colormap logic. - Implemented mask_bad_pix and get_fov_rsun tests. - Cleaned code with Ruff (fixed F401 and PT011).
Metis map gio
|
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 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? |
|
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 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? |
samaloney
left a comment
There was a problem hiding this comment.
Thanks again for the PR all the pieces are there may just need to move some of them around.
| 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 |
There was a problem hiding this comment.
This maybe easier to read, less error prone by using the unit rather than dropping:
| 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?
There was a problem hiding this comment.
I corrected this part as suggested.
Correct, rmin_rsun, rmax_rsun, board_rsun are in a unit of Rsun (i.e., unitless).
| 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) |
There was a problem hiding this comment.
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)
| # ============================================================================ | ||
|
|
||
|
|
||
| @pytest.fixture |
There was a problem hiding this comment.
Could parametrise this test with different header value to test the other code paths
| instrume = header.get("INSTRUME", "").strip().upper() | ||
| obsrvtry = header.get("OBSRVTRY", "").strip().upper() | ||
|
|
||
| return ("METIS" in instrume) and ("SOLAR ORBITER" in obsrvtry) |
There was a problem hiding this comment.
Should this source work or load only L2 files if so should add to the check here.
There was a problem hiding this comment.
Yes, for the moment METISMap works only with L2 Metis data. Added the corresponding check
aburtovoi
left a comment
There was a problem hiding this comment.
Reviewed solo.py code (class METISMap) as requested
| instrume = header.get("INSTRUME", "").strip().upper() | ||
| obsrvtry = header.get("OBSRVTRY", "").strip().upper() | ||
|
|
||
| return ("METIS" in instrume) and ("SOLAR ORBITER" in obsrvtry) |
There was a problem hiding this comment.
Yes, for the moment METISMap works only with L2 Metis data. Added the corresponding check
| 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 |
There was a problem hiding this comment.
I corrected this part as suggested.
Correct, rmin_rsun, rmax_rsun, board_rsun are in a unit of Rsun (i.e., unitless).
| 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) |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
Can we use matplotlib's (somewhat similar) "cividis" instead? Or is it important to use specifically Crameri's "batlow"?
|
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,
|
| @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") |
There was a problem hiding this comment.
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).
- 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
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: