Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update EISMap #50

Merged
merged 8 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
fail-fast: false
matrix:
include:
- python-version: 3.7
- python-version: 3.8
- python-version: 3.9
- python-version: '3.10'

Choose a reason for hiding this comment

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

Do you want to also try 3.11?

Choose a reason for hiding this comment

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

This should also be updated in the setup.cfg.

steps:
- name: Checkout
uses: actions/checkout@v2.3.1
Expand Down
110 changes: 59 additions & 51 deletions eispac/core/eismap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import astropy.units as u
from astropy.visualization import ImageNormalize, AsinhStretch, LinearStretch
import sunpy.map
from sunpy.map.mapbase import SpatialPair
from sunpy.time import parse_time


__all__ = ['EISMap']


Expand All @@ -23,7 +23,7 @@ class EISMap(sunpy.map.GenericMap):
contributions from ESA and Norway. Hinode was launched on September 22, 2006
at 21:36 UTC from the Uchinoura Space Center in Japan and continues to
operate. EIS observes two wavelength ranges in the extreme ultraviolet,
171—212Å and 245—291Å with a spectral resolution of about 22mÅ and a plate
171—212 Å and 245—291 Å with a spectral resolution of about 22 mÅ and a plate
scale of 100 per pixel.

This data structure is designed to hold the fit parameters derived from multi-gaussian
Expand Down Expand Up @@ -61,29 +61,10 @@ def __init__(self, data, header=None, **kwargs):

super().__init__(data, header, **kwargs)

# Validate important keywords and add them in, if missing
self.meta['ctype1'] = self.meta.get('ctype1', 'HPLN-TAN')
self.meta['ctype2'] = self.meta.get('ctype2', 'HPLT-TAN')
self.meta['cunit1'] = self.meta.get('cunit1', 'arcsec')
self.meta['cunit2'] = self.meta.get('cunit2', 'arcsec')
self.meta['waveunit'] = self.meta.get('waveunit', 'angstrom')
self.meta['date-beg'] = self.meta.get('date_beg', self.meta.get('date_obs'))
self.meta['date-end'] = self.meta.get('date_end')
self.meta['date-obs'] = self.meta.get('date_obs')
self.meta['date-avg'] = self.meta.get('date_avg')
# NOTE: this block can be removed once sunpy>=3.1 is only supported as
# the .date_average property will always be constructed in this way if
# date_start and date_end are present
if self.meta['date-avg'] is None:
if self.meta['date-beg'] is not None and self.meta['date-end'] is not None:
timesys = self.meta.get('timesys', 'UTC').lower()
start = parse_time(self.meta['date-beg'], scale=timesys)
end = parse_time(self.meta['date-end'], scale=timesys)
self.meta['date-avg'] = (start + (end - start)/2).isot

# Setup plot settings
self.plot_settings['aspect'] = self.meta['cdelt2'] / self.meta['cdelt1']
# self.plot_settings['interpolation'] = 'kaiser' # KEEP FOR REFERENCE
# Adjust colormap and normalization depending on whether the map contains
# intensity, velocity, or line width data
if self.meta['measrmnt'].lower().startswith('int'):
self.plot_settings['cmap'] = 'Blues_r'
self.plot_settings['norm'] = ImageNormalize(stretch=AsinhStretch())
Expand All @@ -95,45 +76,72 @@ def __init__(self, data, header=None, **kwargs):
elif self.meta['measrmnt'].lower().startswith('wid'):
self.plot_settings['cmap'] = 'viridis'

@classmethod
def is_datasource_for(cls, data, header, **kwargs):
"""
Determines if header corresponds to an EIS image. Used to register
EISMap with the sunpy.map.Map factory.
"""
return str(header.get('instrume', '')).startswith('EIS')
@property
def spatial_units(self):
units = self.meta.get('cunit1', 'arcsec'), self.meta.get('cunit2', 'arcsec')
return SpatialPair(u.Unit(units[0]), u.Unit(units[1]))

@property
def observatory(self):
return 'Hinode'
def processing_level(self):
return self.meta.get('lvl_num', 3)

