Source code for jdaviz.configs.cubeviz.plugins.parsers

import logging
import os

import numpy as np
from astropy import units as u
from astropy.io import fits
from astropy.wcs import WCS
from specutils import Spectrum1D

from jdaviz.core.registries import data_parser_registry

__all__ = ['parse_data']

EXT_TYPES = dict(flux=['flux', 'sci'],
                 uncert=['ivar', 'err', 'var', 'uncert'],
                 mask=['mask', 'dq'])


[docs]@data_parser_registry("cubeviz-data-parser") def parse_data(app, file_obj, data_type=None, data_label=None): """ Attempts to parse a data file and auto-populate available viewers in cubeviz. Parameters ---------- app : `~jdaviz.app.Application` The application-level object used to reference the viewers. file_path : str The path to a cube-like data file. data_type : str, {'flux', 'mask', 'uncert'} The data type used to explicitly differentiate parsed data. data_label : str, optional The label to be applied to the Glue data component. """ if data_type is not None and data_type.lower() not in ['flux', 'mask', 'uncert']: msg = "Data type must be one of 'flux', 'mask', or 'uncertainty'." logging.error(msg) return msg # If the file object is an hdulist or a string, use the generic parser for # fits files. # TODO: this currently only supports fits files. We will want to make this # 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) elif isinstance(file_obj, str) and os.path.exists(file_obj): file_name = os.path.basename(file_obj) with fits.open(file_obj) as hdulist: prihdr = hdulist[0].header telescop = prihdr.get('TELESCOP', '').lower() filetype = prihdr.get('FILETYPE', '').lower() if telescop == 'jwst' and filetype == '3d ifu cube': for ext, viewer_name in (('SCI', 'flux-viewer'), ('ERR', 'uncert-viewer'), ('DQ', 'mask-viewer')): data_label = f'{file_name}[{ext}]' _parse_jwst_s3d(app, hdulist, data_label, ext=ext, viewer_name=viewer_name) else: _parse_hdu(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 # into something glue can understand. elif isinstance(file_obj, Spectrum1D): if file_obj.flux.ndim == 3: _parse_spectrum1d_3d(app, file_obj) else: _parse_spectrum1d(app, file_obj) else: raise NotImplementedError(f'Unsupported data format: {file_obj}')
def _parse_hdu(app, hdulist, file_name=None): if file_name is None: if hasattr(hdulist, 'file_name'): file_name = hdulist.file_name file_name = file_name or "Unknown HDU object" # Now loop through and attempt to parse the fits extensions as spectral # cube object. If the wcs fails to parse in any case, use the wcs # information we scraped above. for hdu in hdulist: data_label = f"{file_name}[{hdu.name}]" if hdu.data is None or not hdu.is_image or hdu.data.ndim != 3: continue try: wcs = WCS(hdu.header, hdulist) except Exception as e: # TODO: Do we just want to fail here? logging.warning(f"Invalid WCS: {repr(e)}") wcs = None if 'BUNIT' in hdu.header: try: flux_unit = u.Unit(hdu.header['BUNIT']) except Exception: logging.warning("Invalid BUNIT, using count as data unit") flux_unit = u.count else: logging.warning("Missing BUNIT, using count as data unit") flux_unit = u.count flux = hdu.data << flux_unit try: sc = Spectrum1D(flux=flux, wcs=wcs) except Exception as e: logging.warning(e) continue 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']): 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']): app.add_data_to_viewer('uncert-viewer', data_label) if any(x in hdu.name.lower() for x in EXT_TYPES['flux']): app.add_data_to_viewer('flux-viewer', data_label) app.add_data_to_viewer('spectrum-viewer', data_label) def _parse_jwst_s3d(app, hdulist, data_label, ext='SCI', viewer_name='flux-viewer'): from specutils import Spectrum1D # Manually inject MJD-OBS until we can support GWCS, see # https://github.com/spacetelescope/jdaviz/issues/690 and # https://github.com/glue-viz/glue-astronomy/issues/59 if ext == 'SCI' and 'MJD-OBS' not in hdulist[ext].header: for key in ('MJD-BEG', 'DATE-OBS'): # Possible alternatives if key in hdulist[ext].header: if key.startswith('MJD'): hdulist[ext].header['MJD-OBS'] = hdulist[ext].header[key] break else: from astropy.time import Time t = Time(hdulist[ext].header[key]) hdulist[ext].header['MJD-OBS'] = t.mjd break if ext == 'DQ': # DQ flags have no unit flux = hdulist[ext].data << u.dimensionless_unscaled else: 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) # 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) app.add_data(data, data_label) app.add_data_to_viewer(viewer_name, data_label) if viewer_name == 'flux-viewer': app.add_data_to_viewer('spectrum-viewer', data_label) def _parse_spectrum1d_3d(app, file_obj): # Load spectrum1d as a cube for attr in ["flux", "mask", "uncertainty"]: val = getattr(file_obj, attr) if val is None: continue if attr == "mask": flux = val << file_obj.flux.unit elif attr == "uncertainty": if hasattr(val, "array"): flux = u.Quantity(val.array, file_obj.flux.unit) else: continue else: flux = val flux = np.moveaxis(flux, 1, 0) s1d = Spectrum1D(flux=flux, wcs=file_obj.wcs) data_label = f"Unknown spectrum object[{attr.upper()}]" app.add_data(s1d, data_label) if attr == 'flux': app.add_data_to_viewer('flux-viewer', data_label) app.add_data_to_viewer('spectrum-viewer', data_label) elif attr == 'mask': app.add_data_to_viewer('mask-viewer', data_label) else: # 'uncertainty' app.add_data_to_viewer('uncert-viewer', data_label) def _parse_spectrum1d(app, file_obj): data_label = "Unknown spectrum object" # TODO: glue-astronomy translators only look at the flux property of # specutils Spectrum1D objects. Fix to support uncertainties and masks. app.add_data(file_obj, f"{data_label}[FLUX]") app.add_data_to_viewer('spectrum-viewer', f"{data_label}[FLUX]")