diff --git a/CHANGES.rst b/CHANGES.rst index bde5176fe1..8ecc95cec6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,8 @@ Cubeviz - Cubeviz image viewer now has coordinates info panel like Imviz. [#1315] +- New Metadata Viewer plugin. [#1325] + Imviz ^^^^^ diff --git a/docs/cubeviz/plugins.rst b/docs/cubeviz/plugins.rst index 1fe71988dd..cf37edc570 100644 --- a/docs/cubeviz/plugins.rst +++ b/docs/cubeviz/plugins.rst @@ -9,6 +9,13 @@ more detail under :ref:`Specviz: Data Analysis Plugins `. All are accessed via the :guilabel:`plugin` icon in the upper right corner of the Cubeviz application. +.. _cubeviz-metadata-viewer: + +Metadata Viewer +=============== + +This plugin allows viewing of any metadata associated with the selected data. + .. _cubeviz-export-plot: Export Plot diff --git a/jdaviz/configs/cubeviz/cubeviz.yaml b/jdaviz/configs/cubeviz/cubeviz.yaml index 4660ed783d..3d2d292bad 100644 --- a/jdaviz/configs/cubeviz/cubeviz.yaml +++ b/jdaviz/configs/cubeviz/cubeviz.yaml @@ -17,6 +17,7 @@ toolbar: - g-subset-tools - g-coords-info tray: + - g-metadata-viewer - g-plot-options - g-subset-plugin - cubeviz-slice diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 58daf7a033..bce4ed9277 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -9,6 +9,7 @@ from specutils import Spectrum1D from jdaviz.core.registries import data_parser_registry +from jdaviz.utils import standardize_metadata, PRIHDR_KEY __all__ = ['parse_data'] @@ -45,7 +46,7 @@ def parse_data(app, file_obj, data_type=None, data_label=None): # generic enough to work with other file types (e.g. ASDF). For now, this # supports MaNGA and JWST data. if isinstance(file_obj, fits.hdu.hdulist.HDUList): - _parse_hdu(app, file_obj, file_name=data_label) + _parse_hdulist(app, file_obj, file_name=data_label) elif isinstance(file_obj, str) and os.path.exists(file_obj): file_name = os.path.basename(file_obj) @@ -68,7 +69,7 @@ def parse_data(app, file_obj, data_type=None, data_label=None): _parse_esa_s3d(app, hdulist, data_label, ext=ext, viewer_name=viewer_name) else: - _parse_hdu(app, hdulist, file_name=data_label or file_name) + _parse_hdulist(app, hdulist, file_name=data_label or file_name) # If the data types are custom data objects, use explicit parsers. Note # that this relies on the glue-astronomy machinery to turn the data object @@ -82,7 +83,7 @@ def parse_data(app, file_obj, data_type=None, data_label=None): raise NotImplementedError(f'Unsupported data format: {file_obj}') -def _parse_hdu(app, hdulist, file_name=None): +def _parse_hdulist(app, hdulist, file_name=None): if file_name is None: if hasattr(hdulist, 'file_name'): file_name = hdulist.file_name @@ -116,8 +117,12 @@ def _parse_hdu(app, hdulist, file_name=None): flux = hdu.data << flux_unit + metadata = standardize_metadata(hdu.header) + if hdu.name != 'PRIMARY' and 'PRIMARY' in hdulist: + metadata[PRIHDR_KEY] = standardize_metadata(hdulist['PRIMARY'].header) + try: - sc = Spectrum1D(flux=flux, wcs=wcs) + sc = Spectrum1D(flux=flux, wcs=wcs, meta=metadata) except Exception as e: logging.warning(e) continue @@ -125,12 +130,12 @@ def _parse_hdu(app, hdulist, file_name=None): app.add_data(sc, data_label) # If the data type is some kind of integer, assume it's the mask/dq - if hdu.data.dtype in (int, np.uint, np.uint32) or \ - any(x in hdu.name.lower() for x in EXT_TYPES['mask']): + if (hdu.data.dtype in (int, np.uint, np.uint32) or + any(x in hdu.name.lower() for x in EXT_TYPES['mask'])): app.add_data_to_viewer('mask-viewer', data_label) - if 'errtype' in [x.lower() for x in hdu.header.keys()] or \ - any(x in hdu.name.lower() for x in EXT_TYPES['uncert']): + if ('errtype' in [x.lower() for x in hdu.header.keys()] or + any(x in hdu.name.lower() for x in EXT_TYPES['uncert'])): app.add_data_to_viewer('uncert-viewer', data_label) if any(x in hdu.name.lower() for x in EXT_TYPES['flux']): @@ -161,11 +166,16 @@ def _parse_jwst_s3d(app, hdulist, data_label, ext='SCI', viewer_name='flux-viewe unit = u.Unit(hdulist[ext].header.get('BUNIT', 'count')) flux = hdulist[ext].data << unit wcs = WCS(hdulist['SCI'].header, hdulist) # Everything uses SCI WCS - data = Spectrum1D(flux, wcs=wcs) + + metadata = standardize_metadata(hdulist[ext].header) + if hdulist[ext].name != 'PRIMARY' and 'PRIMARY' in hdulist: + metadata[PRIHDR_KEY] = standardize_metadata(hdulist['PRIMARY'].header) + + data = Spectrum1D(flux, wcs=wcs, meta=metadata) # NOTE: Tried to only pass in sliced WCS but got error in Glue. # sliced_wcs = wcs[:, 0, 0] # Only want wavelengths - # data = Spectrum1D(flux, wcs=sliced_wcs) + # data = Spectrum1D(flux, wcs=sliced_wcs, meta=metadata) app.add_data(data, data_label) app.add_data_to_viewer(viewer_name, data_label) @@ -194,7 +204,13 @@ def _parse_esa_s3d(app, hdulist, data_label, ext='DATA', viewer_name='flux-viewe wcs = WCS(wcs_dict) flux = np.moveaxis(flux, 0, -1) flux = np.swapaxes(flux, 0, 1) - data = Spectrum1D(flux, wcs=wcs) + + metadata = standardize_metadata(hdulist[ext].header) + metadata.update(wcs_dict) # To be internally consistent + if hdulist[ext].name != 'PRIMARY' and 'PRIMARY' in hdulist: + metadata[PRIHDR_KEY] = standardize_metadata(hdulist['PRIMARY'].header) + + data = Spectrum1D(flux, wcs=wcs, meta=metadata) app.add_data(data, data_label) app.add_data_to_viewer(viewer_name, data_label) @@ -225,7 +241,7 @@ def _parse_spectrum1d_3d(app, file_obj, data_label=None): flux = np.moveaxis(flux, 1, 0) - s1d = Spectrum1D(flux=flux, wcs=file_obj.wcs) + s1d = Spectrum1D(flux=flux, wcs=file_obj.wcs, meta=standardize_metadata(file_obj.meta)) cur_data_label = f"{data_label}[{attr.upper()}]" app.add_data(s1d, cur_data_label) diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index 4da5089072..b7453ede07 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -5,6 +5,8 @@ from astropy.wcs import WCS from specutils import Spectrum1D +from jdaviz.utils import PRIHDR_KEY + @pytest.fixture def image_hdu_obj(): @@ -58,6 +60,12 @@ def test_fits_image_hdu_parse_from_file(tmpdir, image_hdu_obj, cubeviz_helper): # This tests the same data as test_fits_image_hdu_parse above. + cubeviz_helper.app.data_collection[0].meta['EXTNAME'] == 'FLUX' + cubeviz_helper.app.data_collection[1].meta['EXTNAME'] == 'MASK' + cubeviz_helper.app.data_collection[2].meta['EXTNAME'] == 'ERR' + for i in range(3): + assert cubeviz_helper.app.data_collection[i].meta[PRIHDR_KEY]['BITPIX'] == 8 + flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') flux_viewer.on_mouse_or_key_event({'event': 'mousemove', 'domain': {'x': 0, 'y': 0}}) assert flux_viewer.label_mouseover.pixel == 'x=00.0 y=00.0' @@ -114,6 +122,7 @@ def test_spectrum1d_parse(spectrum1d, cubeviz_helper): assert len(cubeviz_helper.app.data_collection) == 1 assert cubeviz_helper.app.data_collection[0].label.endswith('[FLUX]') + assert cubeviz_helper.app.data_collection[0].meta['uncertainty_type'] == 'std' # Coordinate display is only for spatial image, which is missing here. flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') diff --git a/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.py b/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.py index f2bbd7068b..4109912e31 100644 --- a/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.py +++ b/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.py @@ -1,8 +1,8 @@ -from astropy.io.fits import Header from traitlets import Bool, List, observe from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import TemplateMixin, DatasetSelectMixin +from jdaviz.utils import PRIHDR_KEY, COMMENTCARD_KEY __all__ = ['MetadataViewer'] @@ -11,6 +11,9 @@ class MetadataViewer(TemplateMixin, DatasetSelectMixin): template_file = __file__, "metadata_viewer.vue" has_metadata = Bool(False).tag(sync=True) + has_primary = Bool(False).tag(sync=True) + show_primary = Bool(False).tag(sync=True) + has_comments = Bool(False).tag(sync=True) metadata = List([]).tag(sync=True) def __init__(self, *args, **kwargs): @@ -18,29 +21,82 @@ def __init__(self, *args, **kwargs): # override the default filters on dataset entries to require metadata in entries self.dataset.add_filter('not_from_plugin') + def reset(self): + self.has_metadata = False + self.has_primary = False + self.show_primary = False + self.has_comments = False + self.metadata = [] + @observe("dataset_selected") - def _show_metadata(self, event): + def show_metadata(self, event): data = self.dataset.selected_dc_item - if data is None or not hasattr(data, 'meta') or not isinstance(data.meta, dict) or len(data.meta) < 1: # noqa - self.has_metadata = False - self.metadata = [] + if (data is None or not hasattr(data, 'meta') or not isinstance(data.meta, dict) + or len(data.meta) < 1): + self.reset() return - if 'header' in data.meta and isinstance(data.meta['header'], (dict, Header)): - if isinstance(data.meta['header'], Header): # Specviz - meta = dict(data.meta['header']) - else: - meta = data.meta['header'] + if PRIHDR_KEY in data.meta: + self.has_primary = True else: - meta = data.meta + self.has_primary = False + self.show_primary = False + + self.find_public_metadata(data.meta, primary_only=self.show_primary) + + @observe("show_primary") + def handle_show_primary(self, event): + if not self.show_primary: + self.show_metadata(event) + return + + data = self.dataset.selected_dc_item + if (data is None or not hasattr(data, 'meta') or not isinstance(data.meta, dict) + or len(data.meta) < 1): + self.reset() + return + + self.find_public_metadata(data.meta, primary_only=True) + + def find_public_metadata(self, meta, primary_only=False): + if primary_only: + if PRIHDR_KEY in meta: + meta = meta[PRIHDR_KEY] + else: + self.reset() + return d = flatten_nested_dict(meta) - for badkey in ('COMMENT', 'HISTORY', ''): + # Some FITS keywords cause "# ipykernel cannot clean for JSON" messages. + # Also, we want to hide internal metadata that starts with underscore. + badkeys = ['COMMENT', 'HISTORY', ''] + [k for k in d if k.startswith('_')] + for badkey in badkeys: if badkey in d: - del d[badkey] # ipykernel cannot clean for JSON + del d[badkey] + + if COMMENTCARD_KEY in meta: + has_comments = True + + def get_comment(key): + if key in meta[COMMENTCARD_KEY]._header: + val = meta[COMMENTCARD_KEY][key] + else: + val = '' + return val + else: + has_comments = False + + def get_comment(key): + return '' + # TODO: Option to not sort? - self.metadata = sorted(zip(d.keys(), map(str, d.values()))) - self.has_metadata = True + public_meta = sorted(zip(d.keys(), map(str, d.values()), map(get_comment, d.keys()))) + if len(public_meta) > 0: + self.metadata = public_meta + self.has_metadata = True + self.has_comments = has_comments + else: + self.reset() # TODO: If this is natively supported by asdf in the future, replace with native function. diff --git a/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.vue b/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.vue index 988bafa1b1..394c6050cb 100644 --- a/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.vue +++ b/jdaviz/configs/default/plugins/metadata_viewer/metadata_viewer.vue @@ -14,11 +14,21 @@ hint="Select the data to see metadata." /> + + + + + Metadata
Key Value + Comment {{ item[0] }} {{ item[1] }} + {{ item[2] }}
diff --git a/jdaviz/configs/default/plugins/metadata_viewer/tests/test_metadata_viewer.py b/jdaviz/configs/default/plugins/metadata_viewer/tests/test_metadata_viewer.py index 897c374ab5..33ec26697e 100644 --- a/jdaviz/configs/default/plugins/metadata_viewer/tests/test_metadata_viewer.py +++ b/jdaviz/configs/default/plugins/metadata_viewer/tests/test_metadata_viewer.py @@ -1,5 +1,6 @@ import numpy as np import pytest +from astropy.io import fits from astropy.nddata import NDData from jdaviz.configs.default.plugins.metadata_viewer.metadata_viewer import MetadataViewer @@ -7,31 +8,77 @@ def test_view_dict(imviz_helper): mv = MetadataViewer(app=imviz_helper.app) - ndd_1 = NDData(np.zeros((2, 2)), meta={ - 'EXTNAME': 'SCI', 'EXTVER': 1, 'BAR': 10.0, - 'FOO': '', 'COMMENT': 'a test', 'BOZO': None}) - ndd_2 = NDData(np.ones((2, 2)), meta={ + arr = np.zeros((2, 2), dtype=np.float32) + ndd_1 = NDData(arr, meta={ + 'EXTNAME': 'SCI', 'EXTVER': 1, 'BAR': 10.0, '_hidden': 'no show', + 'HISTORY': 'Hmm', '': 'Invalid', 'FOO': '', 'COMMENT': 'a test', 'BOZO': None}) + ndd_2 = NDData(arr, meta={ 'EXTNAME': 'ASDF', 'REF': {'bar': 10.0, 'foo': {'1': '', '2': [1, 2]}}}) - arr = np.zeros((2, 2)) + + # MEF + ndd_3 = fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(arr)]) + ndd_3[1].name = 'DATA' + ndd_4 = fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(arr)]) + ndd_4[0].header['APERTURE'] = ('#TODO', 'Aperture') + ndd_4[1].name = 'DATA' + imviz_helper.load_data(ndd_1, data_label='has_simple_meta') imviz_helper.load_data(ndd_2, data_label='has_nested_meta') + imviz_helper.load_data(ndd_3, data_label='has_primary') + imviz_helper.load_data(ndd_4, data_label='has_primary_2') imviz_helper.load_data(arr, data_label='no_meta') - assert mv.dataset.labels == ['has_simple_meta[DATA]', 'has_nested_meta[DATA]', 'no_meta'] + assert mv.dataset.labels == ['has_simple_meta[DATA]', 'has_nested_meta[DATA]', + 'has_primary[DATA,1]', 'has_primary_2[DATA,1]', 'no_meta'] mv.dataset_selected = 'has_simple_meta[DATA]' + assert not mv.has_primary + assert not mv.show_primary + assert not mv.has_comments assert mv.has_metadata assert mv.metadata == [ - ('BAR', '10.0'), ('BOZO', 'None'), ('EXTNAME', 'SCI'), - ('EXTVER', '1'), ('FOO', '')], mv.metadata + ('BAR', '10.0', ''), ('BOZO', 'None', ''), ('EXTNAME', 'SCI', ''), + ('EXTVER', '1', ''), ('FOO', '', '')] mv.dataset_selected = 'has_nested_meta[DATA]' + assert not mv.has_primary + assert not mv.show_primary + assert not mv.has_comments assert mv.has_metadata assert mv.metadata == [ - ('EXTNAME', 'ASDF'), ('REF.bar', '10.0'), - ('REF.foo.1', ''), ('REF.foo.2.0', '1'), ('REF.foo.2.1', '2')], mv.metadata + ('EXTNAME', 'ASDF', ''), ('REF.bar', '10.0', ''), + ('REF.foo.1', '', ''), ('REF.foo.2.0', '1', ''), ('REF.foo.2.1', '2', '')] + + mv.dataset_selected = 'has_primary[DATA,1]' + assert mv.has_primary + assert not mv.show_primary + assert mv.has_comments + assert mv.has_metadata + assert mv.metadata == [('BITPIX', '-32', 'array data type'), + ('EXTNAME', 'DATA', 'extension name'), + ('GCOUNT', '1', 'number of groups'), + ('NAXIS', '2', 'number of array dimensions'), + ('NAXIS1', '2', ''), ('NAXIS2', '2', ''), + ('PCOUNT', '0', 'number of parameters'), + ('XTENSION', 'IMAGE', 'Image extension')] + mv.show_primary = True + assert mv.metadata == [('BITPIX', '8', 'array data type'), ('EXTEND', 'True', ''), + ('NAXIS', '0', 'number of array dimensions'), + ('SIMPLE', 'True', 'conforms to FITS standard')] + + mv.dataset_selected = 'has_primary_2[DATA,1]' + assert mv.show_primary # Make sure it sticks if possible + assert mv.has_comments + assert mv.metadata == [('APERTURE', '#TODO', 'Aperture'), + ('BITPIX', '8', 'array data type'), ('EXTEND', 'True', ''), + ('NAXIS', '0', 'number of array dimensions'), + ('SIMPLE', 'True', 'conforms to FITS standard')] mv.dataset_selected = 'no_meta' + assert not mv.has_primary + assert not mv.show_primary + assert not mv.has_comments assert not mv.has_metadata + assert mv.metadata == [] def test_view_invalid(imviz_helper): @@ -42,5 +89,8 @@ def test_view_invalid(imviz_helper): with pytest.raises(ValueError): mv.dataset_selected = 'foo' assert mv.dataset_selected == '' + assert not mv.has_primary + assert not mv.show_primary + assert not mv.has_comments assert not mv.has_metadata assert mv.metadata == [] diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index 84074ad388..bff9e4b3a6 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -17,7 +17,7 @@ from jdaviz.core.region_translators import regions2aperture from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import TemplateMixin, DatasetSelectMixin, SubsetSelect -from jdaviz.utils import bqplot_clear_figure +from jdaviz.utils import bqplot_clear_figure, PRIHDR_KEY __all__ = ['SimpleAperturePhotometry'] @@ -83,7 +83,10 @@ def _dataset_selected_changed(self, event={}): self.flux_scaling = 0 # Extract telescope specific unit conversion factors, if applicable. - meta = self._selected_data.meta + meta = self._selected_data.meta.copy() + if PRIHDR_KEY in meta: + meta.update(meta[PRIHDR_KEY]) + del meta[PRIHDR_KEY] if 'telescope' in meta: telescope = meta['telescope'] else: diff --git a/jdaviz/configs/imviz/plugins/parsers.py b/jdaviz/configs/imviz/plugins/parsers.py index f0fb62842a..f08a4115e0 100644 --- a/jdaviz/configs/imviz/plugins/parsers.py +++ b/jdaviz/configs/imviz/plugins/parsers.py @@ -12,7 +12,7 @@ from jdaviz.core.registries import data_parser_registry from jdaviz.core.events import SnackbarMessage - +from jdaviz.utils import standardize_metadata, PRIHDR_KEY __all__ = ['parse_data'] @@ -230,7 +230,7 @@ def _jwst2data(file_obj, ext, data_label): with AsdfInFits.open(file_obj) as af: dm = af.tree dm_meta = af.tree["meta"] - data.meta.update(dm_meta) + data.meta.update(standardize_metadata(dm_meta)) if unit_attr in dm_meta: bunit = _validate_bunit(dm_meta[unit_attr], raise_error=False) @@ -287,8 +287,8 @@ def _hdu2data(hdu, data_label, hdulist, include_wcs=True): data = Data(label=new_data_label) if hdulist is not None and hdu.name != 'PRIMARY' and 'PRIMARY' in hdulist: - data.meta.update(dict(hdulist['PRIMARY'].header)) - data.meta.update(dict(hdu.header)) + data.meta[PRIHDR_KEY] = standardize_metadata(hdulist['PRIMARY'].header) + data.meta.update(standardize_metadata(hdu.header)) if include_wcs: data.coords = WCS(hdu.header, hdulist) component = Component.autotyped(hdu.data, units=bunit) @@ -310,7 +310,7 @@ def _nddata_to_glue_data(ndd, data_label): comp_label = attrib.upper() cur_label = f'{data_label}[{comp_label}]' cur_data = Data(label=cur_label) - cur_data.meta.update(ndd.meta) + cur_data.meta.update(standardize_metadata(ndd.meta)) if ndd.wcs is not None: cur_data.coords = ndd.wcs raw_arr = arr diff --git a/jdaviz/configs/mosviz/plugins/parsers.py b/jdaviz/configs/mosviz/plugins/parsers.py index 7aa9d6fdf9..ba2bc639cf 100644 --- a/jdaviz/configs/mosviz/plugins/parsers.py +++ b/jdaviz/configs/mosviz/plugins/parsers.py @@ -17,6 +17,7 @@ from jdaviz.configs.imviz.plugins.parsers import get_image_data_iterator from jdaviz.core.registries import data_parser_registry from jdaviz.core.events import SnackbarMessage +from jdaviz.utils import standardize_metadata, PRIHDR_KEY __all__ = ['mos_spec1d_parser', 'mos_spec2d_parser', 'mos_image_parser'] @@ -220,8 +221,11 @@ def mos_spec1d_parser(app, data_obj, data_labels=None): with app.data_collection.delay_link_manager_update(): - for i in range(len(data_obj)): - app.add_data(data_obj[i], data_labels[i], notify_done=False) + for cur_data, cur_label in zip(data_obj, data_labels): + # Make metadata layout conform with other viz. + cur_data.meta = standardize_metadata(cur_data.meta) + + app.add_data(cur_data, cur_label, notify_done=False) _add_to_table(app, data_labels, '1D Spectra') @@ -253,7 +257,9 @@ def _parse_as_spectrum1d(path): data = hdulist[1].data header = hdulist[1].header wcs = WCS(header) - return Spectrum1D(data, wcs=wcs) + metadata = standardize_metadata(header) + metadata[PRIHDR_KEY] = standardize_metadata(hdulist[0].header) + return Spectrum1D(data, wcs=wcs, meta=metadata) # Coerce into list-like object if not isinstance(data_obj, (list, tuple, SpectrumCollection)): @@ -283,11 +289,8 @@ def _parse_as_spectrum1d(path): except IORegistryError: data = _parse_as_spectrum1d(data) - # Copy (if present) region to top-level meta object - if ('header' in data.meta and - 'S_REGION' in data.meta['header'] and - 'S_REGION' not in data.meta): - data.meta['S_REGION'] = data.meta['header']['S_REGION'] + # Make metadata layout conform with other viz. + data.meta = standardize_metadata(data.meta) # Set the instrument # TODO: this should not be set to nirspec for all datasets @@ -309,13 +312,9 @@ def _parse_as_spectrum1d(path): def _load_fits_image_from_filename(filename, app): - data_list = [] with fits.open(filename) as hdulist: - meta = dict(hdulist[0].header.copy()) - data_iter = get_image_data_iterator(app, hdulist, "Image", ext=None) - for d, _ in data_iter: # We do not use the generated labels - d.meta.update(meta) - data_list.append(d) + # We do not use the generated labels + data_list = [d for d, _ in get_image_data_iterator(app, hdulist, "Image", ext=None)] return data_list @@ -497,10 +496,10 @@ def _get_source_identifiers_by_hdu(hdus, filepaths=None, header_keys=['SOURCEID' # Fallback 1: filepath if only one is given # Fallback 2: filepath at indx, if list of files given # Fallback 3: If nothing else, just our fallback value - src_name = \ - os.path.basename(filepaths) if type(filepaths) is str \ - else os.path.basename(filepaths[indx]) if type(filepaths) is list \ - else FALLBACK_NAME + src_name = ( + os.path.basename(filepaths) if type(filepaths) is str + else os.path.basename(filepaths[indx]) if type(filepaths) is list + else FALLBACK_NAME) src_names.append(src_name) except Exception: # Source ID lookup shouldn't ever prevent target from loading. Downgrade all errors to @@ -636,7 +635,8 @@ def mos_niriss_parser(app, data_dir, obs_label=""): if temp[sci].header["SPORDER"] == 1: data = temp[sci].data - meta = temp[sci].header + meta = standardize_metadata(temp[sci].header) + meta[PRIHDR_KEY] = standardize_metadata(temp[0].header) # The wavelength is stored in a WAVELENGTH HDU. This is # a 2D array, but in order to be able to use Spectrum1D @@ -647,10 +647,7 @@ def mos_niriss_parser(app, data_dir, obs_label=""): spec2d.meta['INSTRUME'] = 'NIRISS' - label = "{} Source {} spec2d {}".format(filter_name, - temp[sci].header["SOURCEID"], - orientation - ) + label = f"{filter_name} Source {temp[sci].header['SOURCEID']} spec2d {orientation}" # noqa ra, dec = pupil_id_dict[filter_name][temp[sci].header["SOURCEID"]] ras.append(ra) decs.append(dec) @@ -670,8 +667,8 @@ def mos_niriss_parser(app, data_dir, obs_label=""): with fits.open(fname, memmap=False) as temp: # TODO: Remove this once valid SRCTYPE values are present in all headers for hdu in temp: - if "SRCTYPE" in hdu.header and\ - (hdu.header["SRCTYPE"] in ["POINT", "EXTENDED"]): + if ("SRCTYPE" in hdu.header and + (hdu.header["SRCTYPE"] in ("POINT", "EXTENDED"))): pass else: hdu.header["SRCTYPE"] = "EXTENDED" @@ -685,13 +682,11 @@ def mos_niriss_parser(app, data_dir, obs_label=""): orientation = f[-1] for spec in specs: - if spec.meta['header']['SPORDER'] == 1 and\ - spec.meta['header']['EXTNAME'] == "EXTRACT1D": + # Make metadata layout conform with other viz. + spec.meta = standardize_metadata(spec.meta) - label = "{} Source {} spec1d {}".format(filter_name, - spec.meta['header']['SOURCEID'], - orientation - ) + if spec.meta['SPORDER'] == 1 and spec.meta['EXTNAME'] == "EXTRACT1D": + label = f"{filter_name} Source {spec.meta['SOURCEID']} spec1d {orientation}" spec_labels_1d.append(label) add_to_glue[label] = spec diff --git a/jdaviz/configs/mosviz/tests/test_data_loading.py b/jdaviz/configs/mosviz/tests/test_data_loading.py index 8dece712bd..15e23910a9 100644 --- a/jdaviz/configs/mosviz/tests/test_data_loading.py +++ b/jdaviz/configs/mosviz/tests/test_data_loading.py @@ -9,13 +9,17 @@ import pytest from specutils import Spectrum1D +from jdaviz.utils import PRIHDR_KEY + def test_load_spectrum1d(mosviz_helper, spectrum1d): label = "Test 1D Spectrum" mosviz_helper.load_1d_spectra(spectrum1d, data_labels=label) assert len(mosviz_helper.app.data_collection) == 2 - assert mosviz_helper.app.data_collection[0].label == label + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == label + assert dc_0.meta['uncertainty_type'] == 'std' table = mosviz_helper.app.get_viewer('table-viewer') table.widget_table.vue_on_row_clicked(0) @@ -24,7 +28,7 @@ def test_load_spectrum1d(mosviz_helper, spectrum1d): assert isinstance(data[label], Spectrum1D) - with pytest.raises(TypeError): + with pytest.raises(AttributeError): mosviz_helper.load_1d_spectra([1, 2, 3]) @@ -34,7 +38,10 @@ def test_load_image(mosviz_helper, mos_image): mosviz_helper.load_images(mos_image, data_labels=label) assert len(mosviz_helper.app.data_collection) == 2 - assert mosviz_helper.app.data_collection[0].label == f"{label} 0" + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == f"{label} 0" + assert PRIHDR_KEY not in dc_0.meta + assert dc_0.meta['RADESYS'] == 'ICRS' table = mosviz_helper.app.get_viewer('table-viewer') table.widget_table.vue_on_row_clicked(0) @@ -51,7 +58,9 @@ def test_load_spectrum_collection(mosviz_helper, spectrum_collection): mosviz_helper.load_1d_spectra(spectrum_collection, data_labels=labels) assert len(mosviz_helper.app.data_collection) == 6 - assert mosviz_helper.app.data_collection[0].label == labels[0] + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == labels[0] + assert dc_0.meta['uncertainty_type'] == 'std' table = mosviz_helper.app.get_viewer('table-viewer') table.widget_table.vue_on_row_clicked(0) @@ -68,7 +77,9 @@ def test_load_list_of_spectrum1d(mosviz_helper, spectrum1d): mosviz_helper.load_1d_spectra(spectra, data_labels=labels) assert len(mosviz_helper.app.data_collection) == 4 - assert mosviz_helper.app.data_collection[0].label == labels[0] + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == labels[0] + assert dc_0.meta['uncertainty_type'] == 'std' table = mosviz_helper.app.get_viewer('table-viewer') table.widget_table.vue_on_row_clicked(0) @@ -85,7 +96,9 @@ def test_load_mos_spectrum2d(mosviz_helper, mos_spectrum2d): mosviz_helper.load_2d_spectra(mos_spectrum2d, data_labels=label) assert len(mosviz_helper.app.data_collection) == 2 - assert mosviz_helper.app.data_collection[0].label == label + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == label + assert dc_0.meta['INSTRUME'] == 'nirspec' table = mosviz_helper.app.get_viewer('table-viewer') table.widget_table.vue_on_row_clicked(0) @@ -140,7 +153,7 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_ @pytest.mark.filterwarnings('ignore') @pytest.mark.remote_data -def test_nirpsec_loader(mosviz_helper, tmpdir): +def test_nirspec_loader(mosviz_helper, tmpdir): ''' Tests loading our default MosvizExample notebook data ''' @@ -157,12 +170,30 @@ def test_nirpsec_loader(mosviz_helper, tmpdir): mosviz_helper.load_data(directory=data_dir, instrument='nirspec') assert len(mosviz_helper.app.data_collection) == 16 - assert "MOS Table" in mosviz_helper.app.data_collection - assert "Image 4" in mosviz_helper.app.data_collection - assert "1D Spectrum 4" in mosviz_helper.app.data_collection - assert "2D Spectrum 4" in mosviz_helper.app.data_collection + + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == "MOS Table" + assert len(dc_0.meta) == 0 + + dc_5 = mosviz_helper.app.data_collection[5] + assert dc_5.label == "Image 4" + assert PRIHDR_KEY not in dc_5.meta + assert dc_5.meta['WCSAXES'] == 2 + + dc_10 = mosviz_helper.app.data_collection[10] + assert dc_10.label == "1D Spectrum 4" + assert PRIHDR_KEY not in dc_10.meta + assert 'header' not in dc_10.meta + assert dc_10.meta['TARGNAME'] == 'FOO' + + dc_15 = mosviz_helper.app.data_collection[15] + assert dc_15.label == "2D Spectrum 4" + assert PRIHDR_KEY not in dc_15.meta + assert 'header' not in dc_15.meta + assert dc_15.meta['SOURCEID'] == 2315 +# This is another version of test_niriss_parser in test_parsers.py @pytest.mark.remote_data def test_niriss_loader(mosviz_helper, tmpdir): @@ -183,7 +214,7 @@ def test_niriss_loader(mosviz_helper, tmpdir): @pytest.mark.remote_data -def test_nirpsec_fallback(mosviz_helper, tmpdir): +def test_nirspec_fallback(mosviz_helper, tmpdir): ''' When no instrument is provided, mosviz.load_data is expected to fallback to the nirspec loader. Naturally, the nirspec dataset should then work without any instrument keyword diff --git a/jdaviz/configs/mosviz/tests/test_parsers.py b/jdaviz/configs/mosviz/tests/test_parsers.py index 42284cde60..d48d8c5d75 100644 --- a/jdaviz/configs/mosviz/tests/test_parsers.py +++ b/jdaviz/configs/mosviz/tests/test_parsers.py @@ -1,10 +1,13 @@ -import pytest from zipfile import ZipFile import pathlib +import pytest from astropy.utils.data import download_file +from jdaviz.utils import PRIHDR_KEY, COMMENTCARD_KEY + +# This is another version of test_niriss_loader in test_data_loading.py @pytest.mark.remote_data def test_niriss_parser(mosviz_helper, tmpdir): @@ -20,5 +23,26 @@ def test_niriss_parser(mosviz_helper, tmpdir): mosviz_helper.load_niriss_data(data_dir) assert len(mosviz_helper.app.data_collection) == 80 - assert mosviz_helper.app.data_collection[0].label == "Image canucs F150W" - assert mosviz_helper.app.data_collection[-1].label == "MOS Table" + + dc_0 = mosviz_helper.app.data_collection[0] + assert dc_0.label == "Image canucs F150W" + assert PRIHDR_KEY not in dc_0.meta + assert COMMENTCARD_KEY not in dc_0.meta + assert dc_0.meta['bunit_data'] == 'MJy/sr' # ASDF metadata + + dc_1 = mosviz_helper.app.data_collection[1] + assert dc_1.label == 'F150W Source 1 spec2d C' + assert PRIHDR_KEY in dc_1.meta + assert COMMENTCARD_KEY in dc_1.meta + assert dc_1.meta['SOURCEID'] == 1 + + dc_40 = mosviz_helper.app.data_collection[40] + assert dc_40.label == 'F150W Source 1 spec1d C' + assert PRIHDR_KEY not in dc_40.meta + assert COMMENTCARD_KEY in dc_40.meta + assert 'header' not in dc_40.meta + assert dc_40.meta['FILTER'] == 'GR150C' + + dc_tab = mosviz_helper.app.data_collection[-1] + assert dc_tab.label == "MOS Table" + assert len(dc_tab.meta) == 0 diff --git a/jdaviz/configs/specviz/plugins/parsers.py b/jdaviz/configs/specviz/plugins/parsers.py index 56f8529ad0..cf58384d75 100644 --- a/jdaviz/configs/specviz/plugins/parsers.py +++ b/jdaviz/configs/specviz/plugins/parsers.py @@ -3,13 +3,12 @@ import uuid import numpy as np - from astropy.io.registry import IORegistryError from astropy.nddata import StdDevUncertainty - from specutils import Spectrum1D, SpectrumList, SpectrumCollection from jdaviz.core.registries import data_parser_registry +from jdaviz.utils import standardize_metadata __all__ = ["specviz_spectrum1d_parser"] @@ -100,6 +99,9 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v spec = Spectrum1D(flux=spec.flux, spectral_axis=spec.spectral_axis.to(current_unit)) + # Make metadata layout conform with other viz. + spec.meta = standardize_metadata(spec.meta) + app.add_data(spec, data_label[i]) # handle display, with the SpectrumList special case in mind. @@ -132,6 +134,9 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v spec = Spectrum1D(flux=fnuall * flux_units, spectral_axis=wlall * wave_units, uncertainty=unc) + # Make metadata layout conform with other viz. + spec.meta = standardize_metadata(spec.meta) + # needs perhaps a better way to label the combined spectrum label = "Combined " + data_label[0] app.add_data(spec, label) diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index 6b71db428e..68fb8dc408 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -26,7 +26,9 @@ def setup_class(self, specviz_helper, spectrum1d): def test_load_spectrum1d(self): assert len(self.spec_app.app.data_collection) == 1 - assert self.spec_app.app.data_collection[0].label == self.label + dc_0 = self.spec_app.app.data_collection[0] + assert dc_0.label == self.label + assert dc_0.meta['uncertainty_type'] == 'std' data = self.spec_app.app.get_data_from_viewer('spectrum-viewer') @@ -248,9 +250,12 @@ def test_load_spectrum_list_directory(tmpdir, specviz_helper): with pytest.warns(UserWarning, match='SRCTYPE is missing or UNKNOWN in JWST x1d loader'): specviz_helper.load_spectrum(data_path) assert len(specviz_helper.app.data_collection) == 3 - for element in specviz_helper.app.data_collection: - assert element.data.main_components[0] in ['flux'] - assert element.data.main_components[1] in ['uncertainty'] + for data in specviz_helper.app.data_collection: + assert data.main_components[:2] == ['flux', 'uncertainty'] + + dc_0 = specviz_helper.app.data_collection[0] + assert 'header' not in dc_0.meta + assert dc_0.meta['SPORDER'] == 1 def test_plot_uncertainties(specviz_helper, spectrum1d): diff --git a/jdaviz/configs/specviz2d/plugins/parsers.py b/jdaviz/configs/specviz2d/plugins/parsers.py index b6d78f0850..df99a7fcfc 100644 --- a/jdaviz/configs/specviz2d/plugins/parsers.py +++ b/jdaviz/configs/specviz2d/plugins/parsers.py @@ -1,10 +1,12 @@ -from jdaviz.core.registries import data_parser_registry +from pathlib import Path from specutils import Spectrum1D from astropy.io import fits import astropy.units as u import numpy as np -from pathlib import Path + +from jdaviz.core.registries import data_parser_registry +from jdaviz.utils import standardize_metadata, PRIHDR_KEY __all__ = ['spec2d_1d_parser'] @@ -38,6 +40,7 @@ def spec2d_1d_parser(app, data_obj, data_label=None, show_in_viewer=True): with fits.open(data_obj) as hdulist: data = hdulist[1].data header = hdulist[1].header + prihdr = hdulist[0].header # Should only be 2D, so DISPAXIS-1 should be 0 or -1 and sum over the # correct axis. If Unit doesn't understand the BUNIT we leave flux @@ -56,7 +59,10 @@ def spec2d_1d_parser(app, data_obj, data_label=None, show_in_viewer=True): # we use it here as well, even though the actual unit is pixels spectral_axis = np.arange(1, flux.size + 1, 1) * u.m - data_obj = Spectrum1D(flux, spectral_axis=spectral_axis) + metadata = standardize_metadata(header) + metadata[PRIHDR_KEY] = standardize_metadata(prihdr) + + data_obj = Spectrum1D(flux, spectral_axis=spectral_axis, meta=metadata) app.data_collection[data_label] = data_obj diff --git a/jdaviz/configs/specviz2d/specviz2d.yaml b/jdaviz/configs/specviz2d/specviz2d.yaml index 60c8965c2b..f3ebd9a1ce 100644 --- a/jdaviz/configs/specviz2d/specviz2d.yaml +++ b/jdaviz/configs/specviz2d/specviz2d.yaml @@ -12,6 +12,7 @@ toolbar: - g-data-tools - g-subset-tools tray: + - g-metadata-viewer - g-plot-options - g-subset-plugin - g-gaussian-smooth diff --git a/jdaviz/configs/specviz2d/tests/__init__.py b/jdaviz/configs/specviz2d/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py new file mode 100644 index 0000000000..8cb6cccc81 --- /dev/null +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -0,0 +1,33 @@ +import pytest +from asdf.asdf import AsdfWarning +from astropy.utils.data import download_file + +from jdaviz.utils import PRIHDR_KEY + + +@pytest.mark.remote_data +def test_2d_parser(specviz2d_helper): + fn = download_file('https://stsci.box.com/shared/static/exnkul627fcuhy5akf2gswytud5tazmw.fits', cache=True) # noqa + + with pytest.warns(AsdfWarning, match='jwextension'): + specviz2d_helper.load_data(spectrum_2d=fn) + assert len(specviz2d_helper.app.data_collection) == 2 + + dc_0 = specviz2d_helper.app.data_collection[0] + assert dc_0.label == 'Spectrum 2D' + assert PRIHDR_KEY not in dc_0.meta + assert 'header' not in dc_0.meta + assert dc_0.meta['DETECTOR'] == 'MIRIMAGE' + + dc_1 = specviz2d_helper.app.data_collection[1] + assert dc_1.label == 'Spectrum 1D' + assert 'header' not in dc_1.meta + assert dc_1.meta['NAXIS'] == 2 + + +def test_1d_parser(specviz2d_helper, spectrum1d): + specviz2d_helper.load_data(spectrum_1d=spectrum1d) + assert len(specviz2d_helper.app.data_collection) == 1 + dc_0 = specviz2d_helper.app.data_collection[0] + assert dc_0.label == 'Spectrum 1D' + assert dc_0.meta['uncertainty_type'] == 'std' diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index c9ea41783d..1643adc27d 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -99,9 +99,7 @@ def spectrum1d_cube(): "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0} w = WCS(wcs_dict) - spec = Spectrum1D(flux=flux, wcs=w) - - return spec + return Spectrum1D(flux=flux, wcs=w) @pytest.fixture @@ -116,6 +114,7 @@ def mos_spectrum1d(): Unless linking the two is required, try to use the global spectrum1d fixture. ''' spec_axis = np.linspace(6000, 8000, 1024) * u.AA + np.random.seed(42) flux = (np.random.randn(len(spec_axis.value)) + 10*np.exp(-0.001*(spec_axis.value-6563)**2) + spec_axis.value/500) * u.Jy @@ -132,78 +131,36 @@ def mos_spectrum2d(): TODO: This should be reformed to match the global Spectrum1D defined above so that we may deprecate the mos-specific spectrum1d. ''' - header = """ -WCSAXES = 2 / Number of coordinate axes -CRPIX1 = 0.0 / Pixel coordinate of reference point -CRPIX2 = 1024.5 / Pixel coordinate of reference point -CDELT1 = 1E-06 / [m] Coordinate increment at reference point -CDELT2 = 2.9256727777778E-05 / [deg] Coordinate increment at reference point -CUNIT1 = 'm' / Units of coordinate increment and value -CUNIT2 = 'deg' / Units of coordinate increment and value -CTYPE1 = 'WAVE' / Vacuum wavelength (linear) -CTYPE2 = 'OFFSET' / Spatial offset -CRVAL1 = 0.0 / [m] Coordinate value at reference point -CRVAL2 = 5.0 / [deg] Coordinate value at reference point -RADESYS = 'ICRS' / Equatorial coordinate system -SPECSYS = 'BARYCENT' / Reference frame of spectral coordinates -""" - new_hdr = {} - - for line in header.split('\n'): - try: - key, value = line.split('=') - key = key.strip() - value, _ = value.split('/') - value = value.strip() - value = value.strip("'") - except ValueError: - continue - - new_hdr[key] = value - - wcs = WCS(new_hdr) + header = { + 'WCSAXES': 2, + 'CRPIX1': 0.0, 'CRPIX2': 1024.5, + 'CDELT1': 1E-06, 'CDELT2': 2.9256727777778E-05, + 'CUNIT1': 'm', 'CUNIT2': 'deg', + 'CTYPE1': 'WAVE', 'CTYPE2': 'OFFSET', + 'CRVAL1': 0.0, 'CRVAL2': 5.0, + 'RADESYS': 'ICRS', 'SPECSYS': 'BARYCENT'} + wcs = WCS(header) + np.random.seed(42) data = np.random.sample((1024, 15)) * u.one - return Spectrum1D(data, wcs=wcs) + return Spectrum1D(data, wcs=wcs, meta=header) @pytest.fixture def mos_image(): - header = """ -WCSAXES = 2 / Number of coordinate axes -CRPIX1 = 937.0 / Pixel coordinate of reference point -CRPIX2 = 696.0 / Pixel coordinate of reference point -CDELT1 = -1.5182221158397E-05 / [deg] Coordinate increment at reference point -CDELT2 = 1.5182221158397E-05 / [deg] Coordinate increment at reference point -CUNIT1 = 'deg' / Units of coordinate increment and value -CUNIT2 = 'deg' / Units of coordinate increment and value -CTYPE1 = 'RA---TAN' / Right ascension, gnomonic projection -CTYPE2 = 'DEC--TAN' / Declination, gnomonic projection -CRVAL1 = 5.0155198140981 / [deg] Coordinate value at reference point -CRVAL2 = 5.002450989248 / [deg] Coordinate value at reference point -LONPOLE = 180.0 / [deg] Native longitude of celestial pole -LATPOLE = 5.002450989248 / [deg] Native latitude of celestial pole -DATEREF = '1858-11-17' / ISO-8601 fiducial time -MJDREFI = 0.0 / [d] MJD of fiducial time, integer part -MJDREFF = 0.0 / [d] MJD of fiducial time, fractional part -RADESYS = 'ICRS' / Equatorial coordinate system -""" - new_hdr = {} - - for line in header.split('\n'): - try: - key, value = line.split('=') - key = key.strip() - value, _ = value.split('/') - value = value.strip() - value = value.strip("'") - except ValueError: - continue - - new_hdr[key] = value - - wcs = WCS(new_hdr) + header = { + 'WCSAXES': 2, + 'CRPIX1': 937.0, 'CRPIX2': 696.0, + 'CDELT1': -1.5182221158397e-05, 'CDELT2': 1.5182221158397e-05, + 'CUNIT1': 'deg', 'CUNIT2': 'deg', + 'CTYPE1': 'RA---TAN', 'CTYPE2': 'DEC--TAN', + 'CRVAL1': 5.0155198140981, 'CRVAL2': 5.002450989248, + 'LONPOLE': 180.0, 'LATPOLE': 5.002450989248, + 'DATEREF': '1858-11-17', 'MJDREFI': 0.0, 'MJDREFF': 0.0, + 'RADESYS': 'ICRS'} + wcs = WCS(header) + np.random.seed(42) data = np.random.sample((55, 55)) - return CCDData(data, wcs=wcs, unit='Jy') + return CCDData(data, wcs=wcs, unit='Jy', meta=header) try: diff --git a/jdaviz/tests/test_metadata.py b/jdaviz/tests/test_metadata.py new file mode 100644 index 0000000000..ef618a6939 --- /dev/null +++ b/jdaviz/tests/test_metadata.py @@ -0,0 +1,52 @@ +import pytest +from astropy.io import fits + +from jdaviz.utils import standardize_metadata, COMMENTCARD_KEY + + +def test_metadata_plain_dict(): + metadata = {'a': 1, 'b': 2, 'c': {'d': 42}} + out_meta = standardize_metadata(metadata) + assert out_meta == metadata + + # Make sure input is unchanged. + del out_meta['c'] + assert out_meta == {'a': 1, 'b': 2} + assert 'c' in metadata + + +def test_metadata_nested_fits_header(): + hdu = fits.PrimaryHDU() + metadata = {'a': 1, 'header': hdu.header} + out_meta = standardize_metadata(metadata) + assert 'header' not in out_meta + assert sorted(out_meta.keys()) == ['BITPIX', 'EXTEND', 'NAXIS', 'SIMPLE', COMMENTCARD_KEY, 'a'] + assert out_meta[COMMENTCARD_KEY]['BITPIX'] == 'array data type' + assert 'BITPIX' in out_meta[COMMENTCARD_KEY]._header + assert 'a' not in out_meta[COMMENTCARD_KEY]._header + + # Make sure input is unchanged. + assert 'header' in metadata + + +def test_metadata_fits_header(): + hdu = fits.PrimaryHDU() + out_meta = standardize_metadata(hdu.header) + assert sorted(out_meta.keys()) == ['BITPIX', 'EXTEND', 'NAXIS', 'SIMPLE', COMMENTCARD_KEY] + assert out_meta[COMMENTCARD_KEY]['BITPIX'] == 'array data type' + assert 'BITPIX' in out_meta[COMMENTCARD_KEY]._header + + # Make sure input is unchanged. + del out_meta['BITPIX'] + assert 'BITPIX' in hdu.header + + # Make sure you can still nest it afterwards if you want. + # Similar logic is used to separate primary header in Metadata Viewer plugin. + hdu_2 = fits.ImageHDU() + out_meta['image_meta'] = standardize_metadata(hdu_2.header) + assert out_meta['image_meta']['XTENSION'] == 'IMAGE' + + +def test_metadata_invalid(): + with pytest.raises(TypeError, match='metadata must be dictionary or FITS header'): + standardize_metadata([1, 2, 3]) diff --git a/jdaviz/utils.py b/jdaviz/utils.py index 74be42f3c7..741bab6077 100644 --- a/jdaviz/utils.py +++ b/jdaviz/utils.py @@ -1,12 +1,18 @@ +import os import time import threading from collections import deque -import os + +from astropy.io import fits from ipyvue import watch __all__ = [] +# For Metadata Viewer plugin internal use only. +PRIHDR_KEY = '_primary_header' +COMMENTCARD_KEY = '_fits_comment_card' + class SnackbarQueue: ''' @@ -124,3 +130,24 @@ def bqplot_clear_figure(fig): fig.marks = [] fig.axes = [] setattr(fig, 'axis_registry', {}) + + +def standardize_metadata(metadata): + """Standardize given metadata so it can be viewed in + Metadata Viewer plugin. The input can be plain + dictionary or FITS header object. Output is just a plain + dictionary. + """ + if isinstance(metadata, fits.Header): + out_meta = dict(metadata) + out_meta[COMMENTCARD_KEY] = metadata.comments + elif isinstance(metadata, dict): + out_meta = metadata.copy() + # specutils nests it but we do not want nesting + if 'header' in metadata and isinstance(metadata['header'], fits.Header): + out_meta.update(standardize_metadata(metadata['header'])) + del out_meta['header'] + else: + raise TypeError('metadata must be dictionary or FITS header') + + return out_meta