@property
def measurement(self):
line_id = self.meta.get('line_id', '')
quantity = self.meta.get('measrmnt', '')
return f'{line_id} {quantity}'
def waveunit(self):
return u.Unit(self.meta.get('waveunit', 'angstrom'))

@property
def wavelength(self):
line_id = self.meta.get('line_id')
if line_id is not None:
wave = float(line_id.split()[-1])
return u.Quantity(wave, self.meta['waveunit'])
return u.Quantity(wave, self.waveunit)

@property
def date(self):
# Want to make sure we are constructing our coordinate frames
# from DATE_AVG and not the DATE-OBS which is the beginning of
# the observation
# In sunpy 3.1, this may not be needed as we could just remove
# date-obs and then .date will default to .date_average
t = self.meta.get('date-avg')
timesys = self.meta.get('timesys', 'UTC')
if t is None:
return super().date
else:
return parse_time(t, scale=timesys.lower())
def measurement(self):
return self.meta.get('measrmnt', '')

@property
def processing_level(self):
return self.meta.get('lvl_num', 3)
def observatory(self):
return 'Hinode'

@property
def nickname(self):
line_id = self.meta.get('line_id', '')
return f'{self.observatory} {self.instrument} {line_id}'

@property
def date_start(self):
# Try default key DATE-BEG. This is to future proof against
# switching to DATE-BEG when constructing the L1 headers
# NOTE: the DATE_OBS key is the beginning of the observation
# so we can use this in case DATE_BEG is missing
date_beg = self._get_date('date_beg') or super().date_start
date_beg = date_beg or self._date_obs
return date_beg

@property
def date_end(self):
# Try default key DATE-END. This is to future proof against
# switching to DATE-END when constructing the L1 headers
return self._get_date('date_end') or super().date_end

@property
def date_average(self):
return self._get_date('date_avg') or super().date_average

@property
def date(self):
# NOTE: we override this property to prioritize date_average
# over DATE-OBS (or DATE_OBS). In GenericMap, this is reversed.
# We do this because we want to make sure we are constructing our
# coordinate frames from DATE_AVG (the midpoint of the raster) and
# not DATE-OBS which is the beginning of the raster.
return self.date_average or super().date

@classmethod
def is_datasource_for(cls, data, header, **kwargs):
"""
Determines if header corresponds to an EIS image. Used to register
EISMap with the sunpy.map.Map factory.
"""
return str(header.get('instrume', '')).startswith('EIS')
85 changes: 40 additions & 45 deletions eispac/tests/test_eismap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
import numpy as np
import pytest
from astropy.io import fits
import astropy.time
import astropy.units as u
from astropy.visualization import ImageNormalize, AsinhStretch
import sunpy
import sunpy.map
from sunpy.time import parse_time

# This registers the EISMap map class
import eispac # NOQA
Expand Down Expand Up @@ -77,67 +75,64 @@ def test_wavelength(test_eis_map):


def test_measurement(test_eis_map):
assert test_eis_map.measurement == 'Fe XII 195.119 intensity'
assert test_eis_map.measurement == 'intensity'


def test_nickname(test_eis_map):
assert test_eis_map.nickname == 'Hinode EIS Fe XII 195.119'


def test_processing_level(test_eis_map):
assert test_eis_map.processing_level == 3


def test_plot_settings(test_eis_map):
assert test_eis_map.plot_settings['cmap'] == 'Blues_r'
assert isinstance(test_eis_map.plot_settings['norm'], ImageNormalize)
assert isinstance(test_eis_map.plot_settings['norm'].stretch, AsinhStretch)
scale = test_eis_map.scale.axis2 / test_eis_map.scale.axis1
assert test_eis_map.plot_settings['aspect'] == scale.decompose().value
def test_spatial_units(test_eis_map):
assert test_eis_map.spatial_units[0] == u.arcsec
assert test_eis_map.spatial_units[1] == u.arcsec


def test_waveunit(test_eis_map):
assert test_eis_map.waveunit == u.Angstrom


