Skip to content

Commit

Permalink
Update EISMap (#50)
Browse files Browse the repository at this point in the history
* refactor EISMap to not modify metadata in place

* fixup eismap tests

* pin sunpy to >=3.1

* pin to sunpy>=4.0

* bumpy py version in CI

* fix py version number

* unpin sphinx

* simplify date property
  • Loading branch information
wtbarnes committed Nov 10, 2022
1 parent ae13369 commit 7b522e1
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 99 deletions.
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'
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

0 comments on commit 7b522e1

Please sign in to comment.