-
Notifications
You must be signed in to change notification settings - Fork 74
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
ENH: Cubeviz metadata viewer #1325
Changes from all commits
25566ee
0ea9c8c
6236cc9
7433fa3
d8f0977
17f542c
07270be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,21 +117,25 @@ 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 | ||
|
||
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) | ||
Comment on lines
+208
to
+211
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems to be a re-used block of code in a few places... could this logic be moved entirely into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, as passing in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean |
||
|
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,36 +11,92 @@ | |
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): | ||
super().__init__(*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('_')] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kecnry , I think we talked about automatically hiding "hidden" metadata? This would do it for you. |
||
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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,18 +14,29 @@ | |
hint="Select the data to see metadata." | ||
/> | ||
|
||
<v-row v-if="has_primary"> | ||
<v-switch | ||
label="Show primary header" | ||
hint="Show MEF primary header metadata instead." | ||
v-model="show_primary" | ||
persistent-hint> | ||
</v-switch> | ||
</v-row> | ||
Comment on lines
+17
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is the most-intuitive.... but that can be covered in docs or iterated based on user-feedback (I don't really have any suggestions except a full extension-dropdown, but that might not be any more clear). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Ginga, we have a checkbox toggle that would mix in primary header keys into the display, together with actual header. Not that that is not messy but no one has complained over there... 🤷 |
||
|
||
<j-plugin-section-header>Metadata</j-plugin-section-header> | ||
<div v-if="has_metadata"> | ||
<v-row no-gutters> | ||
<v-col cols=6><U>Key</U></v-col> | ||
<v-col cols=6><U>Value</U></v-col> | ||
<v-col v-if="has_comments" cols=6><U>Comment</U></v-col> | ||
</v-row> | ||
<v-row | ||
v-for="item in metadata" | ||
:key="item[0]" | ||
no-gutters> | ||
<v-col cols=6>{{ item[0] }}</v-col> | ||
<v-col cols=6>{{ item[1] }}</v-col> | ||
<v-col v-if="has_comments" cols=6>{{ item[2] }}</v-col> | ||
</v-row> | ||
</div> | ||
<v-row v-else> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe worth a mention explaining the primary switch here?