@pytest.mark.parametrize('attr,key', [
('date_start', 'date_beg'),
('date_end', 'date_end'),
('date_average', 'date_avg'),
])
def test_date_props(test_eis_map, attr, key):
assert getattr(test_eis_map, attr).isot == test_eis_map.meta[key]


def test_date(test_eis_map, test_header):
# NOTE: these tests will change slightly with sunpy 3.1 decause of the existence of
# date-beg, date-end, and date-avg keys
# Case 1: date_obs and date_end exist
assert test_eis_map.meta['date-beg'] == test_eis_map.meta['date_beg']
assert test_eis_map.meta['date-end'] == test_eis_map.meta['date_end']
assert test_eis_map.meta['date-obs'] == test_eis_map.meta['date_obs']
assert test_eis_map.meta['date-avg'] == test_eis_map.meta['date_avg']
assert test_eis_map.date.isot == test_eis_map.meta['date-avg']
# Case 2: date_beg does not exist
# Case 1: date-average exists
assert test_eis_map.date.isot == test_eis_map.meta['date_avg']
# Case 2: date-average is None so default to date_obs
header = copy.deepcopy(test_header)
del header['date_beg']
del header['date_end']
del header['date_avg']
new_map = sunpy.map.Map(test_eis_map.data, header)
assert new_map.meta['date-beg'] == new_map.meta['date_obs']
# Case 3: date_avg does not exist
assert new_map.date.isot == new_map.meta['date_obs']
# Case 3: date_avg is None so default to date_start
header = copy.deepcopy(test_header)
del header['date_avg']
del header['date_end']
del header['date_obs']
new_map = sunpy.map.Map(test_eis_map.data, header)
start = parse_time(new_map.meta['date-beg'], scale='utc')
end = parse_time(new_map.meta['date-end'], scale='utc')
avg = start + (end - start)/2
assert new_map.meta['date-avg'] == avg.isot
assert new_map.date == avg
assert new_map.date.isot == new_map.meta['date_beg']
# Case 4: date_end and date_avg do not exist
header = copy.deepcopy(test_header)
del header['date_avg']
del header['date_end']
del header['date_beg']
del header['date_obs']
new_map = sunpy.map.Map(test_eis_map.data, header)
assert new_map.date.isot == new_map.meta['date-obs']
assert new_map.date.isot == new_map.meta['date_end']


def test_missing_date_raises_warning(test_eis_map, test_header):
header = copy.deepcopy(test_header)
del header['date_end']
del header['date_obs']
del header['date_beg']
del header['date_avg']
now = astropy.time.Time.now()
new_map = sunpy.map.Map(test_eis_map.data, header)
# This raises a slightly different warning in in sunpy>=3.1
version = float('.'.join(sunpy.__version__.split('.')[:-1]))
if version >= 3.1:
from sunpy.util.exceptions import SunpyMetadataWarning
expected_warning = SunpyMetadataWarning
else:
from sunpy.util.exceptions import SunpyUserWarning
expected_warning = SunpyUserWarning
with pytest.warns(expected_warning, match='Missing metadata for observation time'):
assert new_map.date - now < 1*u.s
def test_plot_settings(test_eis_map):
assert test_eis_map.plot_settings['cmap'] == 'Blues_r'
assert isinstance(test_eis_map.plot_settings['norm'], ImageNormalize)
assert isinstance(test_eis_map.plot_settings['norm'].stretch, AsinhStretch)
scale = test_eis_map.scale.axis2 / test_eis_map.scale.axis1
assert test_eis_map.plot_settings['aspect'] == scale.decompose().value
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ install_requires =
matplotlib>=3.1
h5py>=2.9
astropy>=3.1
sunpy[net,map]>=2.1
sunpy[net,map]>=4.0
ndcube>=2.0.0
parfive>=1.5
python-dateutil>=2.8
Expand All @@ -41,7 +41,7 @@ test =
pytest>=4.6.3
pytest-astropy
docs =
sphinx==4.0.2 # why is this pinned so strictly?
sphinx>=4.0.2 # why is this pinned so strictly?
sphinx-automodapi>=0.13
sphinx-rtd-theme

Expand Down